From 56f1aa8be71d93c545634dc30c59d37a7794e0ce Mon Sep 17 00:00:00 2001 From: Pavel Buchnev Date: Tue, 21 Nov 2023 16:33:06 +0400 Subject: [PATCH] Migration commands refactoring (#83) * Migration commands refactoring 1. Added `split` option to the `cycle:migrate` command. This lets you break up changes into several migrations, each for a different table. 2. Now, if there are migrations you haven't done yet and you use the `cycle:migrate` command, it will ask if you want to run these migrations. 3. Put in a check for the `cycle:sync` command to stop it from running by mistake. 4. Made some changes to the messages you see in the console. issue #82 --- composer.json | 2 +- src/Auth/TokenStorage.php | 2 ++ .../CycleOrm/Generator/ShowChanges.php | 23 ++++++------ .../Command/CycleOrm/MigrateCommand.php | 30 ++++++++++++---- .../Command/CycleOrm/RenderCommand.php | 1 - src/Console/Command/CycleOrm/SyncCommand.php | 15 +++++--- .../Command/CycleOrm/UpdateCommand.php | 8 ++--- .../Command/Migrate/AbstractCommand.php | 27 +++++++++----- src/Console/Command/Migrate/InitCommand.php | 4 +-- .../Command/Migrate/MigrateCommand.php | 6 ++-- src/Console/Command/Migrate/ReplayCommand.php | 5 ++- .../Command/Migrate/RollbackCommand.php | 5 ++- src/Console/Command/Migrate/StatusCommand.php | 8 ++--- .../Command/Scaffolder/EntityCommand.php | 6 ++-- .../Command/Scaffolder/MigrationCommand.php | 10 +++--- .../Command/CycleOrm/MigrateCommandTest.php | 21 ++++++----- .../Command/CycleOrm/SyncCommandTest.php | 13 ++++++- .../Command/Migrate/MigrateCommandTest.php | 35 +++++++++++++++++-- 18 files changed, 150 insertions(+), 71 deletions(-) diff --git a/composer.json b/composer.json index 07b7333..88b4a83 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "infection/infection": "^0.26.6", "mockery/mockery": "^1.5", "phpunit/phpunit": "^9.5.20", - "spiral/testing": "^2.0", + "spiral/testing": "^2.4", "spiral/validator": "^1.2", "vimeo/psalm": "^4.27" }, diff --git a/src/Auth/TokenStorage.php b/src/Auth/TokenStorage.php index 01262df..c837440 100644 --- a/src/Auth/TokenStorage.php +++ b/src/Auth/TokenStorage.php @@ -89,11 +89,13 @@ private function issueID(): string { $id = $this->randomHash(64); + /** @psalm-suppress InternalMethod */ $query = $this->orm->getSource(Token::class) ->getDatabase() ->select() ->from($this->orm->getSource(Token::class)->getTable()); + /** @psalm-suppress InternalMethod */ while ((clone $query)->where('id', $id)->count('id') !== 0) { $id = $this->randomHash(64); } diff --git a/src/Console/Command/CycleOrm/Generator/ShowChanges.php b/src/Console/Command/CycleOrm/Generator/ShowChanges.php index f65292b..7591983 100644 --- a/src/Console/Command/CycleOrm/Generator/ShowChanges.php +++ b/src/Console/Command/CycleOrm/Generator/ShowChanges.php @@ -4,10 +4,10 @@ namespace Spiral\Cycle\Console\Command\CycleOrm\Generator; +use Cycle\Database\Schema\ComparatorInterface; use Cycle\Schema\GeneratorInterface; use Cycle\Schema\Registry; use Cycle\Database\Schema\AbstractTable; -use Cycle\Database\Schema\Comparator; use Symfony\Component\Console\Output\OutputInterface; final class ShowChanges implements GeneratorInterface @@ -15,20 +15,18 @@ final class ShowChanges implements GeneratorInterface private array $changes = []; public function __construct( - private readonly OutputInterface $output + private readonly OutputInterface $output, ) { } public function run(Registry $registry): Registry { - $this->output->writeln('Detecting schema changes:'); - $this->changes = []; foreach ($registry->getIterator() as $e) { if ($registry->hasTable($e)) { $table = $registry->getTableSchema($e); if ($table->getComparator()->hasChanges()) { - $key = $registry->getDatabase($e).':'.$registry->getTable($e); + $key = $registry->getDatabase($e) . ':' . $registry->getTable($e); $this->changes[$key] = [ 'database' => $registry->getDatabase($e), 'table' => $registry->getTable($e), @@ -44,6 +42,9 @@ public function run(Registry $registry): Registry return $registry; } + + $this->output->writeln('Schema changes:'); + foreach ($this->changes as $change) { $this->output->write(\sprintf('• %s.%s', $change['database'], $change['table'])); $this->describeChanges($change['schema']); @@ -63,14 +64,14 @@ protected function describeChanges(AbstractTable $table): void $this->output->writeln( \sprintf( ': %s change(s) detected', - $this->numChanges($table) - ) + $this->numChanges($table), + ), ); return; } - $this->output->write("\n"); + $this->output->write("\n"); if (!$table->exists()) { $this->output->writeln(' - create table'); @@ -88,7 +89,7 @@ protected function describeChanges(AbstractTable $table): void $this->describeFKs($cmp); } - protected function describeColumns(Comparator $cmp): void + protected function describeColumns(ComparatorInterface $cmp): void { foreach ($cmp->addedColumns() as $column) { $this->output->writeln(" - add column {$column->getName()}"); @@ -104,7 +105,7 @@ protected function describeColumns(Comparator $cmp): void } } - protected function describeIndexes(Comparator $cmp): void + protected function describeIndexes(ComparatorInterface $cmp): void { foreach ($cmp->addedIndexes() as $index) { $index = \implode(', ', $index->getColumns()); @@ -123,7 +124,7 @@ protected function describeIndexes(Comparator $cmp): void } } - protected function describeFKs(Comparator $cmp): void + protected function describeFKs(ComparatorInterface $cmp): void { foreach ($cmp->addedForeignKeys() as $fk) { $fkColumns = \implode(', ', $fk->getColumns()); diff --git a/src/Console/Command/CycleOrm/MigrateCommand.php b/src/Console/Command/CycleOrm/MigrateCommand.php index 3ce1873..62c6c05 100644 --- a/src/Console/Command/CycleOrm/MigrateCommand.php +++ b/src/Console/Command/CycleOrm/MigrateCommand.php @@ -4,6 +4,9 @@ namespace Spiral\Cycle\Console\Command\CycleOrm; +use Cycle\Schema\Generator\Migrations\Strategy\GeneratorStrategyInterface; +use Cycle\Schema\Generator\Migrations\Strategy\MultipleFilesStrategy; +use Spiral\Core\BinderInterface; use Spiral\Cycle\Bootloader\SchemaBootloader; use Spiral\Cycle\Config\CycleConfig; use Spiral\Cycle\Console\Command\CycleOrm\Generator\ShowChanges; @@ -22,6 +25,7 @@ final class MigrateCommand extends AbstractCommand protected const NAME = 'cycle:migrate'; protected const DESCRIPTION = 'Generate ORM schema migrations'; protected const OPTIONS = [ + ['split', 'p', InputOption::VALUE_NONE, 'Split generated migration into multiple files.'], ['run', 'r', InputOption::VALUE_NONE, 'Automatically run generated migration.'], ]; @@ -30,30 +34,44 @@ public function perform( CycleConfig $config, Registry $registry, MemoryInterface $memory, - GenerateMigrations $migrations, Migrator $migrator, - Console $console + Console $console, ): int { $migrator->configure(); foreach ($migrator->getMigrations() as $migration) { if ($migration->getState()->getStatus() !== State::STATUS_EXECUTED) { - $this->writeln('Outstanding migrations found, run `migrate` first.'); - return self::SUCCESS; + $this->error('Outstanding migrations found.'); + + if ($this->isInteractive() && $this->output->confirm('Do you want to run `migrate` now?')) { + $console->run('migrate', [], $this->output); + } else { + $this->error('Please run `migrate` first.'); + return self::SUCCESS; + } } } + $this->comment('Detecting schema changes...'); + $schemaCompiler = Compiler::compile( $registry, \array_merge($bootloader->getGenerators($config), [ - $show = new ShowChanges($this->output) + $show = new ShowChanges($this->output), ]), - $config->getSchemaDefaults() + $config->getSchemaDefaults(), ); $schemaCompiler->toMemory($memory); if ($show->hasChanges()) { + if ($this->option('split')) { + \assert($this->container instanceof BinderInterface); + $this->container->bind(GeneratorStrategyInterface::class, MultipleFilesStrategy::class); + } + + $migrations = $this->container->get(GenerateMigrations::class); + (new \Cycle\Schema\Compiler())->compile($registry, [$migrations]); if ($this->option('run')) { diff --git a/src/Console/Command/CycleOrm/RenderCommand.php b/src/Console/Command/CycleOrm/RenderCommand.php index a6527f3..5b0ecf3 100644 --- a/src/Console/Command/CycleOrm/RenderCommand.php +++ b/src/Console/Command/CycleOrm/RenderCommand.php @@ -15,7 +15,6 @@ final class RenderCommand extends AbstractCommand { protected const SIGNATURE = 'cycle:render {format=color : Output format}'; - protected const DESCRIPTION = 'Render available CycleORM schemas'; public function perform( diff --git a/src/Console/Command/CycleOrm/SyncCommand.php b/src/Console/Command/CycleOrm/SyncCommand.php index 680cb89..12d0352 100644 --- a/src/Console/Command/CycleOrm/SyncCommand.php +++ b/src/Console/Command/CycleOrm/SyncCommand.php @@ -8,12 +8,12 @@ use Cycle\Schema\Generator\SyncTables; use Cycle\Schema\Registry; use Spiral\Boot\MemoryInterface; -use Spiral\Console\Command; use Spiral\Cycle\Config\CycleConfig; use Spiral\Cycle\Console\Command\CycleOrm\Generator\ShowChanges; +use Spiral\Cycle\Console\Command\Migrate\AbstractCommand; use Spiral\Cycle\Schema\Compiler; -final class SyncCommand extends Command +final class SyncCommand extends AbstractCommand { protected const NAME = 'cycle:sync'; protected const DESCRIPTION = 'Sync Cycle ORM schema with database without intermediate migration (risk operation)'; @@ -22,19 +22,24 @@ public function perform( SchemaBootloader $bootloader, CycleConfig $config, Registry $registry, - MemoryInterface $memory + MemoryInterface $memory, ): int { + if (!$this->verifyEnvironment(message: 'This operation is not recommended for production environment.')) { + return self::FAILURE; + } + $show = new ShowChanges($this->output); $schemaCompiler = Compiler::compile( $registry, \array_merge($bootloader->getGenerators($config), [$show, new SyncTables()]), - $config->getSchemaDefaults() + $config->getSchemaDefaults(), ); + $schemaCompiler->toMemory($memory); if ($show->hasChanges()) { - $this->writeln("\nORM Schema has been synchronized"); + $this->info('ORM Schema has been synchronized with database.'); } return self::SUCCESS; diff --git a/src/Console/Command/CycleOrm/UpdateCommand.php b/src/Console/Command/CycleOrm/UpdateCommand.php index d5eb8c7..ef83522 100644 --- a/src/Console/Command/CycleOrm/UpdateCommand.php +++ b/src/Console/Command/CycleOrm/UpdateCommand.php @@ -20,17 +20,17 @@ public function perform( SchemaBootloader $bootloader, CycleConfig $config, Registry $registry, - MemoryInterface $memory + MemoryInterface $memory, ): int { - $this->write('Updating ORM schema... '); + $this->info('Updating ORM schema... '); Compiler::compile( $registry, $bootloader->getGenerators($config), - $config->getSchemaDefaults() + $config->getSchemaDefaults(), )->toMemory($memory); - $this->writeln('done'); + $this->info('Schema has been updated.'); return self::SUCCESS; } diff --git a/src/Console/Command/Migrate/AbstractCommand.php b/src/Console/Command/Migrate/AbstractCommand.php index 271f8b0..853305d 100644 --- a/src/Console/Command/Migrate/AbstractCommand.php +++ b/src/Console/Command/Migrate/AbstractCommand.php @@ -15,7 +15,7 @@ abstract class AbstractCommand extends Command { public function __construct( protected Migrator $migrator, - protected MigrationConfig $config + protected MigrationConfig $config, ) { parent::__construct(); } @@ -24,7 +24,7 @@ protected function verifyConfigured(): bool { if (!$this->migrator->isConfigured()) { $this->writeln( - "Migrations are not configured yet, run 'migrate:init' first." + "Migrations are not configured yet, run 'migrate:init' first.", ); return false; @@ -36,17 +36,17 @@ protected function verifyConfigured(): bool /** * Check if current environment is safe to run migration. */ - protected function verifyEnvironment(): bool + protected function verifyEnvironment(string $message = 'Confirmation is required to run migrations!'): bool { - if ($this->option('force') || $this->config->isSafe()) { + if ($this->isForce() || $this->config->isSafe()) { //Safe to run return true; } - $this->writeln('Confirmation is required to run migrations!'); + $this->error($message); if (!$this->askConfirmation()) { - $this->writeln('Cancelling operation...'); + $this->comment('Cancelling operation...'); return false; } @@ -60,7 +60,8 @@ protected function defineOptions(): array static::OPTIONS, [ ['force', 's', InputOption::VALUE_NONE, 'Skip safe environment check'], - ] + ['no-interaction', 'n', InputOption::VALUE_NONE, 'Do not ask any interactive question'], + ], ); } @@ -71,7 +72,17 @@ protected function askConfirmation(): bool return $question->ask( $this->input, $this->output, - new ConfirmationQuestion('Would you like to continue? ') + new ConfirmationQuestion('Would you like to continue? ', false), ); } + + protected function isInteractive(): bool + { + return !$this->option('no-interaction'); + } + + protected function isForce(): bool + { + return $this->option('force'); + } } diff --git a/src/Console/Command/Migrate/InitCommand.php b/src/Console/Command/Migrate/InitCommand.php index 6faefec..e8c418c 100644 --- a/src/Console/Command/Migrate/InitCommand.php +++ b/src/Console/Command/Migrate/InitCommand.php @@ -7,7 +7,7 @@ final class InitCommand extends AbstractCommand { protected const NAME = 'migrate:init'; - protected const DESCRIPTION = 'Init migrations component (create migrations table)'; + protected const DESCRIPTION = 'Create migrations table if not exists.'; /** * Perform command. @@ -15,7 +15,7 @@ final class InitCommand extends AbstractCommand public function perform(): int { $this->migrator->configure(); - $this->writeln('Migrations table were successfully created'); + $this->info('Migration table was successfully created.'); return self::SUCCESS; } diff --git a/src/Console/Command/Migrate/MigrateCommand.php b/src/Console/Command/Migrate/MigrateCommand.php index c5884b7..8a2eb78 100644 --- a/src/Console/Command/Migrate/MigrateCommand.php +++ b/src/Console/Command/Migrate/MigrateCommand.php @@ -9,7 +9,7 @@ final class MigrateCommand extends AbstractCommand { protected const NAME = 'migrate'; - protected const DESCRIPTION = 'Perform one or all outstanding migrations'; + protected const DESCRIPTION = 'Execute one or multiple migrations.'; protected const OPTIONS = [ ['one', 'o', InputOption::VALUE_NONE, 'Execute only one (first) migration'], ]; @@ -34,12 +34,12 @@ public function perform(): int $this->sprintf( "Migration %s was successfully executed.\n", - $migration->getState()->getName() + $migration->getState()->getName(), ); } if (!$found) { - $this->writeln('No outstanding migrations were found.'); + $this->error('No outstanding migrations were found.'); } return self::SUCCESS; diff --git a/src/Console/Command/Migrate/ReplayCommand.php b/src/Console/Command/Migrate/ReplayCommand.php index 21c5f3e..eedd760 100644 --- a/src/Console/Command/Migrate/ReplayCommand.php +++ b/src/Console/Command/Migrate/ReplayCommand.php @@ -16,7 +16,6 @@ final class ReplayCommand extends AbstractCommand ]; /** - * @param Console $console * @throws \Throwable */ public function perform(Console $console): int @@ -35,12 +34,12 @@ public function perform(Console $console): int $migrate['--one'] = true; } - $this->writeln('Rolling back executed migration(s)...'); + $this->warning('Rolling back executed migration(s)...'); $console->run('migrate:rollback', $rollback, $this->output); $this->writeln(''); - $this->writeln('Executing outstanding migration(s)...'); + $this->info('Executing outstanding migration(s)...'); $console->run('migrate', $migrate, $this->output); return self::SUCCESS; diff --git a/src/Console/Command/Migrate/RollbackCommand.php b/src/Console/Command/Migrate/RollbackCommand.php index 4d9e429..2760419 100644 --- a/src/Console/Command/Migrate/RollbackCommand.php +++ b/src/Console/Command/Migrate/RollbackCommand.php @@ -17,7 +17,6 @@ final class RollbackCommand extends AbstractCommand public function perform(): int { if (!$this->verifyEnvironment()) { - //Making sure we can safely migrate in this environment return self::FAILURE; } @@ -30,12 +29,12 @@ public function perform(): int $count--; $this->sprintf( "Migration %s was successfully rolled back.\n", - $migration->getState()->getName() + $migration->getState()->getName(), ); } if (!$found) { - $this->writeln('No executed migrations were found.'); + $this->error('No executed migrations were found.'); } return self::SUCCESS; diff --git a/src/Console/Command/Migrate/StatusCommand.php b/src/Console/Command/Migrate/StatusCommand.php index 8f76647..13a79e3 100644 --- a/src/Console/Command/Migrate/StatusCommand.php +++ b/src/Console/Command/Migrate/StatusCommand.php @@ -7,12 +7,12 @@ use Cycle\Migrations\State; /** - * Show all available migrations and their statuses + * Get list of all available migrations and their statuses. */ final class StatusCommand extends AbstractCommand { protected const NAME = 'migrate:status'; - protected const DESCRIPTION = 'Get list of all available migrations and their statuses'; + protected const DESCRIPTION = 'Get list of all available migrations and their statuses.'; protected const PENDING = 'not executed yet'; public function perform(): int @@ -35,8 +35,8 @@ public function perform(): int $state->getTimeCreated()->format('Y-m-d H:i:s'), $state->getStatus() == State::STATUS_PENDING ? self::PENDING - : ''.$state->getTimeExecuted()->format('Y-m-d H:i:s').'', - ] + : '' . $state->getTimeExecuted()->format('Y-m-d H:i:s') . '', + ], ); } diff --git a/src/Console/Command/Scaffolder/EntityCommand.php b/src/Console/Command/Scaffolder/EntityCommand.php index 3a44abc..929497f 100644 --- a/src/Console/Command/Scaffolder/EntityCommand.php +++ b/src/Console/Command/Scaffolder/EntityCommand.php @@ -19,12 +19,12 @@ class EntityCommand extends AbstractCommand { - protected const NAME = 'create:entity'; + protected const NAME = 'create:entity'; protected const DESCRIPTION = 'Create entity declaration'; - protected const ARGUMENTS = [ + protected const ARGUMENTS = [ ['name', InputArgument::REQUIRED, 'Entity name'], ]; - protected const OPTIONS = [ + protected const OPTIONS = [ [ 'role', 'r', diff --git a/src/Console/Command/Scaffolder/MigrationCommand.php b/src/Console/Command/Scaffolder/MigrationCommand.php index 32bf488..8252ae2 100644 --- a/src/Console/Command/Scaffolder/MigrationCommand.php +++ b/src/Console/Command/Scaffolder/MigrationCommand.php @@ -13,12 +13,12 @@ class MigrationCommand extends AbstractCommand { - protected const NAME = 'create:migration'; + protected const NAME = 'create:migration'; protected const DESCRIPTION = 'Create migration declaration'; - protected const ARGUMENTS = [ + protected const ARGUMENTS = [ ['name', InputArgument::REQUIRED, 'Migration name'], ]; - protected const OPTIONS = [ + protected const OPTIONS = [ [ 'table', 't', @@ -66,12 +66,12 @@ public function perform(Migrator $migrator): int $filename = $migrator->getRepository()->registerMigration( (string)$this->argument('name'), $declaration->getClass()->getName(), - (string) $declaration->getFile() + (string)$declaration->getFile(), ); $this->writeln( "Declaration of '{$declaration->getClass()->getName()}' " - . "has been successfully written into '{$filename}'." + . "has been successfully written into '{$filename}'.", ); return self::SUCCESS; diff --git a/tests/src/Console/Command/CycleOrm/MigrateCommandTest.php b/tests/src/Console/Command/CycleOrm/MigrateCommandTest.php index 7961e3b..e43456d 100644 --- a/tests/src/Console/Command/CycleOrm/MigrateCommandTest.php +++ b/tests/src/Console/Command/CycleOrm/MigrateCommandTest.php @@ -4,8 +4,6 @@ namespace Spiral\Tests\Console\Command\CycleOrm; -use Cycle\Annotated\Annotation\Column; -use Cycle\Annotated\Annotation\Entity; use Cycle\ORM\SchemaInterface; use Spiral\Boot\MemoryInterface; use Spiral\Cycle\Config\CycleConfig; @@ -38,8 +36,15 @@ protected function setUp(): void public function testMigrate(): void { + // Create migration $this->assertConsoleCommandOutputContainsStrings('cycle:migrate', [], self::USER_MIGRATION); - $this->assertConsoleCommandOutputContainsStrings('cycle:migrate', [], 'Outstanding migrations found'); + + $this->assertConsoleCommandOutputContainsStrings('cycle:migrate', [], [ + 'Outstanding migrations found', + 'Detecting schema changes...', + 'was successfully executed.', + 'no database changes has been detected', + ]); } public function testMigrateNoChanges(): void @@ -55,9 +60,10 @@ public function testMigrationShouldBeCreatedWhenNewEntityAppeared(): void $fs = new Files(); - $entityPatch = __DIR__.'/../../../../app/Entities/Tag.php'; + $entityPatch = __DIR__ . '/../../../../app/Entities/Tag.php'; file_put_contents( - $entityPatch, <<<'PHP' + $entityPatch, + <<<'PHP' assertConsoleCommandOutputContainsStrings('cycle:migrate', ['-r' => true], [ @@ -89,8 +95,7 @@ public function testSchemaDefaultsShouldBePassedToCompiler(): void { $config['schema']['defaults'][SchemaInterface::TYPECAST_HANDLER][] = 'foo'; - $memory = new class implements MemoryInterface - { + $memory = new class implements MemoryInterface { private mixed $data; public function loadData(string $section): mixed diff --git a/tests/src/Console/Command/CycleOrm/SyncCommandTest.php b/tests/src/Console/Command/CycleOrm/SyncCommandTest.php index aef66c6..dc674aa 100644 --- a/tests/src/Console/Command/CycleOrm/SyncCommandTest.php +++ b/tests/src/Console/Command/CycleOrm/SyncCommandTest.php @@ -8,15 +8,24 @@ use Spiral\App\Entities\User; use Spiral\Boot\MemoryInterface; use Spiral\Cycle\Config\CycleConfig; +use Spiral\Testing\Attribute\Env; use Spiral\Tests\ConsoleTest; final class SyncCommandTest extends ConsoleTest { public const ENV = [ - 'SAFE_MIGRATIONS' => true, 'USE_MIGRATIONS' => true, ]; + #[Env('SAFE_MIGRATIONS', 'false')] + public function testUnsafeSync(): void + { + $output = $this->runCommand('cycle:sync'); + $this->assertStringContainsString('This operation is not recommended for production environment.', $output); + $this->assertStringContainsString('Cancelling operation...', $output); + } + + #[Env('SAFE_MIGRATIONS', 'true')] public function testSync(): void { $output = $this->runCommand('cycle:sync'); @@ -28,6 +37,7 @@ public function testSync(): void $this->assertSame(1, $u->id); } + #[Env('SAFE_MIGRATIONS', 'true')] public function testSyncDebug(): void { $this->assertConsoleCommandOutputContainsStrings('cycle:sync', ['-vvv'], [ @@ -43,6 +53,7 @@ public function testSyncDebug(): void $this->assertSame(1, $u->id); } + #[Env('SAFE_MIGRATIONS', 'true')] public function testSchemaDefaultsShouldBePassedToCompiler(): void { $config['schema']['defaults'][SchemaInterface::TYPECAST_HANDLER][] = 'foo'; diff --git a/tests/src/Console/Command/Migrate/MigrateCommandTest.php b/tests/src/Console/Command/Migrate/MigrateCommandTest.php index 7a5eba8..df97c67 100644 --- a/tests/src/Console/Command/Migrate/MigrateCommandTest.php +++ b/tests/src/Console/Command/Migrate/MigrateCommandTest.php @@ -5,16 +5,46 @@ namespace Spiral\Tests\Console\Command\Migrate; use Cycle\Database\DatabaseInterface; +use Spiral\Testing\Attribute\Env; use Spiral\Tests\ConsoleTest; final class MigrateCommandTest extends ConsoleTest { public const ENV = [ - 'SAFE_MIGRATIONS' => true, 'USE_MIGRATIONS' => true, ]; + #[Env('SAFE_MIGRATIONS', true)] public function testMigrate(): void + { + $db = $this->initMigrations(); + $this->runCommand('migrate'); + $this->assertCount(4, $db->getTables()); + } + + #[Env('SAFE_MIGRATIONS', false)] + public function tesForceMigrate(): void + { + $db = $this->initMigrations(); + $this->runCommand('migrate', ['--force' => true]); + $this->assertCount(4, $db->getTables()); + } + + #[Env('SAFE_MIGRATIONS', false)] + public function testUnsafeMigrate(): void + { + $db = $this->initMigrations(); + $output = $this->runCommand('migrate'); + $this->assertStringContainsString('Confirmation is required to run migrations!', $output); + $this->assertStringContainsString('Cancelling operation...', $output); + $this->assertCount(1, $db->getTables()); + } + + /** + * @return void + * @throws \Throwable + */ + public function initMigrations(): DatabaseInterface { /** @var DatabaseInterface $db */ $db = $this->getContainer()->get(DatabaseInterface::class); @@ -25,7 +55,6 @@ public function testMigrate(): void $this->assertCount(1, $db->getTables()); - $this->runCommand('migrate'); - $this->assertCount(4, $db->getTables()); + return $db; } }