Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
75 / 75
91.84% covered (success)
91.84%
45 / 49
52.50% covered (warning)
52.50%
21 / 40
42.86% covered (danger)
42.86%
3 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Add
100.00% covered (success)
100.00%
75 / 75
91.84% covered (success)
91.84%
45 / 49
52.50% covered (warning)
52.50%
21 / 40
100.00% covered (success)
100.00%
7 / 7
79.69
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
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
 __invoke
100.00% covered (success)
100.00%
34 / 34
95.00% covered (success)
95.00%
19 / 20
60.00% covered (warning)
60.00%
9 / 15
100.00% covered (success)
100.00%
1 / 1
14.18
 fileName
100.00% covered (success)
100.00%
14 / 14
90.91% covered (success)
90.91%
10 / 11
38.46% covered (danger)
38.46%
5 / 13
100.00% covered (success)
100.00%
1 / 1
10.83
 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 Celema\Quma\Commands;
6
7use Celema\Console\Arg;
8use Celema\Console\Args;
9use Celema\Console\Command;
10use Celema\Console\Io;
11use Celema\Console\Opt;
12use Celema\Quma\Connection;
13use Celema\Quma\Environment;
14
15#[Command('db:add-migration', 'Initialize a new migration', group: 'Database')]
16#[Arg(
17    'name',
18    'Name of the migration script; prompted for interactively when omitted',
19    optional: true,
20)]
21#[Opt('--conn', 'Connection to use', value: 'name')]
22final class Add
23{
24    private readonly Environment $env;
25
26    /** @param array<non-empty-string, Connection>|Connection $conn */
27    public function __construct(array|Connection $conn, array $options = [])
28    {
29        $this->env = new Environment($conn, $options);
30    }
31
32    public function __invoke(Args $args, Io $io): int
33    {
34        $env = $this->env;
35        $fileName = $this->fileName($args, $io);
36
37        if ($fileName === null) {
38            return 1;
39        }
40
41        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
42        $migrations = $env->conn->config->migrations;
43
44        if (count($migrations) === 0) {
45            $io->echoln('No migration directories configured. Aborting.');
46
47            return 1;
48        }
49
50        // Get the first migrations directory from the config
51        // Handles both flat list and namespaced formats
52        $migrationsDir = $this->getFirstMigrationDir($migrations);
53
54        if ($migrationsDir === null) {
55            $io->echoln('No valid migration directory found. Aborting.');
56
57            return 1;
58        }
59
60        if (str_contains($migrationsDir, '/vendor')) {
61            $io->echoln(
62                "The migrations directory is inside './vendor'.\n  -> {$migrationsDir}\nAborting.",
63            );
64
65            return 1;
66        }
67
68        if (!is_writable($migrationsDir)) {
69            $io->echoln("Migrations directory is not writable\n  -> {$migrationsDir}\nAborting. ");
70
71            return 1;
72        }
73
74        $timestamp = date('ymd-His', time());
75
76        $migration = $migrationsDir . DIRECTORY_SEPARATOR . $timestamp . '-' . $fileName;
77        $f = fopen($migration, 'w');
78
79        if ($f === false) {
80            $io->echoln("Could not create migration file: {$migration}\nAborting.");
81
82            return 1;
83        }
84
85        if ($ext === 'php') {
86            fwrite($f, $this->getPhpContent($fileName, $timestamp));
87        } elseif ($ext === 'tpql') {
88            fwrite($f, $this->getTpqlContent());
89        }
90
91        fclose($f);
92        $io->echoln("Migration created:\n{$migration}");
93
94        return 0;
95    }
96
97    /**
98     * Resolves the migration file name from the argument or a prompt.
99     *
100     * Returns null when no name was provided or the extension is invalid.
101     */
102    private function fileName(Args $args, Io $io): ?string
103    {
104        $fileName = (string) $args->positional(0, '');
105
106        if ($fileName === '') {
107            $fileName = $io->ask('Name of the migration script:');
108
109            if ($fileName === '') {
110                $io->echoln('No input provided. Aborting.');
111
112                return null;
113            }
114        }
115
116        $fileName = strtolower(str_replace([' ', '_'], '-', $fileName));
117        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
118
119        if (!$ext) {
120            return $fileName . '.sql';
121        }
122
123        if (!in_array($ext, ['sql', 'php', 'tpql'], strict: true)) {
124            $io->echoln("Wrong file extension '{$ext}'. Use 'sql', 'php' or 'tpql' instead.\nAborting.");
125
126            return null;
127        }
128
129        return $fileName;
130    }
131
132    protected function getPhpContent(string $fileName, string $timestamp): string
133    {
134        $name = $this->getPhpMigrationName($fileName);
135        $namespace = 'Quma\\Migrations\\M' . str_replace('-', '_', $timestamp) . '_' . $name;
136
137        return "<?php
138
139declare(strict_types=1);
140
141namespace {$namespace};
142
143use Celema\\Quma\\Contract;
144use Celema\\Quma\\Environment;
145
146class Migration implements Contract\\Migration
147{
148    public function run(Environment \$env): void
149    {
150        throw new \\LogicException('Implement migration {$name} before running it.');
151    }
152}
153
154return Migration::class;";
155    }
156
157    protected function getPhpMigrationName(string $fileName): string
158    {
159        $parts = preg_split(
160            '/[^a-zA-Z0-9]+/',
161            pathinfo($fileName, PATHINFO_FILENAME),
162            -1,
163            PREG_SPLIT_NO_EMPTY,
164        );
165
166        if ($parts === false || count($parts) === 0) {
167            return 'Migration';
168        }
169
170        $words = array_map(
171            static fn(string $part): string => ucfirst(strtolower($part)),
172            $parts,
173        );
174
175        return implode('', $words);
176    }
177
178    protected function getTpqlContent(): string
179    {
180        return "<?php if (\$driver === 'pgsql') : ?>
181
182<?php else : ?>
183
184<?php endif ?>
185";
186    }
187
188    /**
189     * Gets the first migration directory from the config.
190     *
191     * Handles both flat list and namespaced formats.
192     *
193     * @param array<int|string, string|list<string>> $migrations
194     */
195    protected function getFirstMigrationDir(array $migrations): ?string
196    {
197        $first = reset($migrations);
198
199        if ($first === false) {
200            return null; // @codeCoverageIgnore
201        }
202
203        // If it's a string, return it directly
204        if (is_string($first)) {
205            return $first;
206        }
207
208        // It's a list, return the first element
209        return $first[0] ?? null;
210    }
211}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Add->__construct
27    public function __construct(array|Connection $conn, array $options = [])
28    {
29        $this->env = new Environment($conn, $options);
30    }
Add->__invoke
32    public function __invoke(Args $args, Io $io): int
33    {
34        $env = $this->env;
35        $fileName = $this->fileName($args, $io);
36
37        if ($fileName === null) {
38            return 1;
41        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
42        $migrations = $env->conn->config->migrations;
43
44        if (count($migrations) === 0) {
45            $io->echoln('No migration directories configured. Aborting.');
46
47            return 1;
52        $migrationsDir = $this->getFirstMigrationDir($migrations);
53
54        if ($migrationsDir === null) {
55            $io->echoln('No valid migration directory found. Aborting.');
56
57            return 1;
60        if (str_contains($migrationsDir, '/vendor')) {
60        if (str_contains($migrationsDir, '/vendor')) {
60        if (str_contains($migrationsDir, '/vendor')) {
60        if (str_contains($migrationsDir, '/vendor')) {
61            $io->echoln(
62                "The migrations directory is inside './vendor'.\n  -> {$migrationsDir}\nAborting.",
63            );
64
65            return 1;
68        if (!is_writable($migrationsDir)) {
69            $io->echoln("Migrations directory is not writable\n  -> {$migrationsDir}\nAborting. ");
70
71            return 1;
74        $timestamp = date('ymd-His', time());
75
76        $migration = $migrationsDir . DIRECTORY_SEPARATOR . $timestamp . '-' . $fileName;
77        $f = fopen($migration, 'w');
78
79        if ($f === false) {
80            $io->echoln("Could not create migration file: {$migration}\nAborting.");
81
82            return 1;
85        if ($ext === 'php') {
85        if ($ext === 'php') {
86            fwrite($f, $this->getPhpContent($fileName, $timestamp));
87        } elseif ($ext === 'tpql') {
88            fwrite($f, $this->getTpqlContent());
89        }
90
91        fclose($f);
91        fclose($f);
92        $io->echoln("Migration created:\n{$migration}");
93
94        return 0;
95    }
Add->fileName
102    private function fileName(Args $args, Io $io): ?string
103    {
104        $fileName = (string) $args->positional(0, '');
105
106        if ($fileName === '') {
107            $fileName = $io->ask('Name of the migration script:');
108
109            if ($fileName === '') {
110                $io->echoln('No input provided. Aborting.');
111
112                return null;
116        $fileName = strtolower(str_replace([' ', '_'], '-', $fileName));
116        $fileName = strtolower(str_replace([' ', '_'], '-', $fileName));
116        $fileName = strtolower(str_replace([' ', '_'], '-', $fileName));
116        $fileName = strtolower(str_replace([' ', '_'], '-', $fileName));
117        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
118
119        if (!$ext) {
120            return $fileName . '.sql';
123        if (!in_array($ext, ['sql', 'php', 'tpql'], strict: true)) {
124            $io->echoln("Wrong file extension '{$ext}'. Use 'sql', 'php' or 'tpql' instead.\nAborting.");
125
126            return null;
129        return $fileName;
130    }
Add->getFirstMigrationDir
195    protected function getFirstMigrationDir(array $migrations): ?string
196    {
197        $first = reset($migrations);
198
199        if ($first === false) {
204        if (is_string($first)) {
205            return $first;
209        return $first[0] ?? null;
210    }
Add->getPhpContent
132    protected function getPhpContent(string $fileName, string $timestamp): string
133    {
134        $name = $this->getPhpMigrationName($fileName);
135        $namespace = 'Quma\\Migrations\\M' . str_replace('-', '_', $timestamp) . '_' . $name;
135        $namespace = 'Quma\\Migrations\\M' . str_replace('-', '_', $timestamp) . '_' . $name;
135        $namespace = 'Quma\\Migrations\\M' . str_replace('-', '_', $timestamp) . '_' . $name;
135        $namespace = 'Quma\\Migrations\\M' . str_replace('-', '_', $timestamp) . '_' . $name;
136
137        return "<?php
138
139declare(strict_types=1);
140
141namespace {$namespace};
142
143use Celema\\Quma\\Contract;
144use Celema\\Quma\\Environment;
145
146class Migration implements Contract\\Migration
147{
148    public function run(Environment \$env): void
149    {
150        throw new \\LogicException('Implement migration {$name} before running it.');
151    }
152}
153
154return Migration::class;";
155    }
Add->getPhpMigrationName
157    protected function getPhpMigrationName(string $fileName): string
158    {
159        $parts = preg_split(
160            '/[^a-zA-Z0-9]+/',
161            pathinfo($fileName, PATHINFO_FILENAME),
162            -1,
163            PREG_SPLIT_NO_EMPTY,
164        );
165
166        if ($parts === false || count($parts) === 0) {
166        if ($parts === false || count($parts) === 0) {
166        if ($parts === false || count($parts) === 0) {
167            return 'Migration';
170        $words = array_map(
171            static fn(string $part): string => ucfirst(strtolower($part)),
172            $parts,
173        );
174
175        return implode('', $words);
175        return implode('', $words);
175        return implode('', $words);
175        return implode('', $words);
176    }
Add->getTpqlContent
180        return "<?php if (\$driver === 'pgsql') : ?>
181
182<?php else : ?>
183
184<?php endif ?>
185";
186    }
{closure:/workspace/celema/quma/src/Commands/Add.php:171-171}
171            static fn(string $part): string => ucfirst(strtolower($part)),