Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
100 / 100 |
|
100.00% |
74 / 74 |
|
82.14% |
23 / 28 |
|
100.00% |
15 / 15 |
CRAP | |
100.00% |
1 / 1 |
| Migrations | |
100.00% |
100 / 100 |
|
100.00% |
74 / 74 |
|
82.14% |
23 / 28 |
|
100.00% |
15 / 15 |
52.04 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| __invoke | |
100.00% |
34 / 34 |
|
100.00% |
33 / 33 |
n/a |
0 / 0 |
|
100.00% |
1 / 1 |
17 | ||||
| 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% |
20 / 20 |
|
100.00% |
15 / 15 |
|
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
7 | |||
| migrationIdsAreUnique | |
100.00% |
6 / 6 |
|
100.00% |
7 / 7 |
|
40.00% |
2 / 5 |
|
100.00% |
1 / 1 |
4.94 | |||
| createMigrationsTable | |
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
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% |
8 / 8 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celema\Quma\Commands; |
| 6 | |
| 7 | use Celema\Console\Command; |
| 8 | use Celema\Console\Exception\InvalidUsage; |
| 9 | use Celema\Console\Io; |
| 10 | use Celema\Console\Opt; |
| 11 | use Celema\Quma\Connection; |
| 12 | use Celema\Quma\Contract; |
| 13 | use Celema\Quma\Environment; |
| 14 | use Celema\Quma\Migrations\DriverPolicy; |
| 15 | use Celema\Quma\Migrations\Executor; |
| 16 | use Celema\Quma\Migrations\Log; |
| 17 | use Celema\Quma\Migrations\MetadataTable; |
| 18 | use Celema\Quma\Migrations\PhpLoader; |
| 19 | use Celema\Quma\Migrations\Plan; |
| 20 | use Celema\Quma\Migrations\Planner; |
| 21 | use Celema\Quma\Migrations\Runner; |
| 22 | use Celema\Quma\Migrations\RunOptions; |
| 23 | use Celema\Quma\Migrations\TestRunConfirmation; |
| 24 | |
| 25 | #[Command('db:migrations', 'Apply missing database migrations', group: 'Database')] |
| 26 | final class Migrations |
| 27 | { |
| 28 | /** @psalm-suppress PropertyNotSetInConstructor Assigned first thing in __invoke() */ |
| 29 | private Environment $env; |
| 30 | |
| 31 | /** @psalm-suppress PropertyNotSetInConstructor Assigned first thing in __invoke() */ |
| 32 | private Io $io; |
| 33 | |
| 34 | /** |
| 35 | * @param array<non-empty-string, Connection>|Connection $connections |
| 36 | * @param array<array-key, mixed> $options |
| 37 | */ |
| 38 | public function __construct( |
| 39 | private readonly array|Connection $connections, |
| 40 | private readonly array $options = [], |
| 41 | private readonly ?Contract\MigrationFactory $migrationFactory = null, |
| 42 | ) {} |
| 43 | |
| 44 | // The parameters are the command line's options. |
| 45 | // @mago-expect lint:excessive-parameter-list |
| 46 | public function __invoke( |
| 47 | Io $io, |
| 48 | #[Opt('Apply the pending migrations instead of only planning them')] |
| 49 | bool $apply = false, |
| 50 | #[Opt('Run pending migrations inside a transaction and roll back (sqlite/pgsql only)')] |
| 51 | bool $testRun = false, |
| 52 | #[Opt('Migration namespace to run', value: 'name')] |
| 53 | string $namespace = '', |
| 54 | #[Opt('Connection to use', value: 'name')] |
| 55 | string $conn = 'default', |
| 56 | #[Opt('Show stack traces for failing migrations')] |
| 57 | bool $stacktrace = false, |
| 58 | #[Opt('Skip the test-run confirmation prompt')] |
| 59 | bool $yes = false, |
| 60 | ): int { |
| 61 | if ($apply && $testRun) { |
| 62 | throw new InvalidUsage('Options --apply and --test-run cannot be used together'); |
| 63 | } |
| 64 | |
| 65 | $this->io = $io; |
| 66 | $this->env = new Environment($this->connections, $this->options, $conn, $stacktrace); |
| 67 | $env = $this->env; |
| 68 | $driverSupported = $this->driverPolicy()->isKnown(); |
| 69 | |
| 70 | if ($testRun && (!$driverSupported || !$this->supportsTransactions())) { |
| 71 | $io->error('Error: Test runs are only supported for transactional drivers: sqlite and pgsql.'); |
| 72 | $io->stderr()->line( |
| 73 | 'MySQL migrations are plan-only without --apply because DDL statements can cause implicit commits.', |
| 74 | ); |
| 75 | |
| 76 | return 1; |
| 77 | } |
| 78 | |
| 79 | $migrations = $this->migrationsForNamespace($namespace); |
| 80 | |
| 81 | if ($migrations === false) { |
| 82 | return 1; |
| 83 | } |
| 84 | |
| 85 | $migrationNamespace = $namespace !== '' ? $namespace : 'default'; |
| 86 | $tableExists = $driverSupported && $env->checkIfMigrationsTableExists($env->db); |
| 87 | |
| 88 | if (!$apply && !$testRun) { |
| 89 | return $this->plan()->show($migrationNamespace, $migrations, $tableExists); |
| 90 | } |
| 91 | |
| 92 | if ( |
| 93 | !$apply |
| 94 | && !$this->confirmTestRunForPending($migrationNamespace, $migrations, $tableExists, $yes) |
| 95 | ) { |
| 96 | return 1; |
| 97 | } |
| 98 | |
| 99 | if ($driverSupported && !$tableExists && !$this->supportsTransactions()) { |
| 100 | $result = $this->createMigrationsTable(); |
| 101 | |
| 102 | if ($result !== 0) { |
| 103 | return $result; |
| 104 | } |
| 105 | |
| 106 | $tableExists = true; |
| 107 | } |
| 108 | |
| 109 | return $this->migrate( |
| 110 | $migrationNamespace, |
| 111 | $migrations, |
| 112 | $stacktrace, |
| 113 | $apply, |
| 114 | $tableExists, |
| 115 | ); |
| 116 | } |
| 117 | |
| 118 | /** @param list<string> $migrations */ |
| 119 | private function migrate( |
| 120 | string $namespace, |
| 121 | array $migrations, |
| 122 | bool $showStacktrace, |
| 123 | bool $apply, |
| 124 | bool $tableExists, |
| 125 | ): int { |
| 126 | return $this->runner()->run( |
| 127 | $namespace, |
| 128 | $migrations, |
| 129 | new RunOptions( |
| 130 | $showStacktrace, |
| 131 | $apply, |
| 132 | $tableExists, |
| 133 | $this->createMigrationsTable(...), |
| 134 | ), |
| 135 | ); |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * @param list<string> $migrations |
| 140 | */ |
| 141 | private function confirmTestRunForPending( |
| 142 | string $namespace, |
| 143 | array $migrations, |
| 144 | bool $tableExists, |
| 145 | bool $yes, |
| 146 | ): bool { |
| 147 | $appliedMigrations = $tableExists ? $this->log()->applied($this->env->db) : []; |
| 148 | $pendingMigrations = $this->planner()->pendingMigrations( |
| 149 | $namespace, |
| 150 | $migrations, |
| 151 | $appliedMigrations, |
| 152 | ); |
| 153 | |
| 154 | if (count($pendingMigrations) === 0) { |
| 155 | return true; |
| 156 | } |
| 157 | |
| 158 | return new TestRunConfirmation()->confirm($this->io, $yes); |
| 159 | } |
| 160 | |
| 161 | /** |
| 162 | * @return list<string>|false |
| 163 | */ |
| 164 | private function migrationsForNamespace(string $namespace): array|false |
| 165 | { |
| 166 | $migrationNamespaces = $this->env->getMigrations(); |
| 167 | |
| 168 | if ($migrationNamespaces === false) { |
| 169 | $this->io->error('No migration directories defined in configuration'); |
| 170 | |
| 171 | return false; |
| 172 | } |
| 173 | |
| 174 | if ($namespace) { |
| 175 | if (!array_key_exists($namespace, $migrationNamespaces)) { |
| 176 | $this->io->error("Migration namespace '%s' does not exist", $namespace); |
| 177 | |
| 178 | return false; |
| 179 | } |
| 180 | |
| 181 | $migrations = $migrationNamespaces[$namespace]; |
| 182 | |
| 183 | return $this->migrationIdsAreUnique($namespace, $migrations) ? $migrations : false; |
| 184 | } |
| 185 | |
| 186 | if (!array_key_exists('default', $migrationNamespaces)) { |
| 187 | $this->io->error("Migration namespace 'default' does not exist"); |
| 188 | $this->io->line( |
| 189 | 'If you have defined namespaced migrations, you must either provide a namespace using the ' |
| 190 | . "`--namespace` flag when running this command, or define a namespace named 'default' which " |
| 191 | . 'will be used when no namespace is provided.', |
| 192 | ); |
| 193 | |
| 194 | return false; |
| 195 | } |
| 196 | |
| 197 | $migrations = $migrationNamespaces['default']; |
| 198 | |
| 199 | return $this->migrationIdsAreUnique('default', $migrations) ? $migrations : false; |
| 200 | } |
| 201 | |
| 202 | /** @param list<string> $migrations */ |
| 203 | private function migrationIdsAreUnique(string $namespace, array $migrations): bool |
| 204 | { |
| 205 | $duplicates = $this->planner()->duplicateMigrationIds($namespace, $migrations); |
| 206 | |
| 207 | foreach ($duplicates as $id => $paths) { |
| 208 | $this->io->error("Duplicate migration id '%s' in namespace '%s'", $id, $namespace); |
| 209 | |
| 210 | foreach ($paths as $path) { |
| 211 | $this->io->stderr()->line(' - %s', $path); |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | return count($duplicates) === 0; |
| 216 | } |
| 217 | |
| 218 | private function createMigrationsTable(): int |
| 219 | { |
| 220 | $result = new MetadataTable($this->env, $this->io)->create($this->env->db); |
| 221 | |
| 222 | if ($result === 0) { |
| 223 | return 0; |
| 224 | } |
| 225 | |
| 226 | $this->io->error('Migration table could not be created.'); |
| 227 | |
| 228 | return $result; |
| 229 | } |
| 230 | |
| 231 | private 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(), $this->io); |
| 259 | } |
| 260 | |
| 261 | private function executor(): Executor |
| 262 | { |
| 263 | return new Executor($this->env, $this->log(), $this->phpLoader(), $this->io); |
| 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 | $this->io, |
| 275 | ); |
| 276 | } |
| 277 | } |
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.
| 39 | private readonly array|Connection $connections, |
| 40 | private readonly array $options = [], |
| 41 | private readonly ?Contract\MigrationFactory $migrationFactory = null, |
| 42 | ) {} |
| 61 | if ($apply && $testRun) { |
| 61 | if ($apply && $testRun) { |
| 62 | throw new InvalidUsage('Options --apply and --test-run cannot be used together'); |
| 65 | $this->io = $io; |
| 66 | $this->env = new Environment($this->connections, $this->options, $conn, $stacktrace); |
| 67 | $env = $this->env; |
| 68 | $driverSupported = $this->driverPolicy()->isKnown(); |
| 69 | |
| 70 | if ($testRun && (!$driverSupported || !$this->supportsTransactions())) { |
| 70 | if ($testRun && (!$driverSupported || !$this->supportsTransactions())) { |
| 70 | if ($testRun && (!$driverSupported || !$this->supportsTransactions())) { |
| 70 | if ($testRun && (!$driverSupported || !$this->supportsTransactions())) { |
| 70 | if ($testRun && (!$driverSupported || !$this->supportsTransactions())) { |
| 71 | $io->error('Error: Test runs are only supported for transactional drivers: sqlite and pgsql.'); |
| 72 | $io->stderr()->line( |
| 73 | 'MySQL migrations are plan-only without --apply because DDL statements can cause implicit commits.', |
| 74 | ); |
| 75 | |
| 76 | return 1; |
| 79 | $migrations = $this->migrationsForNamespace($namespace); |
| 80 | |
| 81 | if ($migrations === false) { |
| 82 | return 1; |
| 85 | $migrationNamespace = $namespace !== '' ? $namespace : 'default'; |
| 85 | $migrationNamespace = $namespace !== '' ? $namespace : 'default'; |
| 85 | $migrationNamespace = $namespace !== '' ? $namespace : 'default'; |
| 85 | $migrationNamespace = $namespace !== '' ? $namespace : 'default'; |
| 86 | $tableExists = $driverSupported && $env->checkIfMigrationsTableExists($env->db); |
| 86 | $tableExists = $driverSupported && $env->checkIfMigrationsTableExists($env->db); |
| 86 | $tableExists = $driverSupported && $env->checkIfMigrationsTableExists($env->db); |
| 87 | |
| 88 | if (!$apply && !$testRun) { |
| 88 | if (!$apply && !$testRun) { |
| 88 | if (!$apply && !$testRun) { |
| 89 | return $this->plan()->show($migrationNamespace, $migrations, $tableExists); |
| 93 | !$apply |
| 94 | && !$this->confirmTestRunForPending($migrationNamespace, $migrations, $tableExists, $yes) |
| 94 | && !$this->confirmTestRunForPending($migrationNamespace, $migrations, $tableExists, $yes) |
| 96 | return 1; |
| 99 | if ($driverSupported && !$tableExists && !$this->supportsTransactions()) { |
| 99 | if ($driverSupported && !$tableExists && !$this->supportsTransactions()) { |
| 99 | if ($driverSupported && !$tableExists && !$this->supportsTransactions()) { |
| 99 | if ($driverSupported && !$tableExists && !$this->supportsTransactions()) { |
| 99 | if ($driverSupported && !$tableExists && !$this->supportsTransactions()) { |
| 100 | $result = $this->createMigrationsTable(); |
| 101 | |
| 102 | if ($result !== 0) { |
| 103 | return $result; |
| 106 | $tableExists = true; |
| 107 | } |
| 108 | |
| 109 | return $this->migrate( |
| 109 | return $this->migrate( |
| 110 | $migrationNamespace, |
| 111 | $migrations, |
| 112 | $stacktrace, |
| 113 | $apply, |
| 114 | $tableExists, |
| 115 | ); |
| 116 | } |
| 142 | string $namespace, |
| 143 | array $migrations, |
| 144 | bool $tableExists, |
| 145 | bool $yes, |
| 146 | ): bool { |
| 147 | $appliedMigrations = $tableExists ? $this->log()->applied($this->env->db) : []; |
| 147 | $appliedMigrations = $tableExists ? $this->log()->applied($this->env->db) : []; |
| 147 | $appliedMigrations = $tableExists ? $this->log()->applied($this->env->db) : []; |
| 147 | $appliedMigrations = $tableExists ? $this->log()->applied($this->env->db) : []; |
| 148 | $pendingMigrations = $this->planner()->pendingMigrations( |
| 149 | $namespace, |
| 150 | $migrations, |
| 151 | $appliedMigrations, |
| 152 | ); |
| 153 | |
| 154 | if (count($pendingMigrations) === 0) { |
| 155 | return true; |
| 158 | return new TestRunConfirmation()->confirm($this->io, $yes); |
| 159 | } |
| 220 | $result = new MetadataTable($this->env, $this->io)->create($this->env->db); |
| 221 | |
| 222 | if ($result === 0) { |
| 223 | return 0; |
| 226 | $this->io->error('Migration table could not be created.'); |
| 227 | |
| 228 | return $result; |
| 229 | } |
| 238 | return new DriverPolicy($this->env->driver); |
| 239 | } |
| 263 | return new Executor($this->env, $this->log(), $this->phpLoader(), $this->io); |
| 264 | } |
| 253 | return new Log($this->env, $this->planner()); |
| 254 | } |
| 120 | string $namespace, |
| 121 | array $migrations, |
| 122 | bool $showStacktrace, |
| 123 | bool $apply, |
| 124 | bool $tableExists, |
| 125 | ): int { |
| 126 | return $this->runner()->run( |
| 127 | $namespace, |
| 128 | $migrations, |
| 129 | new RunOptions( |
| 130 | $showStacktrace, |
| 131 | $apply, |
| 132 | $tableExists, |
| 133 | $this->createMigrationsTable(...), |
| 134 | ), |
| 135 | ); |
| 136 | } |
| 203 | private function migrationIdsAreUnique(string $namespace, array $migrations): bool |
| 204 | { |
| 205 | $duplicates = $this->planner()->duplicateMigrationIds($namespace, $migrations); |
| 206 | |
| 207 | foreach ($duplicates as $id => $paths) { |
| 207 | foreach ($duplicates as $id => $paths) { |
| 207 | foreach ($duplicates as $id => $paths) { |
| 208 | $this->io->error("Duplicate migration id '%s' in namespace '%s'", $id, $namespace); |
| 209 | |
| 210 | foreach ($paths as $path) { |
| 210 | foreach ($paths as $path) { |
| 210 | foreach ($paths as $path) { |
| 211 | $this->io->stderr()->line(' - %s', $path); |
| 207 | foreach ($duplicates as $id => $paths) { |
| 208 | $this->io->error("Duplicate migration id '%s' in namespace '%s'", $id, $namespace); |
| 209 | |
| 210 | foreach ($paths as $path) { |
| 207 | foreach ($duplicates as $id => $paths) { |
| 208 | $this->io->error("Duplicate migration id '%s' in namespace '%s'", $id, $namespace); |
| 209 | |
| 210 | foreach ($paths as $path) { |
| 211 | $this->io->stderr()->line(' - %s', $path); |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | return count($duplicates) === 0; |
| 216 | } |
| 164 | private function migrationsForNamespace(string $namespace): array|false |
| 165 | { |
| 166 | $migrationNamespaces = $this->env->getMigrations(); |
| 167 | |
| 168 | if ($migrationNamespaces === false) { |
| 169 | $this->io->error('No migration directories defined in configuration'); |
| 170 | |
| 171 | return false; |
| 174 | if ($namespace) { |
| 175 | if (!array_key_exists($namespace, $migrationNamespaces)) { |
| 176 | $this->io->error("Migration namespace '%s' does not exist", $namespace); |
| 177 | |
| 178 | return false; |
| 181 | $migrations = $migrationNamespaces[$namespace]; |
| 182 | |
| 183 | return $this->migrationIdsAreUnique($namespace, $migrations) ? $migrations : false; |
| 183 | return $this->migrationIdsAreUnique($namespace, $migrations) ? $migrations : false; |
| 183 | return $this->migrationIdsAreUnique($namespace, $migrations) ? $migrations : false; |
| 183 | return $this->migrationIdsAreUnique($namespace, $migrations) ? $migrations : false; |
| 186 | if (!array_key_exists('default', $migrationNamespaces)) { |
| 187 | $this->io->error("Migration namespace 'default' does not exist"); |
| 188 | $this->io->line( |
| 189 | 'If you have defined namespaced migrations, you must either provide a namespace using the ' |
| 190 | . "`--namespace` flag when running this command, or define a namespace named 'default' which " |
| 191 | . 'will be used when no namespace is provided.', |
| 192 | ); |
| 193 | |
| 194 | return false; |
| 197 | $migrations = $migrationNamespaces['default']; |
| 198 | |
| 199 | return $this->migrationIdsAreUnique('default', $migrations) ? $migrations : false; |
| 199 | return $this->migrationIdsAreUnique('default', $migrations) ? $migrations : false; |
| 199 | return $this->migrationIdsAreUnique('default', $migrations) ? $migrations : false; |
| 199 | return $this->migrationIdsAreUnique('default', $migrations) ? $migrations : false; |
| 200 | } |
| 248 | return new PhpLoader($this->env, $this->migrationFactory); |
| 249 | } |
| 258 | return new Plan($this->env, $this->planner(), $this->log(), $this->io); |
| 259 | } |
| 243 | return new Planner($this->driverPolicy()); |
| 244 | } |
| 268 | return new Runner( |
| 269 | $this->env, |
| 270 | $this->driverPolicy(), |
| 271 | $this->planner(), |
| 272 | $this->log(), |
| 273 | $this->executor(), |
| 274 | $this->io, |
| 275 | ); |
| 276 | } |
| 233 | return $this->driverPolicy()->supportsTransactions(); |
| 234 | } |
| 220 | $result = new MetadataTable($this->env, $this->io)->create($this->env->db); |
| 221 | |
| 222 | if ($result === 0) { |
| 223 | return 0; |
| 226 | $this->io->error('Migration table could not be created.'); |
| 227 | |
| 228 | return $result; |
| 229 | } |