Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
77 / 77
92.31% covered (success)
92.31%
36 / 39
66.67% covered (warning)
66.67%
18 / 27
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Executor
100.00% covered (success)
100.00%
77 / 77
92.31% covered (success)
92.31%
36 / 39
66.67% covered (warning)
66.67%
18 / 27
100.00% covered (success)
100.00%
8 / 8
34.81
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%
25 / 25
100.00% covered (success)
100.00%
7 / 7
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
3.33
 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%
4 / 4
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%
14 / 14
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 The migration path is only known at run time. */
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            return $this->migrateCompiledSQL($namespace, $migration, $script);
127        } catch (Throwable $e) {
128            $this->showMessage($migration, $e, $showStacktrace);
129
130            return self::ERROR;
131        }
132    }
133
134    private function migratePHP(
135        string $namespace,
136        string $migration,
137        bool $showStacktrace,
138    ): string {
139        try {
140            $migrationObject = $this->phpLoader->load($migration);
141            $migrationObject->run($this->env);
142            $this->log->record($this->env->db, $namespace, $migration);
143            $this->showMessage($migration);
144
145            return self::SUCCESS;
146        } catch (Throwable $e) {
147            $this->showMessage($migration, $e, $showStacktrace);
148
149            return self::ERROR;
150        }
151    }
152
153    private function showEmptyMessage(string $migration): void
154    {
155        $this->io->warn(
156            "Warning: Migration '<bright-yellow>%s</bright-yellow>' is empty. Skipped",
157            basename($migration),
158        );
159    }
160
161    private function showMessage(
162        string $migration,
163        ?Throwable $e = null,
164        bool $showStacktrace = false,
165    ): void {
166        $io = $this->io;
167
168        if ($e) {
169            $io->error(
170                "Error: while working on migration '<bright-yellow>%s</bright-yellow>'",
171                basename($migration),
172            );
173            $io->stderr()->line('%s', $e->getMessage());
174
175            if ($showStacktrace) {
176                $io->stderr()->line('%s', $e->getTraceAsString());
177            }
178
179            return;
180        }
181
182        $io->line(
183            "<bright-green>Success</bright-green>: Migration '<bright-yellow>%s</bright-yellow>' successfully applied",
184            basename($migration),
185        );
186    }
187}