Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
66 / 66
89.13% covered (warning)
89.13%
41 / 46
12.60% covered (danger)
12.60%
16 / 127
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Add
100.00% covered (success)
100.00%
66 / 66
89.13% covered (warning)
89.13%
41 / 46
12.60% covered (danger)
12.60%
16 / 127
100.00% covered (success)
100.00%
5 / 5
287.07
100.00% covered (success)
100.00%
1 / 1
 run
100.00% covered (success)
100.00%
40 / 40
89.66% covered (warning)
89.66%
26 / 29
8.62% covered (danger)
8.62%
10 / 116
100.00% covered (success)
100.00%
1 / 1
121.88
 getPhpContent
100.00% covered (success)
100.00%
6 / 6
75.00% covered (warning)
75.00%
3 / 4
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
1.12
 getPhpMigrationName
100.00% covered (success)
100.00%
13 / 13
87.50% covered (warning)
87.50%
7 / 8
33.33% covered (danger)
33.33%
2 / 6
100.00% covered (success)
100.00%
1 / 1
5.67
 getTpqlContent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFirstMigrationDir
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Quma\Commands;
6
7use Celemas\Cli\Opts;
8use Override;
9
10final class Add extends Command
11{
12    protected string $name = 'add-migration';
13    protected string $group = 'Database';
14    protected string $prefix = 'db';
15    protected string $description = 'Initialize a new migration';
16
17    #[Override]
18    public function run(): string|int
19    {
20        $env = $this->env;
21        $opts = new Opts();
22        $fileName = $opts->get('-f', $opts->get('--file', ''));
23
24        if ($fileName === '') {
25            // Would stop the test suit and wait for input
26            // @codeCoverageIgnoreStart
27            $input = readline('Name of the migration script: ');
28
29            if ($input === false) {
30                echo "No input provided. Aborting.\n";
31
32                return 1;
33            }
34            $fileName = $input;
35
36            // @codeCoverageIgnoreEnd
37        }
38
39        $fileName = str_replace(' ', '-', $fileName);
40        $fileName = str_replace('_', '-', $fileName);
41        $fileName = strtolower($fileName);
42        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
43
44        if (!$ext) {
45            $fileName .= '.sql';
46        } else {
47            if (!in_array($ext, ['sql', 'php', 'tpql'], strict: true)) {
48                echo "Wrong file extension '{$ext}'. Use 'sql', 'php' or 'tpql' instead.\nAborting.\n";
49
50                return 1;
51            }
52        }
53
54        $migrations = $env->conn->config->migrations;
55
56        if (count($migrations) === 0) {
57            echo "No migration directories configured. Aborting.\n";
58
59            return 1;
60        }
61
62        // Get the first migrations directory from the config
63        // Handles both flat list and namespaced formats
64        $migrationsDir = $this->getFirstMigrationDir($migrations);
65
66        if ($migrationsDir === null) {
67            echo "No valid migration directory found. Aborting.\n";
68
69            return 1;
70        }
71
72        if (str_contains($migrationsDir, '/vendor')) {
73            echo "The migrations directory is inside './vendor'.\n  -> {$migrationsDir}\nAborting.\n";
74
75            return 1;
76        }
77
78        if (!is_writable($migrationsDir)) {
79            echo "Migrations directory is not writable\n  -> {$migrationsDir}\nAborting. \n";
80
81            return 1;
82        }
83
84        $timestamp = date('ymd-His', time());
85
86        $migration = $migrationsDir . DIRECTORY_SEPARATOR . $timestamp . '-' . $fileName;
87        $f = fopen($migration, 'w');
88
89        if ($f === false) {
90            echo "Could not create migration file: {$migration}\nAborting.\n";
91
92            return 1;
93        }
94
95        if ($ext === 'php') {
96            fwrite($f, $this->getPhpContent($fileName, $timestamp));
97        } elseif ($ext === 'tpql') {
98            fwrite($f, $this->getTpqlContent());
99        }
100
101        fclose($f);
102        echo "Migration created:\n{$migration}\n";
103
104        return $migration;
105    }
106
107    protected function getPhpContent(string $fileName, string $timestamp): string
108    {
109        $name = $this->getPhpMigrationName($fileName);
110        $namespace = 'Quma\\Migrations\\M' . str_replace('-', '_', $timestamp) . '_' . $name;
111
112        return "<?php
113
114declare(strict_types=1);
115
116namespace {$namespace};
117
118use Celemas\\Quma\\Contract;
119use Celemas\\Quma\\Environment;
120
121class Migration implements Contract\\Migration
122{
123    public function run(Environment \$env): void
124    {
125        throw new \\LogicException('Implement migration {$name} before running it.');
126    }
127}
128
129return Migration::class;";
130    }
131
132    protected function getPhpMigrationName(string $fileName): string
133    {
134        $parts = preg_split(
135            '/[^a-zA-Z0-9]+/',
136            pathinfo($fileName, PATHINFO_FILENAME),
137            -1,
138            PREG_SPLIT_NO_EMPTY,
139        );
140
141        if ($parts === false || count($parts) === 0) {
142            return 'Migration';
143        }
144
145        $words = array_map(
146            static fn(string $part): string => ucfirst(strtolower($part)),
147            $parts,
148        );
149
150        return implode('', $words);
151    }
152
153    protected function getTpqlContent(): string
154    {
155        return "<?php if (\$driver === 'pgsql') : ?>
156
157<?php else : ?>
158
159<?php endif ?>
160";
161    }
162
163    /**
164     * Gets the first migration directory from the config.
165     *
166     * Handles both flat list and namespaced formats.
167     *
168     * @param array<int|string, string|list<string>> $migrations
169     */
170    protected function getFirstMigrationDir(array $migrations): ?string
171    {
172        $first = reset($migrations);
173
174        if ($first === false) {
175            return null; // @codeCoverageIgnore
176        }
177
178        // If it's a string, return it directly
179        if (is_string($first)) {
180            return $first;
181        }
182
183        // It's a list, return the first element
184        return $first[0] ?? null;
185    }
186}