Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
104 / 104 |
|
98.68% |
75 / 76 |
|
3.46% |
33 / 954 |
|
93.33% |
14 / 15 |
CRAP | |
0.00% |
0 / 1 |
| Migrations | |
100.00% |
104 / 104 |
|
98.68% |
75 / 76 |
|
3.46% |
33 / 954 |
|
100.00% |
15 / 15 |
1706.68 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
4 / 4 |
|
100.00% |
4 / 4 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| run | |
100.00% |
39 / 39 |
|
100.00% |
33 / 33 |
|
1.19% |
11 / 926 |
|
100.00% |
1 / 1 |
295.82 | |||
| migrate | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| confirmTestRunForPending | |
100.00% |
9 / 9 |
|
100.00% |
6 / 6 |
|
50.00% |
2 / 4 |
|
100.00% |
1 / 1 |
4.12 | |||
| migrationsForNamespace | |
100.00% |
19 / 19 |
|
93.33% |
14 / 15 |
|
85.71% |
6 / 7 |
|
100.00% |
1 / 1 |
7.14 | |||
| migrationIdsAreUnique | |
100.00% |
6 / 6 |
|
100.00% |
7 / 7 |
|
40.00% |
2 / 5 |
|
100.00% |
1 / 1 |
4.94 | |||
| createMigrationsTable | |
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| supportsTransactions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| driverPolicy | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| planner | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| phpLoader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| log | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| plan | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| executor | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| runner | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celemas\Quma\Commands; |
| 6 | |
| 7 | use Celemas\Cli\Command; |
| 8 | use Celemas\Cli\Opts; |
| 9 | use Celemas\Quma\Connection; |
| 10 | use Celemas\Quma\Contract; |
| 11 | use Celemas\Quma\Environment; |
| 12 | use Celemas\Quma\Migrations\DriverPolicy; |
| 13 | use Celemas\Quma\Migrations\Executor; |
| 14 | use Celemas\Quma\Migrations\Log; |
| 15 | use Celemas\Quma\Migrations\MetadataTable; |
| 16 | use Celemas\Quma\Migrations\PhpLoader; |
| 17 | use Celemas\Quma\Migrations\Plan; |
| 18 | use Celemas\Quma\Migrations\Planner; |
| 19 | use Celemas\Quma\Migrations\Runner; |
| 20 | use Celemas\Quma\Migrations\RunOptions; |
| 21 | use Celemas\Quma\Migrations\TestRunConfirmation; |
| 22 | use Override; |
| 23 | |
| 24 | final class Migrations extends Command |
| 25 | { |
| 26 | protected readonly Environment $env; |
| 27 | protected readonly ?Contract\MigrationFactory $migrationFactory; |
| 28 | protected string $name = 'migrations'; |
| 29 | protected string $group = 'Database'; |
| 30 | protected string $prefix = 'db'; |
| 31 | protected string $description = 'Apply missing database migrations'; |
| 32 | |
| 33 | /** @param array<non-empty-string, Connection>|Connection $conn */ |
| 34 | public function __construct( |
| 35 | array|Connection $conn, |
| 36 | array $options = [], |
| 37 | ?Contract\MigrationFactory $migrationFactory = null, |
| 38 | ) { |
| 39 | if (is_array($conn)) { |
| 40 | $this->env = new Environment($conn, $options); |
| 41 | } else { |
| 42 | $this->env = new Environment(['default' => $conn], $options); |
| 43 | } |
| 44 | |
| 45 | $this->migrationFactory = $migrationFactory; |
| 46 | } |
| 47 | |
| 48 | #[Override] |
| 49 | public function run(): string|int |
| 50 | { |
| 51 | $env = $this->env; |
| 52 | $opts = new Opts(); |
| 53 | $namespace = $opts->get('--namespace', ''); |
| 54 | $showStacktrace = $opts->has('--stacktrace'); |
| 55 | $apply = $opts->has('--apply'); |
| 56 | $testRun = $opts->has('--test-run'); |
| 57 | $yes = $opts->has('--yes'); |
| 58 | $driverSupported = $this->driverPolicy()->isKnown(); |
| 59 | |
| 60 | if ($apply && $testRun) { |
| 61 | echo "\033[1;31mError\033[0m: Options --apply and --test-run cannot be used together.\n"; |
| 62 | |
| 63 | return 1; |
| 64 | } |
| 65 | |
| 66 | if ($testRun && (!$driverSupported || !$this->supportsTransactions())) { |
| 67 | echo |
| 68 | "\033[1;31mError\033[0m: Test runs are only supported for transactional drivers: sqlite and pgsql.\n" |
| 69 | ; |
| 70 | echo |
| 71 | "MySQL migrations are plan-only without --apply because DDL statements can cause implicit commits.\n" |
| 72 | ; |
| 73 | |
| 74 | return 1; |
| 75 | } |
| 76 | |
| 77 | $migrations = $this->migrationsForNamespace($namespace); |
| 78 | |
| 79 | if ($migrations === false) { |
| 80 | return 1; |
| 81 | } |
| 82 | |
| 83 | $migrationNamespace = $namespace !== '' ? $namespace : 'default'; |
| 84 | $tableExists = $driverSupported && $env->checkIfMigrationsTableExists($env->db); |
| 85 | |
| 86 | if (!$apply && !$testRun) { |
| 87 | return $this->plan()->show($migrationNamespace, $migrations, $tableExists); |
| 88 | } |
| 89 | |
| 90 | if ( |
| 91 | !$apply && !$this->confirmTestRunForPending($migrationNamespace, $migrations, $tableExists, $yes) |
| 92 | ) { |
| 93 | return 1; |
| 94 | } |
| 95 | |
| 96 | if ($driverSupported && !$tableExists && !$this->supportsTransactions()) { |
| 97 | $result = $this->createMigrationsTable(); |
| 98 | |
| 99 | if ($result !== 0) { |
| 100 | // Requires simulating failing metadata table creation. |
| 101 | return $result; // @codeCoverageIgnore |
| 102 | } |
| 103 | |
| 104 | $tableExists = true; |
| 105 | } |
| 106 | |
| 107 | return $this->migrate( |
| 108 | $migrationNamespace, |
| 109 | $migrations, |
| 110 | $showStacktrace, |
| 111 | $apply, |
| 112 | $tableExists, |
| 113 | ); |
| 114 | } |
| 115 | |
| 116 | /** @param list<string> $migrations */ |
| 117 | protected function migrate( |
| 118 | string $namespace, |
| 119 | array $migrations, |
| 120 | bool $showStacktrace, |
| 121 | bool $apply, |
| 122 | bool $tableExists, |
| 123 | ): int { |
| 124 | return $this->runner()->run( |
| 125 | $namespace, |
| 126 | $migrations, |
| 127 | new RunOptions( |
| 128 | $showStacktrace, |
| 129 | $apply, |
| 130 | $tableExists, |
| 131 | $this->createMigrationsTable(...), |
| 132 | ), |
| 133 | ); |
| 134 | } |
| 135 | |
| 136 | /** |
| 137 | * @param list<string> $migrations |
| 138 | */ |
| 139 | protected function confirmTestRunForPending( |
| 140 | string $namespace, |
| 141 | array $migrations, |
| 142 | bool $tableExists, |
| 143 | bool $yes, |
| 144 | ): bool { |
| 145 | $appliedMigrations = $tableExists ? $this->log()->applied($this->env->db) : []; |
| 146 | $pendingMigrations = $this->planner()->pendingMigrations( |
| 147 | $namespace, |
| 148 | $migrations, |
| 149 | $appliedMigrations, |
| 150 | ); |
| 151 | |
| 152 | if (count($pendingMigrations) === 0) { |
| 153 | return true; |
| 154 | } |
| 155 | |
| 156 | return new TestRunConfirmation()->confirm($yes); |
| 157 | } |
| 158 | |
| 159 | /** |
| 160 | * @return list<string>|false |
| 161 | */ |
| 162 | protected function migrationsForNamespace(string $namespace): array|false |
| 163 | { |
| 164 | $migrationNamespaces = $this->env->getMigrations(); |
| 165 | |
| 166 | if ($migrationNamespaces === false) { |
| 167 | return false; |
| 168 | } |
| 169 | |
| 170 | if ($namespace) { |
| 171 | if (!array_key_exists($namespace, $migrationNamespaces)) { |
| 172 | $this->error("Migration namespace '{$namespace}' does not exist"); |
| 173 | |
| 174 | return false; |
| 175 | } |
| 176 | |
| 177 | $migrations = $migrationNamespaces[$namespace]; |
| 178 | |
| 179 | return $this->migrationIdsAreUnique($namespace, $migrations) ? $migrations : false; |
| 180 | } |
| 181 | |
| 182 | if (!array_key_exists('default', $migrationNamespaces)) { |
| 183 | $this->error("Migration namespace 'default' does not exist"); |
| 184 | $this->info( |
| 185 | 'If you have defined namespaced migrations, you must either provide a namespace using the ' |
| 186 | . "`--namespace` flag when running this command, or define a namespace named 'default' which " |
| 187 | . 'will be used when no namespace is provided.', |
| 188 | ); |
| 189 | |
| 190 | return false; |
| 191 | } |
| 192 | |
| 193 | $migrations = $migrationNamespaces['default']; |
| 194 | |
| 195 | return $this->migrationIdsAreUnique('default', $migrations) ? $migrations : false; |
| 196 | } |
| 197 | |
| 198 | /** @param list<string> $migrations */ |
| 199 | protected function migrationIdsAreUnique(string $namespace, array $migrations): bool |
| 200 | { |
| 201 | $duplicates = $this->planner()->duplicateMigrationIds($namespace, $migrations); |
| 202 | |
| 203 | foreach ($duplicates as $id => $paths) { |
| 204 | $this->error("Duplicate migration id '{$id}' in namespace '{$namespace}'"); |
| 205 | |
| 206 | foreach ($paths as $path) { |
| 207 | echo " - {$path}\n"; |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | return count($duplicates) === 0; |
| 212 | } |
| 213 | |
| 214 | protected function createMigrationsTable(): int |
| 215 | { |
| 216 | $result = new MetadataTable($this->env)->create($this->env->db); |
| 217 | |
| 218 | if ($result === 0) { |
| 219 | return 0; |
| 220 | } |
| 221 | |
| 222 | // Would require simulating failing metadata table creation. |
| 223 | // @codeCoverageIgnoreStart |
| 224 | $this->error('Migration table could not be created.'); |
| 225 | |
| 226 | return $result; |
| 227 | |
| 228 | // @codeCoverageIgnoreEnd |
| 229 | } |
| 230 | |
| 231 | protected function supportsTransactions(): bool |
| 232 | { |
| 233 | return $this->driverPolicy()->supportsTransactions(); |
| 234 | } |
| 235 | |
| 236 | private function driverPolicy(): DriverPolicy |
| 237 | { |
| 238 | return new DriverPolicy($this->env->driver); |
| 239 | } |
| 240 | |
| 241 | private function planner(): Planner |
| 242 | { |
| 243 | return new Planner($this->driverPolicy()); |
| 244 | } |
| 245 | |
| 246 | private function phpLoader(): PhpLoader |
| 247 | { |
| 248 | return new PhpLoader($this->env, $this->migrationFactory); |
| 249 | } |
| 250 | |
| 251 | private function log(): Log |
| 252 | { |
| 253 | return new Log($this->env, $this->planner()); |
| 254 | } |
| 255 | |
| 256 | private function plan(): Plan |
| 257 | { |
| 258 | return new Plan($this->env, $this->planner(), $this->log()); |
| 259 | } |
| 260 | |
| 261 | private function executor(): Executor |
| 262 | { |
| 263 | return new Executor($this->env, $this->log(), $this->phpLoader()); |
| 264 | } |
| 265 | |
| 266 | private function runner(): Runner |
| 267 | { |
| 268 | return new Runner( |
| 269 | $this->env, |
| 270 | $this->driverPolicy(), |
| 271 | $this->planner(), |
| 272 | $this->log(), |
| 273 | $this->executor(), |
| 274 | ); |
| 275 | } |
| 276 | } |
Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not
necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once.
Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement
always has an else as part of its logical flow even if you didn't write one.
| 35 | array|Connection $conn, |
| 36 | array $options = [], |
| 37 | ?Contract\MigrationFactory $migrationFactory = null, |
| 38 | ) { |
| 39 | if (is_array($conn)) { |
| 39 | if (is_array($conn)) { |
| 40 | $this->env = new Environment($conn, $options); |
| 42 | $this->env = new Environment(['default' => $conn], $options); |
| 43 | } |
| 44 | |
| 45 | $this->migrationFactory = $migrationFactory; |
| 45 | $this->migrationFactory = $migrationFactory; |
| 46 | } |
| 140 | string $namespace, |
| 141 | array $migrations, |
| 142 | bool $tableExists, |
| 143 | bool $yes, |
| 144 | ): bool { |
| 145 | $appliedMigrations = $tableExists ? $this->log()->applied($this->env->db) : []; |
| 145 | $appliedMigrations = $tableExists ? $this->log()->applied($this->env->db) : []; |
| 145 | $appliedMigrations = $tableExists ? $this->log()->applied($this->env->db) : []; |
| 145 | $appliedMigrations = $tableExists ? $this->log()->applied($this->env->db) : []; |
| 146 | $pendingMigrations = $this->planner()->pendingMigrations( |
| 147 | $namespace, |
| 148 | $migrations, |
| 149 | $appliedMigrations, |
| 150 | ); |
| 151 | |
| 152 | if (count($pendingMigrations) === 0) { |
| 153 | return true; |
| 156 | return new TestRunConfirmation()->confirm($yes); |
| 157 | } |
| 216 | $result = new MetadataTable($this->env)->create($this->env->db); |
| 217 | |
| 218 | if ($result === 0) { |
| 219 | return 0; |
| 238 | return new DriverPolicy($this->env->driver); |
| 239 | } |
| 263 | return new Executor($this->env, $this->log(), $this->phpLoader()); |
| 264 | } |
| 253 | return new Log($this->env, $this->planner()); |
| 254 | } |
| 118 | string $namespace, |
| 119 | array $migrations, |
| 120 | bool $showStacktrace, |
| 121 | bool $apply, |
| 122 | bool $tableExists, |
| 123 | ): int { |
| 124 | return $this->runner()->run( |
| 125 | $namespace, |
| 126 | $migrations, |
| 127 | new RunOptions( |
| 128 | $showStacktrace, |
| 129 | $apply, |
| 130 | $tableExists, |
| 131 | $this->createMigrationsTable(...), |
| 132 | ), |
| 133 | ); |
| 134 | } |
| 199 | protected function migrationIdsAreUnique(string $namespace, array $migrations): bool |
| 200 | { |
| 201 | $duplicates = $this->planner()->duplicateMigrationIds($namespace, $migrations); |
| 202 | |
| 203 | foreach ($duplicates as $id => $paths) { |
| 203 | foreach ($duplicates as $id => $paths) { |
| 203 | foreach ($duplicates as $id => $paths) { |
| 204 | $this->error("Duplicate migration id '{$id}' in namespace '{$namespace}'"); |
| 205 | |
| 206 | foreach ($paths as $path) { |
| 206 | foreach ($paths as $path) { |
| 206 | foreach ($paths as $path) { |
| 207 | echo " - {$path}\n"; |
| 203 | foreach ($duplicates as $id => $paths) { |
| 204 | $this->error("Duplicate migration id '{$id}' in namespace '{$namespace}'"); |
| 205 | |
| 206 | foreach ($paths as $path) { |
| 203 | foreach ($duplicates as $id => $paths) { |
| 204 | $this->error("Duplicate migration id '{$id}' in namespace '{$namespace}'"); |
| 205 | |
| 206 | foreach ($paths as $path) { |
| 207 | echo " - {$path}\n"; |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | return count($duplicates) === 0; |
| 212 | } |
| 162 | protected function migrationsForNamespace(string $namespace): array|false |
| 163 | { |
| 164 | $migrationNamespaces = $this->env->getMigrations(); |
| 165 | |
| 166 | if ($migrationNamespaces === false) { |
| 167 | return false; |
| 170 | if ($namespace) { |
| 171 | if (!array_key_exists($namespace, $migrationNamespaces)) { |
| 172 | $this->error("Migration namespace '{$namespace}' does not exist"); |
| 173 | |
| 174 | return false; |
| 177 | $migrations = $migrationNamespaces[$namespace]; |
| 178 | |
| 179 | return $this->migrationIdsAreUnique($namespace, $migrations) ? $migrations : false; |
| 179 | return $this->migrationIdsAreUnique($namespace, $migrations) ? $migrations : false; |
| 179 | return $this->migrationIdsAreUnique($namespace, $migrations) ? $migrations : false; |
| 179 | return $this->migrationIdsAreUnique($namespace, $migrations) ? $migrations : false; |
| 182 | if (!array_key_exists('default', $migrationNamespaces)) { |
| 183 | $this->error("Migration namespace 'default' does not exist"); |
| 184 | $this->info( |
| 185 | 'If you have defined namespaced migrations, you must either provide a namespace using the ' |
| 186 | . "`--namespace` flag when running this command, or define a namespace named 'default' which " |
| 187 | . 'will be used when no namespace is provided.', |
| 188 | ); |
| 189 | |
| 190 | return false; |
| 193 | $migrations = $migrationNamespaces['default']; |
| 194 | |
| 195 | return $this->migrationIdsAreUnique('default', $migrations) ? $migrations : false; |
| 195 | return $this->migrationIdsAreUnique('default', $migrations) ? $migrations : false; |
| 195 | return $this->migrationIdsAreUnique('default', $migrations) ? $migrations : false; |
| 195 | return $this->migrationIdsAreUnique('default', $migrations) ? $migrations : false; |
| 196 | } |
| 248 | return new PhpLoader($this->env, $this->migrationFactory); |
| 249 | } |
| 258 | return new Plan($this->env, $this->planner(), $this->log()); |
| 259 | } |
| 243 | return new Planner($this->driverPolicy()); |
| 244 | } |
| 51 | $env = $this->env; |
| 52 | $opts = new Opts(); |
| 53 | $namespace = $opts->get('--namespace', ''); |
| 54 | $showStacktrace = $opts->has('--stacktrace'); |
| 55 | $apply = $opts->has('--apply'); |
| 56 | $testRun = $opts->has('--test-run'); |
| 57 | $yes = $opts->has('--yes'); |
| 58 | $driverSupported = $this->driverPolicy()->isKnown(); |
| 59 | |
| 60 | if ($apply && $testRun) { |
| 60 | if ($apply && $testRun) { |
| 60 | if ($apply && $testRun) { |
| 61 | echo "\033[1;31mError\033[0m: Options --apply and --test-run cannot be used together.\n"; |
| 62 | |
| 63 | return 1; |
| 66 | if ($testRun && (!$driverSupported || !$this->supportsTransactions())) { |
| 66 | if ($testRun && (!$driverSupported || !$this->supportsTransactions())) { |
| 66 | if ($testRun && (!$driverSupported || !$this->supportsTransactions())) { |
| 66 | if ($testRun && (!$driverSupported || !$this->supportsTransactions())) { |
| 66 | if ($testRun && (!$driverSupported || !$this->supportsTransactions())) { |
| 68 | "\033[1;31mError\033[0m: Test runs are only supported for transactional drivers: sqlite and pgsql.\n" |
| 69 | ; |
| 70 | echo |
| 71 | "MySQL migrations are plan-only without --apply because DDL statements can cause implicit commits.\n" |
| 72 | ; |
| 73 | |
| 74 | return 1; |
| 77 | $migrations = $this->migrationsForNamespace($namespace); |
| 78 | |
| 79 | if ($migrations === false) { |
| 80 | return 1; |
| 83 | $migrationNamespace = $namespace !== '' ? $namespace : 'default'; |
| 83 | $migrationNamespace = $namespace !== '' ? $namespace : 'default'; |
| 83 | $migrationNamespace = $namespace !== '' ? $namespace : 'default'; |
| 83 | $migrationNamespace = $namespace !== '' ? $namespace : 'default'; |
| 84 | $tableExists = $driverSupported && $env->checkIfMigrationsTableExists($env->db); |
| 84 | $tableExists = $driverSupported && $env->checkIfMigrationsTableExists($env->db); |
| 84 | $tableExists = $driverSupported && $env->checkIfMigrationsTableExists($env->db); |
| 85 | |
| 86 | if (!$apply && !$testRun) { |
| 86 | if (!$apply && !$testRun) { |
| 86 | if (!$apply && !$testRun) { |
| 87 | return $this->plan()->show($migrationNamespace, $migrations, $tableExists); |
| 91 | !$apply && !$this->confirmTestRunForPending($migrationNamespace, $migrations, $tableExists, $yes) |
| 91 | !$apply && !$this->confirmTestRunForPending($migrationNamespace, $migrations, $tableExists, $yes) |
| 91 | !$apply && !$this->confirmTestRunForPending($migrationNamespace, $migrations, $tableExists, $yes) |
| 93 | return 1; |
| 96 | if ($driverSupported && !$tableExists && !$this->supportsTransactions()) { |
| 96 | if ($driverSupported && !$tableExists && !$this->supportsTransactions()) { |
| 96 | if ($driverSupported && !$tableExists && !$this->supportsTransactions()) { |
| 96 | if ($driverSupported && !$tableExists && !$this->supportsTransactions()) { |
| 96 | if ($driverSupported && !$tableExists && !$this->supportsTransactions()) { |
| 97 | $result = $this->createMigrationsTable(); |
| 98 | |
| 99 | if ($result !== 0) { |
| 104 | $tableExists = true; |
| 105 | } |
| 106 | |
| 107 | return $this->migrate( |
| 107 | return $this->migrate( |
| 108 | $migrationNamespace, |
| 109 | $migrations, |
| 110 | $showStacktrace, |
| 111 | $apply, |
| 112 | $tableExists, |
| 113 | ); |
| 114 | } |
| 268 | return new Runner( |
| 269 | $this->env, |
| 270 | $this->driverPolicy(), |
| 271 | $this->planner(), |
| 272 | $this->log(), |
| 273 | $this->executor(), |
| 274 | ); |
| 275 | } |
| 233 | return $this->driverPolicy()->supportsTransactions(); |
| 234 | } |
| 216 | $result = new MetadataTable($this->env)->create($this->env->db); |
| 217 | |
| 218 | if ($result === 0) { |
| 219 | return 0; |