Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
82 / 82
90.91% covered (success)
90.91%
40 / 44
60.00% covered (warning)
60.00%
18 / 30
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Executor
100.00% covered (success)
100.00%
82 / 82
90.91% covered (success)
90.91%
40 / 44
60.00% covered (warning)
60.00%
18 / 30
100.00% covered (success)
100.00%
8 / 8
49.22
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
 migrate
100.00% covered (success)
100.00%
12 / 12
84.62% covered (warning)
84.62%
11 / 13
45.45% covered (danger)
45.45%
5 / 11
100.00% covered (success)
100.00%
1 / 1
11.84
 migrateSQL
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 migrateCompiledSQL
100.00% covered (success)
100.00%
8 / 8
83.33% covered (warning)
83.33%
5 / 6
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
2.50
 migrateTPQL
100.00% covered (success)
100.00%
28 / 28
91.67% covered (success)
91.67%
11 / 12
33.33% covered (danger)
33.33%
2 / 6
100.00% covered (success)
100.00%
1 / 1
8.74
 migratePHP
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 showEmptyMessage
100.00% covered (success)
100.00%
5 / 5
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
 showMessage
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Quma\Migrations;
6
7use Celemas\Quma\Environment;
8use RuntimeException;
9use Throwable;
10
11final readonly class Executor
12{
13    public const string STARTED = 'start';
14    public const string ERROR = 'error';
15    public const string WARNING = 'warning';
16    public const string SUCCESS = 'success';
17
18    public function __construct(
19        private Environment $env,
20        private Log $log,
21        private PhpLoader $phpLoader,
22    ) {}
23
24    public function migrate(string $namespace, string $migration, bool $showStacktrace): string
25    {
26        $script = file_get_contents($migration);
27
28        if ($script === false) {
29            $this->showMessage($migration, new RuntimeException('Could not read migration file'));
30
31            return self::ERROR;
32        }
33
34        if (trim($script) === '') {
35            $this->showEmptyMessage($migration);
36
37            return self::WARNING;
38        }
39
40        return match (pathinfo($migration, PATHINFO_EXTENSION)) {
41            'sql' => $this->migrateSQL($namespace, $migration, $script, $showStacktrace),
42            'tpql' => $this->migrateTPQL($namespace, $migration, $showStacktrace),
43            'php' => $this->migratePHP($namespace, $migration, $showStacktrace),
44        };
45    }
46
47    private function migrateSQL(
48        string $namespace,
49        string $migration,
50        string $script,
51        bool $showStacktrace,
52    ): string {
53        try {
54            $script = $this->env->conn->config->placeholders?->compileSql($script, $migration) ?? $script;
55
56            return $this->migrateCompiledSQL($namespace, $migration, $script);
57        } catch (Throwable $e) {
58            $this->showMessage($migration, $e, $showStacktrace);
59
60            return self::ERROR;
61        }
62    }
63
64    private function migrateCompiledSQL(
65        string $namespace,
66        string $migration,
67        string $script,
68    ): string {
69        if (trim($script) === '') {
70            $this->showEmptyMessage($migration);
71
72            return self::WARNING;
73        }
74
75        $db = $this->env->db;
76        $db->execute($script)->run();
77        $this->log->record($db, $namespace, $migration);
78        $this->showMessage($migration);
79
80        return self::SUCCESS;
81    }
82
83    private function migrateTPQL(
84        string $namespace,
85        string $migration,
86        bool $showStacktrace,
87    ): string {
88        try {
89            $db = $this->env->db;
90            $conn = $this->env->conn;
91            $context = [
92                'driver' => $db->getPdoDriver(),
93                'db' => $db,
94                'conn' => $conn,
95            ];
96
97            $executeTemplate = static function (
98                string $templatePath,
99                array $context,
100            ): void {
101                extract($context, EXTR_SKIP);
102
103                /** @psalm-suppress UnresolvableInclude */
104                require $templatePath;
105            };
106
107            ob_start();
108            $script = '';
109
110            try {
111                $executeTemplate($migration, $context);
112                $script = ob_get_contents();
113            } finally {
114                ob_end_clean();
115            }
116
117            if (!is_string($script)) {
118                // Defensive guard for an impossible false from ob_get_contents() after ob_start().
119                $script = ''; // @codeCoverageIgnore
120            }
121
122            $script = $conn->config->placeholders?->compileSql($script, $migration) ?? $script;
123
124            if (trim($script) === '') {
125                $this->showEmptyMessage($migration);
126
127                return self::WARNING;
128            }
129
130            return $this->migrateCompiledSQL($namespace, $migration, $script);
131        } catch (Throwable $e) {
132            $this->showMessage($migration, $e, $showStacktrace);
133
134            return self::ERROR;
135        }
136    }
137
138    private function migratePHP(
139        string $namespace,
140        string $migration,
141        bool $showStacktrace,
142    ): string {
143        try {
144            $migrationObject = $this->phpLoader->load($migration);
145            $migrationObject->run($this->env);
146            $this->log->record($this->env->db, $namespace, $migration);
147            $this->showMessage($migration);
148
149            return self::SUCCESS;
150        } catch (Throwable $e) {
151            $this->showMessage($migration, $e, $showStacktrace);
152
153            return self::ERROR;
154        }
155    }
156
157    private function showEmptyMessage(string $migration): void
158    {
159        echo
160            "\033[33mWarning\033[0m: Migration '\033[1;33m"
161                . basename($migration)
162                . "'\033[0m is empty. Skipped\n"
163        ;
164    }
165
166    private function showMessage(
167        string $migration,
168        ?Throwable $e = null,
169        bool $showStacktrace = false,
170    ): void {
171        if ($e) {
172            echo
173                "\033[1;31mError\033[0m: while working on migration '\033[1;33m"
174                    . basename($migration)
175                    . "\033[0m'\n"
176            ;
177            echo $e->getMessage() . "\n";
178
179            if ($showStacktrace) {
180                echo $e->getTraceAsString() . "\n";
181            }
182
183            return;
184        }
185
186        echo
187            "\033[1;32mSuccess\033[0m: Migration '\033[1;33m"
188                . basename($migration)
189                . "\033[0m' successfully applied\n"
190        ;
191    }
192}