Passed
Push — main ( a41053...92bfd1 )
by Thomas
01:48
created

Migrations::migrate()   B

Complexity

Conditions 9
Paths 7

Size

Total Lines 53
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 9

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 27
c 1
b 0
f 0
nc 7
nop 4
dl 0
loc 53
ccs 28
cts 28
cp 1
crap 9
rs 8.0555

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
            $result = match (pathinfo($migration, PATHINFO_EXTENSION)) {
92 21
                'sql' => $this->migrateSQL($db, $migration, $script, $showStacktrace),
93 21
                'tpql' => $this->migrateTPQL($db, $conn, $migration, $showStacktrace),
94 21
                'php' => $this->migratePHP($db, $migration, $showStacktrace),
95 21
            };
96
97 21
            if ($result === self::ERROR) {
98 12
                break;
99
            }
100
101 21
            if ($result === self::SUCCESS) {
102 6
                $numApplied++;
103
            }
104
        }
105
106 21
        return $this->finish($db, $result, $apply, $numApplied);
107
    }
108
109 22
    protected function begin(Database $db): void
110
    {
111 22
        if ($this->supportsTransactions()) {
112 16
            $db->begin();
113
        }
114
    }
115
116 21
    protected function finish(
117
        Database $db,
118
        string $result,
119
        bool $apply,
120
        int $numApplied,
121
    ): int {
122 21
        $plural = $numApplied > 1 ? 's' : '';
123
124 21
        if ($this->supportsTransactions()) {
125 15
            if ($result === self::ERROR) {
126 8
                $db->rollback();
127 8
                echo "\nDue to errors no migrations applied\n";
128
129 8
                return 1;
130
            }
131
132 7
            if ($numApplied === 0) {
133 2
                $db->rollback();
134 2
                echo "\nNo migrations applied\n";
135
136 2
                return 0;
137
            }
138
139 5
            if ($apply) {
140 3
                $db->commit();
141 3
                echo "\n{$numApplied} migration{$plural} successfully applied\n";
142
143 3
                return 0;
144
            }
145 2
            echo "\n\033[1;31mNotice\033[0m: Test run only\033[0m";
146 2
            echo "\nWould apply {$numApplied} migration{$plural}. ";
147 2
            echo "Use the switch --apply to make it happen\n";
148 2
            $db->rollback();
149
150 2
            return 0;
151
        }
152 6
        if ($result === self::ERROR) {
153 4
            echo "\n{$numApplied} migration{$plural} applied until the error occured\n";
154
155 4
            return 1;
156
        }
157
158 2
        if ($numApplied > 0) {
159 1
            echo "\n{$numApplied} migration{$plural} successfully applied\n";
160
161 1
            return 0;
162
        }
163
164 1
        echo "\nNo migrations applied\n";
165
166 1
        return 0;
167
    }
168
169 22
    protected function supportsTransactions(): bool
170
    {
171 22
        switch ($this->env->driver) {
172 22
            case 'sqlite':
173 9
                return true;
174
175 13
            case 'pgsql':
176 7
                return true;
177
178 6
            case 'mysql':
179 6
                return false;
180
        }
181
182
        // An unsupported driver would have to be installed
183
        // to be able to test meaningfully
184
        // @codeCoverageIgnoreStart
185
        throw new RuntimeException('Database driver not supported');
186
        // @codeCoverageIgnoreEnd
187
    }
188
189 22
    protected function getAppliedMigrations(Database $db): array
190
    {
191 22
        $table = $this->env->table;
192 22
        $column = $this->env->columnMigration;
193 22
        $migrations = $db->execute("SELECT {$column} FROM {$table};")->all();
194
195 22
        return array_map(fn (array $mig): string => (string)$mig['migration'], $migrations);
196
    }
197
198
    /**
199
     * Returns if the given migration is driver specific.
200
     */
201 21
    protected function supportedByDriver(string $migration): bool
202
    {
203
        // First checks if there are brackets in the filename.
204 21
        if (preg_match('/\[[a-z]{3,8}\]/', $migration)) {
205
            // We have found a driver specific migration.
206
            // Check if it matches the current driver.
207 21
            if (preg_match('/\[' . $this->env->driver . '\]/', $migration)) {
208 5
                return true;
209
            }
210
211 21
            return false;
212
        }
213
214
        // This is no driver specific migration
215 21
        return true;
216
    }
