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}

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->getFirstMigrationDir
170    protected function getFirstMigrationDir(array $migrations): ?string
171    {
172        $first = reset($migrations);
173
174        if ($first === false) {
179        if (is_string($first)) {
180            return $first;
184        return $first[0] ?? null;
185    }
Add->getPhpContent
107    protected function getPhpContent(string $fileName, string $timestamp): string
108    {
109        $name = $this->getPhpMigrationName($fileName);
110        $namespace = 'Quma\\Migrations\\M' . str_replace('-', '_', $timestamp) . '_' . $name;
110        $namespace = 'Quma\\Migrations\\M' . str_replace('-', '_', $timestamp) . '_' . $name;
110        $namespace = 'Quma\\Migrations\\M' . str_replace('-', '_', $timestamp) . '_' . $name;
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    }
Add->getPhpMigrationName
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) {
141        if ($parts === false || count($parts) === 0) {
141        if ($parts === false || count($parts) === 0) {
142            return 'Migration';
145        $words = array_map(
146            static fn(string $part): string => ucfirst(strtolower($part)),
147            $parts,
148        );
149
150        return implode('', $words);
150        return implode('', $words);
150        return implode('', $words);
150        return implode('', $words);
151    }
Add->getTpqlContent
155        return "<?php if (\$driver === 'pgsql') : ?>
156
157<?php else : ?>
158
159<?php endif ?>
160";
161    }
Add->run
20        $env = $this->env;
21        $opts = new Opts();
22        $fileName = $opts->get('-f', $opts->get('--file', ''));
23
24        if ($fileName === '') {
39        $fileName = str_replace(' ', '-', $fileName);
39        $fileName = str_replace(' ', '-', $fileName);
39        $fileName = str_replace(' ', '-', $fileName);
39        $fileName = str_replace(' ', '-', $fileName);
40        $fileName = str_replace('_', '-', $fileName);
40        $fileName = str_replace('_', '-', $fileName);
40        $fileName = str_replace('_', '-', $fileName);
40        $fileName = str_replace('_', '-', $fileName);
41        $fileName = strtolower($fileName);
42        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
43
44        if (!$ext) {
44        if (!$ext) {
45            $fileName .= '.sql';
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;
54        $migrations = $env->conn->config->migrations;
55
56        if (count($migrations) === 0) {
57            echo "No migration directories configured. Aborting.\n";
58
59            return 1;
64        $migrationsDir = $this->getFirstMigrationDir($migrations);
65
66        if ($migrationsDir === null) {
67            echo "No valid migration directory found. Aborting.\n";
68
69            return 1;
72        if (str_contains($migrationsDir, '/vendor')) {
72        if (str_contains($migrationsDir, '/vendor')) {
72        if (str_contains($migrationsDir, '/vendor')) {
72        if (str_contains($migrationsDir, '/vendor')) {
73            echo "The migrations directory is inside './vendor'.\n  -> {$migrationsDir}\nAborting.\n";
74
75            return 1;
78        if (!is_writable($migrationsDir)) {
79            echo "Migrations directory is not writable\n  -> {$migrationsDir}\nAborting. \n";
80
81            return 1;
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;
95        if ($ext === 'php') {
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);
101        fclose($f);
102        echo "Migration created:\n{$migration}\n";
103
104        return $migration;
105    }
{closure:/workspace/celemas/quma/src/Commands/Add.php:146-146}
146            static fn(string $part): string => ucfirst(strtolower($part)),