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