217
218 12
    protected function migrateSQL(
219
        Database $db,
220
        string $migration,
221
        string $script,
222
        bool $showStacktrace
223
    ): string {
224
        try {
225 12
            $db->execute($script)->run();
226 6
            $this->logMigration($db, $migration);
227 6
            $this->showMessage($migration);
228
229 6
            return self::SUCCESS;
230 6
        } catch (PDOException $e) {
231 6
            $this->showMessage($migration, $e, $showStacktrace);
232
233 6
            return self::ERROR;
234
        }
235
    }
236
237 21
    protected function migrateTPQL(
238
        Database $db,
239
        Connection $conn,
240
        string $migration,
241
        bool $showStacktrace
242
    ): string {
243
        try {
244 21
            $load = function (string $migrationPath, array $context = []): void {
245
                // Hide $migrationPath. Could be overwritten if $context['templatePath'] exists.
246 21
                $____migration_path____ = $migrationPath;
247
248 21
                extract($context);
249
250
                /** @psalm-suppress UnresolvableInclude */
251 21
                include $____migration_path____;
252 21
            };
253
254 21
            $error = null;
255 21
            $context = [
256 21
                'driver' => $db->getPdoDriver(),
257 21
                'db' => $db,
258 21
                'conn' => $conn,
259 21
            ];
260
261 21
            ob_start();
262
263
            try {
264 21
                $load($migration, $context);
265 3
            } catch (Throwable $e) {
266 3
                $error = $e;
267
            }
268
269 21
            $script = ob_get_contents();
270 21
            ob_end_clean();
271
272 21
            if ($error !== null) {
273 3
                throw $error;
274
            }
275
276 21
            if (empty(trim($script))) {
277 21
                $this->showEmptyMessage($migration);
278
279 21
                return self::WARNING;
280
            }
281
282 8
            return $this->migrateSQL($db, $migration, $script, $showStacktrace);
283 3
        } catch (Throwable $e) {
284 3
            $this->showMessage($migration, $e, $showStacktrace);
285
286 3
            return self::ERROR;
287
        }
288
    }
289
290
    /** @psalm-suppress UnresolvableInclude, MixedAssignment, MixedMethodCall */
291 8
    protected function migratePHP(
292
        Database $db,
293
        string $migration,
294
        bool $showStacktrace
295
    ): string {
296
        try {
297 8
            $migObj = require $migration;
298 5
            $migObj->run($this->env);
299 5
            $this->logMigration($db, $migration);
300 5
            $this->showMessage($migration);
301
302 5
            return self::SUCCESS;
303 3
        } catch (Throwable $e) {
304 3
            $this->showMessage($migration, $e, $showStacktrace);
305
306 3
            return self::ERROR;
307
        }
308
    }
309
310 6
    protected function logMigration(Database $db, string $migration): void
311
    {
312 6
        $name = basename($migration);
313 6
        $db->execute(
314 6
            'INSERT INTO migrations (migration) VALUES (:migration)',
315 6
            ['migration' => $name]
316 6
        )->run();
317
    }
318
319 21
    protected function showEmptyMessage(string $migration): void
320
    {
321 21
        echo "\033[33mWarning\033[0m: Migration '\033[1;33m" .
322 21
            basename($migration) .
323 21
            "'\033[0m is empty. Skipped\n";
324
    }
325
326 18
    protected function showMessage(
327
        string $migration,
328
        Throwable|null $e = null,
329
        bool $showStacktrace = false
330
    ): void {
331 18
        if ($e) {
332 12
            echo "\033[1;31mError\033[0m: while working on migration '\033[1;33m" .
333 12
                basename($migration) .
334 12
                "\033[0m'\n";
335 12
            echo $e->getMessage() . "\n";
336
337 12
            if ($showStacktrace) {
338 6
                echo $e->getTraceAsString() . "\n";
339
            }
340
341 12
            return;
342
        }
343
344 6
        echo "\033[1;32mSuccess\033[0m: Migration '\033[1;33m" .
345 6
            basename($migration) .
346 6
            "\033[0m' successfully applied\n";
347
    }
348
}
349