Passed
Push — main ( 9c95cc...5952b6 )
by Thomas
01:49
created

Migrations::run()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 17
nc 3
nop 0
dl 0
loc 28
ccs 16
cts 16
cp 1
crap 4
rs 9.7
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Conia\Quma\Migrations;
6
7
use Conia\Cli\Opts;
8
use Conia\Quma\Connection;
9
use Conia\Quma\Database;
10
use PDOException;
11
use RuntimeException;
12
use Throwable;
13
14
class Migrations extends Command
15
{
16
    protected const STARTED = 'start';
17
    protected const ERROR = 'error';
18
    protected const WARNING = 'warning';
19
    protected const SUCCESS = 'success';
20
    protected string $name = 'migrations';
21
    protected string $group = 'Migrations';
22
    protected string $description = 'Apply missing database migrations';
23
24 23
    public function run(): string|int
25
    {
26 23
        $env = $this->env;
27 23
        $opts = new Opts();
28
29 23
        if (!$env->convenience || $env->checkIfMigrationsTableExists($env->db)) {
30 22
            return $this->migrate($env->db, $env->conn, $opts->has('--stacktrace'), $opts->has('--apply'));
31
        }
32 1
        $ddl = $env->getMigrationsTableDDL();
33
34 1
        if ($ddl) {
35 1
            echo "Migrations table does not exist. For '{$env->driver}' it should look like:\n\n";
36 1
            echo $ddl;
37 1
            echo "\n\nIf you want to create the table above, simply run\n\n";
38 1
            echo "    php run create-migrations-table\n\n";
39 1
            echo "If you need to change the table or column names set them via \n\n";
40 1
            echo "    \$\\Conia\\Quma\\Connection::setMigrationsTable(...)\n";
41 1
            echo "    \$\\Conia\\Quma\\Connection::setMigrationsColumnMigration(...)\n";
42 1
            echo "    \$\\Conia\\Quma\\Connection::setMigrationsColumnApplied(...)\n";
43
        } else {
44
            // An unsupported driver would have to be installed
45
            // to be able to test meaningfully
46
            // @codeCoverageIgnoreStart
47
            echo "Driver '{$env->driver}' is not supported.\n";
48
            // @codeCoverageIgnoreEnd
49
        }
50
51 1
        return 1;
52
    }
53
54 22
    protected function migrate(
55
        Database $db,
56
        Connection $conn,
57
        bool $showStacktrace,
58
        bool $apply
59
    ): int {
60 22
        $this->begin($db);
61 22
        $appliedMigrations = $this->getAppliedMigrations($db);
62 22
        $result = self::STARTED;
63 22
        $numApplied = 0;
64
65 22
        $migrations = $this->env->getMigrations();
66
67 22
        if ($migrations === false) {
0 ignored issues
show
introduced by
The condition $migrations === false is always true.
Loading history...
68 1
            return 1;
69
        }
70
71 21
        foreach ($migrations as $migration) {
72
            assert(!empty($migration) && is_string($migration));
73
74 21
            if (in_array(basename($migration), $appliedMigrations)) {
75 16
                continue;
76
            }
77
78 21
            if (!$this->supportedByDriver($migration)) {
79 21
                continue;
80
            }
81
82 21
            $script = file_get_contents($migration);
83
84 21
            if (empty(trim($script))) {
85 21
                $this->showEmptyMessage($migration);
86 21
                $result = self::WARNING;
87
88 21
                continue;
89
            }
90
91 21
            switch (pathinfo($migration, PATHINFO_EXTENSION)) {
92 21
                case 'sql':
93 9
                    $result = $this->migrateSQL($db, $migration, $script, $showStacktrace);
94
95 9
                    break;
96
97 21
                case 'tpql':
98 21
                    $result = $this->migrateTPQL($db, $conn, $migration, $showStacktrace);
99
100 21
                    break;
101
102 8
                case 'php':
103 8
                    $result = $this->migratePHP($db, $migration, $showStacktrace);
104
105 8
                    break;
106
            }
107
108 21
            if ($result === self::ERROR) {
109 12
                break;
110
            }
111
112 21
            if ($result === self::SUCCESS) {
113 6
                $numApplied++;
114
            }
115
        }
116
117 21
        return $this->finish($db, $result, $apply, $numApplied);
118
    }
119
120 22
    protected function begin(Database $db): void
121
    {
122 22
        if ($this->supportsTransactions()) {
123 16
            $db->begin();
124
        }
125
    }
126
127 21
    protected function finish(
128
        Database $db,
129
        string $result,
130
        bool $apply,
131
        int $numApplied,
132
    ): int {
133 21
        $plural = $numApplied > 1 ? 's' : '';
134
135 21
        if ($this->supportsTransactions()) {
136 15
            if ($result === self::ERROR) {
137 8
                $db->rollback();
138 8
                echo "\nDue to errors no migrations applied\n";
139
140 8
                return 1;
141
            }
142
143 7
            if ($numApplied === 0) {
144 2
                $db->rollback();
145 2
                echo "\nNo migrations applied\n";
146
147 2
                return 0;
148
            }
149
150 5
            if ($apply) {
151 3
                $db->commit();
152 3
                echo "\n{$numApplied} migration{$plural} successfully applied\n";
153
154 3
                return 0;
155
            }
156 2
            echo "\n\033[1;31mNotice\033[0m: Test run only\033[0m";
157 2
            echo "\nWould apply {$numApplied} migration{$plural}. ";
158 2
            echo "Use the switch --apply to make it happen\n";
159 2
            $db->rollback();
160
161 2
            return 0;
162
        }
163 6
        if ($result === self::ERROR) {
164 4
            echo "\n{$numApplied} migration{$plural} applied until the error occured\n";
165
166 4
            return 1;
167
        }
168
169 2
        if ($numApplied > 0) {
170 1
            echo "\n{$numApplied} migration{$plural} successfully applied\n";
171
172 1
            return 0;
173
        }
174
175 1
        echo "\nNo migrations applied\n";
176
177 1
        return 0;
178
    }
179
180 22
    protected function supportsTransactions(): bool
181
    {
182 22
        switch ($this->env->driver) {
183 22
            case 'sqlite':
184 9
                return true;
185
186 13
            case 'pgsql':
187 7
                return true;
188
189 6
            case 'mysql':
190 6
                return false;
191
        }
192
193
        // An unsupported driver would have to be installed
194
        // to be able to test meaningfully
195
        // @codeCoverageIgnoreStart
196
        throw new RuntimeException('Database driver not supported');
197
        // @codeCoverageIgnoreEnd
198
    }
199
200 22
    protected function getAppliedMigrations(Database $db): array
201
    {
202 22
        $table = $this->env->table;
203 22
        $column = $this->env->columnMigration;
204 22
        $migrations = $db->execute("SELECT {$column} FROM {$table};")->all();
205
206 22
        return array_map(fn (array $mig): string => (string)$mig['migration'], $migrations);
207
    }
208
209
    /**
210
     * Returns if the given migration is driver specific.
211
     */
212 21
    protected function supportedByDriver(string $migration): bool
213
    {
214
        // First checks if there are brackets in the filename.
215 21
        if (preg_match('/\[[a-z]{3,8}\]/', $migration)) {
216
            // We have found a driver specific migration.
217
            // Check if it matches the current driver.
218 21
            if (preg_match('/\[' . $this->env->driver . '\]/', $migration)) {
219 5
                return true;
220
            }
221
222 21
            return false;
223
        }
224
225
        // This is no driver specific migration
226 21
        return true;
227
    }
228
229 12
    protected function migrateSQL(
230
        Database $db,
231
        string $migration,
232
        string $script,
233
        bool $showStacktrace
234
    ): string {
235
        try {
236 12
            $db->execute($script)->run();
237 6
            $this->logMigration($db, $migration);
238 6
            $this->showMessage($migration);
239
240 6
            return self::SUCCESS;
241 6
        } catch (PDOException $e) {
242 6
            $this->showMessage($migration, $e, $showStacktrace);
243
244 6
            return self::ERROR;
245
        }
246
    }
247
248 21
    protected function migrateTPQL(
249
        Database $db,
250
        Connection $conn,
251
        string $migration,
252
        bool $showStacktrace
253
    ): string {
254
        try {
255 21
            $load = function (string $migrationPath, array $context = []): void {
256
                // Hide $migrationPath. Could be overwritten if $context['templatePath'] exists.
257 21
                $____migration_path____ = $migrationPath;
258
259 21
                extract($context);
260
261
                /** @psalm-suppress UnresolvableInclude */
262 21
                include $____migration_path____;
263 21
            };
264
265 21
            $error = null;
266 21
            $context = [
267 21
                'driver' => $db->getPdoDriver(),
268 21
                'db' => $db,
269 21
                'conn' => $conn,
270 21
            ];
271
272 21
            ob_start();
273
274
            try {
275 21
                $load($migration, $context);
276 3
            } catch (Throwable $e) {
277 3
                $error = $e;
278
            }
279
280 21
            $script = ob_get_contents();
281 21
            ob_end_clean();
282
283 21
            if ($error !== null) {
284 3
                throw $error;
285
            }
286
287 21
            if (empty(trim($script))) {
288 21
                $this->showEmptyMessage($migration);
289
290 21
                return self::WARNING;
291
            }
292
293 8
            return $this->migrateSQL($db, $migration, $script, $showStacktrace);
294 3
        } catch (Throwable $e) {
295 3
            $this->showMessage($migration, $e, $showStacktrace);
296
297 3
            return self::ERROR;
298
        }
299
    }
300
301
    /** @psalm-suppress UnresolvableInclude, MixedAssignment, MixedMethodCall */
302 8
    protected function migratePHP(
303
        Database $db,
304
        string $migration,
305
        bool $showStacktrace
306
    ): string {
307
        try {
308 8
            $migObj = require $migration;
309 5
            $migObj->run($this->env);
310 5
            $this->logMigration($db, $migration);
311 5
            $this->showMessage($migration);
312
313 5
            return self::SUCCESS;
314 3
        } catch (Throwable $e) {
315 3
            $this->showMessage($migration, $e, $showStacktrace);
316
317 3
            return self::ERROR;
318
        }
319
    }
320
321 6
    protected function logMigration(Database $db, string $migration): void
322
    {
323 6
        $name = basename($migration);
324 6
        $db->execute(
325 6
            'INSERT INTO migrations (migration) VALUES (:migration)',
326 6
            ['migration' => $name]
327 6
        )->run();
328
    }
329
330 21
    protected function showEmptyMessage(string $migration): void
331
    {
332 21
        echo "\033[33mWarning\033[0m: Migration '\033[1;33m" .
333 21
            basename($migration) .
334 21
            "'\033[0m is empty. Skipped\n";
335
    }
336
337 18
    protected function showMessage(
338
        string $migration,
339
        Throwable|null $e = null,
340
        bool $showStacktrace = false
341
    ): void {
342 18
        if ($e) {
343 12
            echo "\033[1;31mError\033[0m: while working on migration '\033[1;33m" .
344 12
                basename($migration) .
345 12
                "\033[0m'\n";
346 12
            echo $e->getMessage() . "\n";
347
348 12
            if ($showStacktrace) {
349 6
                echo $e->getTraceAsString() . "\n";
350
            }
351
352 12
            return;
353
        }
354
355 6
        echo "\033[1;32mSuccess\033[0m: Migration '\033[1;33m" .
356 6
            basename($migration) .
357 6
            "\033[0m' successfully applied\n";
358
    }
359
}
360