From 16f2232113989cb793453db5e7f46f4abb87e68b Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 15 Oct 2026 12:40:22 +0300 Subject: [PATCH 01/10] Refactor --- CHANGELOG.md | 4 +- src/Command/CreateCommand.php | 5 + src/Command/DatabaseCommand.php | 106 +++++++++++++------- src/Command/DownCommand.php | 5 + src/Command/HistoryCommand.php | 9 +- src/Command/MarkCommand.php | 5 + src/Command/MissingDatabaseOptionPolicy.php | 34 +++++++ src/Command/NewCommand.php | 11 +- src/Command/RedoCommand.php | 5 + src/Command/UpdateCommand.php | 7 ++ 10 files changed, 146 insertions(+), 45 deletions(-) create mode 100644 src/Command/MissingDatabaseOptionPolicy.php diff --git a/CHANGELOG.md b/CHANGELOG.md index fe32a236..4220a2ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,8 @@ ## 2.1.2 under development - New #363: Add `migrate:mark` to move migration history to a version without executing migrations (@samdark) -- New #360, #366: Add support for additional named databases with separate migration history and `--db` option to select - database (@samdark, @vjik) +- New #360, #366, #371: Add support for additional named databases with separate migration history and `--db` + option to select database (@samdark, @vjik) - Enh #255: Show friendly error on run `./vendor/bin/yii-db-migration` without configuration file (@KalimeroMK) ## 2.1.1 August 10, 2026 diff --git a/src/Command/CreateCommand.php b/src/Command/CreateCommand.php index 1a84cc62..d3a19f00 100644 --- a/src/Command/CreateCommand.php +++ b/src/Command/CreateCommand.php @@ -93,6 +93,11 @@ protected function configure(): void ->setHelp('This command generates new migration file.'); } + protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy + { + return MissingDatabaseOptionPolicy::ProcessDefaultDatabase; + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, diff --git a/src/Command/DatabaseCommand.php b/src/Command/DatabaseCommand.php index 6f8c4fbb..911cf058 100644 --- a/src/Command/DatabaseCommand.php +++ b/src/Command/DatabaseCommand.php @@ -16,6 +16,7 @@ use function count; use function implode; use function in_array; +use function is_string; /** * @internal @@ -30,55 +31,27 @@ public function __construct(private readonly ?DatabaseSetRegistry $databases = n final protected function execute(InputInterface $input, OutputInterface $output): int { - /** @var string|null $name */ - $name = $input->getOption('db'); - $names = $this->databases?->getNames() ?? [DatabaseSetRegistry::DEFAULT_DATABASE]; - $multipleDatabases = count($names) > 1; + $availableNames = $this->databases?->getNames() ?? [DatabaseSetRegistry::DEFAULT_DATABASE]; + $multipleDatabases = count($availableNames) > 1; $io = new SymfonyStyle($input, $output); - if ($name !== null && !in_array($name, $names, true)) { - $io->error( - 'Unknown database "' . OutputFormatter::escape($name) . '". Available databases: ' - . OutputFormatter::escape(implode(', ', $names)) . '.', - ); - - return Command::INVALID; - } - - if ($name === null && $multipleDatabases && ($this instanceof DownCommand || $this instanceof RedoCommand || $this instanceof MarkCommand)) { - $io->error('The --db option is required when multiple databases are configured.'); - + $names = $this->resolveDatabaseNames($input, $availableNames, $multipleDatabases); + if (is_string($names)) { + $io->error($names); return Command::INVALID; } - if ( - $name === null - && $multipleDatabases - && ($this instanceof UpdateCommand || $this instanceof NewCommand) - && ($input->getOption('path') !== [] || $input->getOption('namespace') !== []) - ) { - $io->error('The --db option is required with --path or --namespace when multiple databases are configured.'); - - return Command::INVALID; - } - - if ($name !== null || $this instanceof CreateCommand) { - $names = [$name ?? DatabaseSetRegistry::DEFAULT_DATABASE]; - } - - $isListing = $this instanceof NewCommand || $this instanceof HistoryCommand; $hasResults = false; - foreach ($names as $database) { - $context = $this->databases?->createContext($database); + foreach ($names as $name) { + $context = $this->databases?->createContext($name); if ($multipleDatabases) { - $io->section('Database: ' . OutputFormatter::escape($database)); + $io->section('Database: ' . OutputFormatter::escape($name)); } $status = $this->executeForDatabase($input, $output, $context); - // Listing commands return FAILURE for an empty set; other sets may still have results. - if ($isListing && $status === Command::FAILURE) { + if ($status === null) { continue; } if ($status !== Command::SUCCESS) { @@ -87,12 +60,67 @@ final protected function execute(InputInterface $input, OutputInterface $output) $hasResults = true; } - return $isListing && !$hasResults ? Command::FAILURE : Command::SUCCESS; + return $hasResults ? Command::SUCCESS : Command::FAILURE; } + /** + * Returns how the command behaves when the `--db` option isn't specified. + */ + abstract protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy; + + /** + * Executes the command for a single database. + * + * @return int|null The command exit status, or `null` if the database has an empty result. The command fails if + * all processed databases have empty results. + */ abstract protected function executeForDatabase( InputInterface $input, OutputInterface $output, ?DatabaseContext $context, - ): int; + ): ?int; + + /** + * Resolves names of databases to process. + * + * @param string[] $availableNames + * @return string[]|string The database names, or an error message if the databases can't be resolved. + * + * @psalm-param list $availableNames + * @psalm-return list|string + */ + private function resolveDatabaseNames( + InputInterface $input, + array $availableNames, + bool $multipleDatabases, + ): array|string { + /** @var string|null $name */ + $name = $input->getOption('db'); + + if ($name === null) { + $policy = $this->getMissingDatabaseOptionPolicy($input); + if ($multipleDatabases) { + $error = match ($policy) { + MissingDatabaseOptionPolicy::RejectIfMultiple => 'The --db option is required when multiple databases are configured.', + MissingDatabaseOptionPolicy::RejectIfMultipleWithFilter => 'The --db option is required with --path or --namespace when multiple databases are configured.', + MissingDatabaseOptionPolicy::ProcessDefaultDatabase, + MissingDatabaseOptionPolicy::ProcessAllDatabases => null, + }; + if ($error !== null) { + return $error; + } + } + + return $policy === MissingDatabaseOptionPolicy::ProcessDefaultDatabase + ? [DatabaseSetRegistry::DEFAULT_DATABASE] + : $availableNames; + } + + if (!in_array($name, $availableNames, true)) { + return 'Unknown database "' . OutputFormatter::escape($name) . '". Available databases: ' + . OutputFormatter::escape(implode(', ', $availableNames)) . '.'; + } + + return [$name]; + } } diff --git a/src/Command/DownCommand.php b/src/Command/DownCommand.php index a507c433..686b2fc0 100644 --- a/src/Command/DownCommand.php +++ b/src/Command/DownCommand.php @@ -60,6 +60,11 @@ protected function configure(): void ->addOption('force-yes', 'y', InputOption::VALUE_NONE, 'Force yes to all questions.'); } + protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy + { + return MissingDatabaseOptionPolicy::RejectIfMultiple; + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, diff --git a/src/Command/HistoryCommand.php b/src/Command/HistoryCommand.php index bb65a0d6..69d21e59 100644 --- a/src/Command/HistoryCommand.php +++ b/src/Command/HistoryCommand.php @@ -49,11 +49,16 @@ protected function configure(): void ->addOption('all', 'a', InputOption::VALUE_NONE, 'All migrations.'); } + protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy + { + return MissingDatabaseOptionPolicy::ProcessAllDatabases; + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, ?DatabaseContext $context, - ): int { + ): ?int { $migrator = $context->migrator ?? $this->migrator; $migrationService = $context->migrationService ?? $this->migrationService; @@ -80,7 +85,7 @@ protected function executeForDatabase( if (empty($migrations)) { $io->warning('No migration has been done before.'); - return Command::FAILURE; + return null; } $countMigrations = count($migrations); diff --git a/src/Command/MarkCommand.php b/src/Command/MarkCommand.php index e55d0437..75c7459d 100644 --- a/src/Command/MarkCommand.php +++ b/src/Command/MarkCommand.php @@ -63,6 +63,11 @@ protected function configure(): void ->addOption('force-yes', 'y', InputOption::VALUE_NONE, 'Force yes to all questions.'); } + protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy + { + return MissingDatabaseOptionPolicy::RejectIfMultiple; + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, diff --git a/src/Command/MissingDatabaseOptionPolicy.php b/src/Command/MissingDatabaseOptionPolicy.php new file mode 100644 index 00000000..ce74e39d --- /dev/null +++ b/src/Command/MissingDatabaseOptionPolicy.php @@ -0,0 +1,34 @@ +addOption('namespace', 'ns', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Namespace of migrations to display.'); } + protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy + { + return $input->getOption('path') !== [] || $input->getOption('namespace') !== [] + ? MissingDatabaseOptionPolicy::RejectIfMultipleWithFilter + : MissingDatabaseOptionPolicy::ProcessAllDatabases; + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, ?DatabaseContext $context, - ): int { + ): ?int { $migrator = $context->migrator ?? $this->migrator; $migrationService = $context->migrationService ?? $this->migrationService; @@ -99,7 +106,7 @@ protected function executeForDatabase( if (empty($migrations)) { $io->warning('No new migrations found. Your system is up-to-date.'); - return Command::FAILURE; + return null; } $countMigrations = count($migrations); diff --git a/src/Command/RedoCommand.php b/src/Command/RedoCommand.php index 55f1f8f0..b7034d0a 100644 --- a/src/Command/RedoCommand.php +++ b/src/Command/RedoCommand.php @@ -61,6 +61,11 @@ protected function configure(): void ->addOption('force-yes', 'y', InputOption::VALUE_NONE, 'Force yes to all questions.'); } + protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy + { + return MissingDatabaseOptionPolicy::RejectIfMultiple; + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php index b63ee251..e2f070a8 100644 --- a/src/Command/UpdateCommand.php +++ b/src/Command/UpdateCommand.php @@ -58,6 +58,13 @@ protected function configure(): void ->addOption('force-yes', 'y', InputOption::VALUE_NONE, 'Force yes to all questions.'); } + protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy + { + return $input->getOption('path') !== [] || $input->getOption('namespace') !== [] + ? MissingDatabaseOptionPolicy::RejectIfMultipleWithFilter + : MissingDatabaseOptionPolicy::ProcessAllDatabases; + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, From 73c9b55f6a38da768eaaec210699fbc96353198e Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 15 Oct 2026 13:01:20 +0300 Subject: [PATCH 02/10] Refactor --- docs/guide/en/usage-with-symfony.md | 2 +- src/Command/Context/CreateContext.php | 23 ++++++++ src/Command/Context/DownContext.php | 23 ++++++++ src/Command/Context/MigrationContext.php | 21 ++++++++ .../Context/RedoContext.php} | 9 ++-- src/Command/Context/UpdateContext.php | 23 ++++++++ src/Command/CreateCommand.php | 52 +++++++++++------- src/Command/DatabaseCommand.php | 35 ++++++++++-- src/Command/DownCommand.php | 44 +++++++++------ src/Command/HistoryCommand.php | 33 ++++++++---- src/Command/MarkCommand.php | 36 +++++++++---- src/Command/NewCommand.php | 37 ++++++++----- src/Command/RedoCommand.php | 54 ++++++++++++------- src/Command/UpdateCommand.php | 50 +++++++++++------ src/DatabaseSetRegistry.php | 21 +++----- tests/Migration/DatabaseSetRegistryTest.php | 11 ++-- 16 files changed, 343 insertions(+), 131 deletions(-) create mode 100644 src/Command/Context/CreateContext.php create mode 100644 src/Command/Context/DownContext.php create mode 100644 src/Command/Context/MigrationContext.php rename src/{DatabaseContext.php => Command/Context/RedoContext.php} (66%) create mode 100644 src/Command/Context/UpdateContext.php diff --git a/docs/guide/en/usage-with-symfony.md b/docs/guide/en/usage-with-symfony.md index a804d811..69651ebd 100644 --- a/docs/guide/en/usage-with-symfony.md +++ b/docs/guide/en/usage-with-symfony.md @@ -20,7 +20,7 @@ services: exclude: - '../vendor/yiisoft/db-migration/src/DatabaseSetRegistry.php' - '../vendor/yiisoft/db-migration/src/DatabaseSet.php' - - '../vendor/yiisoft/db-migration/src/DatabaseContext.php' + - '../vendor/yiisoft/db-migration/src/Command/Context/' Yiisoft\Db\Migration\Informer\MigrationInformerInterface: class: 'Yiisoft\Db\Migration\Informer\ConsoleMigrationInformer' diff --git a/src/Command/Context/CreateContext.php b/src/Command/Context/CreateContext.php new file mode 100644 index 00000000..b3602bb7 --- /dev/null +++ b/src/Command/Context/CreateContext.php @@ -0,0 +1,23 @@ + */ #[AsCommand('migrate:create', 'Creates a new migration.')] final class CreateCommand extends DatabaseCommand @@ -98,21 +101,34 @@ protected function getMissingDatabaseOptionPolicy(InputInterface $input): Missin return MissingDatabaseOptionPolicy::ProcessDefaultDatabase; } + protected function createDefaultContext(): CreateContext + { + return new CreateContext($this->migrator, $this->migrationService, $this->createService); + } + + protected function createContext( + DatabaseSet $database, + Migrator $migrator, + MigrationService $migrationService, + ): CreateContext { + return new CreateContext( + $migrator, + $migrationService, + new CreateService($database->db, $database->useTablePrefix), + ); + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, - ?DatabaseContext $context, + object $context, ): int { - $migrator = $context->migrator ?? $this->migrator; - $migrationService = $context->migrationService ?? $this->migrationService; - $createService = $context->createService ?? $this->createService; - $io = new SymfonyStyle($input, $output); - $migrator->setIo($io); - $migrationService->setIo($io); - $createService->setIo($io); + $context->migrator->setIo($io); + $context->migrationService->setIo($io); + $context->createService->setIo($io); - $migrationService->databaseConnection(); + $context->migrationService->databaseConnection(); /** @var string|null $path */ $path = $input->getOption('path'); @@ -121,13 +137,13 @@ protected function executeForDatabase( $namespace = $input->getOption('namespace'); if ($path !== null || $namespace !== null) { - $migrationService->setNewMigrationPath((string) $path); - $migrationService->setNewMigrationNamespace((string) $namespace); + $context->migrationService->setNewMigrationPath((string) $path); + $context->migrationService->setNewMigrationNamespace((string) $namespace); } else { - $namespace = $migrationService->getNewMigrationNamespace(); + $namespace = $context->migrationService->getNewMigrationNamespace(); } - if ($migrationService->before($this->getName() ?? '') === Command::INVALID) { + if ($context->migrationService->before($this->getName() ?? '') === Command::INVALID) { return Command::INVALID; } @@ -157,8 +173,8 @@ protected function executeForDatabase( $and = $input->getOption('and'); $name = $this->generateName($command, $table, $and); - $className = $migrationService->generateClassName($name); - $nameLimit = $migrator->getMigrationNameLimit(); + $className = $context->migrationService->generateClassName($name); + $nameLimit = $context->migrator->getMigrationNameLimit(); if ($nameLimit !== 0 && strlen($className) > $nameLimit) { $io->error('The migration name is too long.'); @@ -167,7 +183,7 @@ protected function executeForDatabase( } try { - $migrationPath = $migrationService->findMigrationPath(); + $migrationPath = $context->migrationService->findMigrationPath(); } catch (LogicException $e) { $io->error($e->getMessage()); @@ -185,7 +201,7 @@ protected function executeForDatabase( /** @var string|null $tableComment */ $tableComment = $input->getOption('table-comment'); - $content = $createService->run( + $content = $context->createService->run( $command, $table, $className, diff --git a/src/Command/DatabaseCommand.php b/src/Command/DatabaseCommand.php index 911cf058..e3eda3e7 100644 --- a/src/Command/DatabaseCommand.php +++ b/src/Command/DatabaseCommand.php @@ -10,8 +10,10 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use Yiisoft\Db\Migration\DatabaseContext; +use Yiisoft\Db\Migration\DatabaseSet; use Yiisoft\Db\Migration\DatabaseSetRegistry; +use Yiisoft\Db\Migration\Migrator; +use Yiisoft\Db\Migration\Service\MigrationService; use function count; use function implode; @@ -20,6 +22,8 @@ /** * @internal + * + * @template TContext of object */ abstract class DatabaseCommand extends Command { @@ -44,7 +48,12 @@ final protected function execute(InputInterface $input, OutputInterface $output) $hasResults = false; foreach ($names as $name) { - $context = $this->databases?->createContext($name); + if ($this->databases === null || $name === DatabaseSetRegistry::DEFAULT_DATABASE) { + $context = $this->createDefaultContext(); + } else { + [$database, $migrator, $migrationService] = $this->databases->createServices($name); + $context = $this->createContext($database, $migrator, $migrationService); + } if ($multipleDatabases) { $io->section('Database: ' . OutputFormatter::escape($name)); @@ -68,16 +77,36 @@ final protected function execute(InputInterface $input, OutputInterface $output) */ abstract protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy; + /** + * Creates the command context for the default database from the command dependencies. + * + * @psalm-return TContext + */ + abstract protected function createDefaultContext(): object; + + /** + * Creates the command context for a named database. + * + * @psalm-return TContext + */ + abstract protected function createContext( + DatabaseSet $database, + Migrator $migrator, + MigrationService $migrationService, + ): object; + /** * Executes the command for a single database. * + * @psalm-param TContext $context + * * @return int|null The command exit status, or `null` if the database has an empty result. The command fails if * all processed databases have empty results. */ abstract protected function executeForDatabase( InputInterface $input, OutputInterface $output, - ?DatabaseContext $context, + object $context, ): ?int; /** diff --git a/src/Command/DownCommand.php b/src/Command/DownCommand.php index 686b2fc0..2df54f80 100644 --- a/src/Command/DownCommand.php +++ b/src/Command/DownCommand.php @@ -11,7 +11,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Throwable; -use Yiisoft\Db\Migration\DatabaseContext; +use Yiisoft\Db\Migration\Command\Context\DownContext; +use Yiisoft\Db\Migration\DatabaseSet; use Yiisoft\Db\Migration\DatabaseSetRegistry; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Runner\DownRunner; @@ -37,6 +38,8 @@ * ./yii migrate:down --path=@vendor/yiisoft/rbac-db/migrations --path=@vendor/yiisoft/cache-db/migrations * ./yii migrate:down --namespace=Yiisoft\\Rbac\\Db\\Migrations --namespace=Yiisoft\\Cache\\Db\\Migrations * ``` + * + * @extends DatabaseCommand */ #[AsCommand('migrate:down', 'Reverts the specified number of latest migrations.')] final class DownCommand extends DatabaseCommand @@ -65,23 +68,32 @@ protected function getMissingDatabaseOptionPolicy(InputInterface $input): Missin return MissingDatabaseOptionPolicy::RejectIfMultiple; } + protected function createDefaultContext(): DownContext + { + return new DownContext($this->migrator, $this->migrationService, $this->downRunner); + } + + protected function createContext( + DatabaseSet $database, + Migrator $migrator, + MigrationService $migrationService, + ): DownContext { + return new DownContext($migrator, $migrationService, new DownRunner($migrator)); + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, - ?DatabaseContext $context, + object $context, ): int { - $migrator = $context->migrator ?? $this->migrator; - $migrationService = $context->migrationService ?? $this->migrationService; - $downRunner = $context->downRunner ?? $this->downRunner; - $io = new SymfonyStyle($input, $output); - $migrator->setIo($io); - $migrationService->setIo($io); - $downRunner->setIo($io); + $context->migrator->setIo($io); + $context->migrationService->setIo($io); + $context->downRunner->setIo($io); - $migrationService->databaseConnection(); + $context->migrationService->databaseConnection(); - $migrationService->before($this->getName() ?? ''); + $context->migrationService->before($this->getName() ?? ''); $limit = !$input->getOption('all') ? (int) $input->getOption('limit') @@ -99,9 +111,9 @@ protected function executeForDatabase( $namespaces = $input->getOption('namespace'); if (!empty($paths) || !empty($namespaces)) { - $migrations = $migrator->getHistory(); + $migrations = $context->migrator->getHistory(); $migrations = array_keys($migrations); - $migrations = $migrationService->filterMigrations($migrations, $namespaces, $paths); + $migrations = $context->migrationService->filterMigrations($migrations, $namespaces, $paths); if (empty($migrations)) { $io->warning('No applied migrations found.'); @@ -113,7 +125,7 @@ protected function executeForDatabase( $migrations = array_slice($migrations, 0, $limit); } } else { - $migrations = $migrator->getHistory($limit); + $migrations = $context->migrator->getHistory($limit); if (empty($migrations)) { $output->writeln("Apply at least one migration first.\n"); @@ -137,12 +149,12 @@ protected function executeForDatabase( } if ($input->getOption('force-yes') || $io->confirm("Revert the above $migrationWord?")) { - $instances = $migrationService->makeRevertibleMigrations($migrations); + $instances = $context->migrationService->makeRevertibleMigrations($migrations); $migrationWas = ($countMigrations === 1 ? 'migration was' : 'migrations were'); foreach ($instances as $i => $instance) { try { - $downRunner->run($instance, $i + 1); + $context->downRunner->run($instance, $i + 1); } catch (Throwable $e) { $output->writeln("\nTotal $i out of $countMigrations $migrationWas reverted.\n"); $io->error($i > 0 ? 'Partially reverted.' : 'Not reverted.'); diff --git a/src/Command/HistoryCommand.php b/src/Command/HistoryCommand.php index 69d21e59..2b3338c5 100644 --- a/src/Command/HistoryCommand.php +++ b/src/Command/HistoryCommand.php @@ -10,7 +10,8 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use Yiisoft\Db\Migration\DatabaseContext; +use Yiisoft\Db\Migration\Command\Context\MigrationContext; +use Yiisoft\Db\Migration\DatabaseSet; use Yiisoft\Db\Migration\DatabaseSetRegistry; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Service\MigrationService; @@ -30,6 +31,8 @@ * ./yii migrate:history --limit=5 # last 5 migrations * ./yii migrate:history --all # whole history * ``` + * + * @extends DatabaseCommand */ #[AsCommand('migrate:history', 'Displays the migration history.')] final class HistoryCommand extends DatabaseCommand @@ -54,21 +57,31 @@ protected function getMissingDatabaseOptionPolicy(InputInterface $input): Missin return MissingDatabaseOptionPolicy::ProcessAllDatabases; } + protected function createDefaultContext(): MigrationContext + { + return new MigrationContext($this->migrator, $this->migrationService); + } + + protected function createContext( + DatabaseSet $database, + Migrator $migrator, + MigrationService $migrationService, + ): MigrationContext { + return new MigrationContext($migrator, $migrationService); + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, - ?DatabaseContext $context, + object $context, ): ?int { - $migrator = $context->migrator ?? $this->migrator; - $migrationService = $context->migrationService ?? $this->migrationService; - $io = new SymfonyStyle($input, $output); - $migrator->setIo($io); - $migrationService->setIo($io); + $context->migrator->setIo($io); + $context->migrationService->setIo($io); - $migrationService->databaseConnection(); + $context->migrationService->databaseConnection(); - $migrationService->before($this->getName() ?? ''); + $context->migrationService->before($this->getName() ?? ''); $limit = !$input->getOption('all') ? (int) $input->getOption('limit') @@ -80,7 +93,7 @@ protected function executeForDatabase( return Command::INVALID; } - $migrations = $migrator->getHistory($limit); + $migrations = $context->migrator->getHistory($limit); if (empty($migrations)) { $io->warning('No migration has been done before.'); diff --git a/src/Command/MarkCommand.php b/src/Command/MarkCommand.php index 75c7459d..5662e91a 100644 --- a/src/Command/MarkCommand.php +++ b/src/Command/MarkCommand.php @@ -12,7 +12,8 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use Yiisoft\Db\Migration\DatabaseContext; +use Yiisoft\Db\Migration\Command\Context\MigrationContext; +use Yiisoft\Db\Migration\DatabaseSet; use Yiisoft\Db\Migration\DatabaseSetRegistry; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Service\MigrationService; @@ -42,6 +43,8 @@ * * A pending target is recorded along with all earlier pending migrations. A recorded target removes newer history * entries. Use the full class name when several migrations share a timestamp. + * + * @extends DatabaseCommand */ #[AsCommand('migrate:mark', 'Modifies migration history without executing migrations. WARNING: Use only if you understand the consequences; incorrect history can cause data loss.')] final class MarkCommand extends DatabaseCommand @@ -68,17 +71,28 @@ protected function getMissingDatabaseOptionPolicy(InputInterface $input): Missin return MissingDatabaseOptionPolicy::RejectIfMultiple; } + protected function createDefaultContext(): MigrationContext + { + return new MigrationContext($this->migrator, $this->migrationService); + } + + protected function createContext( + DatabaseSet $database, + Migrator $migrator, + MigrationService $migrationService, + ): MigrationContext { + return new MigrationContext($migrator, $migrationService); + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, - ?DatabaseContext $context, + object $context, ): int { - $migrator = $context->migrator ?? $this->migrator; - $service = $context->migrationService ?? $this->migrationService; $io = new SymfonyStyle($input, $output); - $migrator->setIo($io); - $service->setIo($io); - $service->databaseConnection(); + $context->migrator->setIo($io); + $context->migrationService->setIo($io); + $context->migrationService->databaseConnection(); /** @var string $version */ $version = $input->getArgument('version'); @@ -98,7 +112,7 @@ protected function executeForDatabase( $add = []; $remove = []; $found = false; - $history = array_keys($migrator->getHistory()); + $history = array_keys($context->migrator->getHistory()); $isRecordedClass = $timestamp === null && in_array( $version, array_map(static fn(string $name): string => trim($name, '\\'), $history), @@ -107,7 +121,7 @@ protected function executeForDatabase( // Exact recorded classes do not need source files. Timestamps still select pending targets first. if ($version !== self::BASE_MIGRATION && !$isRecordedClass) { - $pending = $service->getNewMigrations(); + $pending = $context->migrationService->getNewMigrations(); foreach ($pending as $i => $migration) { if ($this->matches($migration, $version, $timestamp)) { $add = array_slice($pending, 0, $i + 1); @@ -138,7 +152,7 @@ protected function executeForDatabase( return Command::SUCCESS; } - $limit = $migrator->getMigrationNameLimit(); + $limit = $context->migrator->getMigrationNameLimit(); foreach ($add as $migration) { if ($limit !== null && strlen($migration) > $limit) { $io->error('The migration name "' . OutputFormatter::escape($migration) . '" is too long.'); @@ -148,7 +162,7 @@ protected function executeForDatabase( $io->note('Only migration history will change. No migrations will be applied or reverted.'); if ($input->getOption('force-yes') || $io->confirm("Set migration history at $label?", false)) { - $migrator->updateHistory($add, $remove); + $context->migrator->updateHistory($add, $remove); $io->success("The migration history is set at $label. No actual migration was performed."); } diff --git a/src/Command/NewCommand.php b/src/Command/NewCommand.php index d93cd212..7d6ca705 100644 --- a/src/Command/NewCommand.php +++ b/src/Command/NewCommand.php @@ -10,7 +10,8 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use Yiisoft\Db\Migration\DatabaseContext; +use Yiisoft\Db\Migration\Command\Context\MigrationContext; +use Yiisoft\Db\Migration\DatabaseSet; use Yiisoft\Db\Migration\DatabaseSetRegistry; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Service\MigrationService; @@ -36,6 +37,8 @@ * ./yii migrate:new --path=@vendor/yiisoft/rbac-db/migrations --path=@vendor/yiisoft/cache-db/migrations * ./yii migrate:new --namespace=Yiisoft\\Rbac\\Db\\Migrations --namespace=Yiisoft\\Cache\\Db\\Migrations * ``` + * + * @extends DatabaseCommand */ #[AsCommand('migrate:new', 'Displays not yet applied migrations.')] final class NewCommand extends DatabaseCommand @@ -64,19 +67,29 @@ protected function getMissingDatabaseOptionPolicy(InputInterface $input): Missin : MissingDatabaseOptionPolicy::ProcessAllDatabases; } + protected function createDefaultContext(): MigrationContext + { + return new MigrationContext($this->migrator, $this->migrationService); + } + + protected function createContext( + DatabaseSet $database, + Migrator $migrator, + MigrationService $migrationService, + ): MigrationContext { + return new MigrationContext($migrator, $migrationService); + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, - ?DatabaseContext $context, + object $context, ): ?int { - $migrator = $context->migrator ?? $this->migrator; - $migrationService = $context->migrationService ?? $this->migrationService; - $io = new SymfonyStyle($input, $output); - $migrator->setIo($io); - $migrationService->setIo($io); + $context->migrator->setIo($io); + $context->migrationService->setIo($io); - $migrationService->databaseConnection(); + $context->migrationService->databaseConnection(); /** @var string[] $paths */ $paths = $input->getOption('path'); @@ -85,11 +98,11 @@ protected function executeForDatabase( $namespaces = $input->getOption('namespace'); if (!empty($paths) || !empty($namespaces)) { - $migrationService->setSourcePaths($paths); - $migrationService->setSourceNamespaces($namespaces); + $context->migrationService->setSourcePaths($paths); + $context->migrationService->setSourceNamespaces($namespaces); } - $migrationService->before($this->getName() ?? ''); + $context->migrationService->before($this->getName() ?? ''); $limit = !$input->getOption('all') ? (int) $input->getOption('limit') @@ -101,7 +114,7 @@ protected function executeForDatabase( return Command::INVALID; } - $migrations = $migrationService->getNewMigrations(); + $migrations = $context->migrationService->getNewMigrations(); if (empty($migrations)) { $io->warning('No new migrations found. Your system is up-to-date.'); diff --git a/src/Command/RedoCommand.php b/src/Command/RedoCommand.php index b7034d0a..13cb4922 100644 --- a/src/Command/RedoCommand.php +++ b/src/Command/RedoCommand.php @@ -11,7 +11,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Throwable; -use Yiisoft\Db\Migration\DatabaseContext; +use Yiisoft\Db\Migration\Command\Context\RedoContext; +use Yiisoft\Db\Migration\DatabaseSet; use Yiisoft\Db\Migration\DatabaseSetRegistry; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Runner\DownRunner; @@ -37,6 +38,8 @@ * ./yii migrate:redo --path=@vendor/yiisoft/rbac-db/migrations # redo the last migration from the directory * ./yii migrate:redo --namespace=Yiisoft\\Rbac\\Db\\Migrations # redo the last migration from the namespace * ``` + * + * @extends DatabaseCommand */ #[AsCommand('migrate:redo', 'Redoes the last few migrations.')] final class RedoCommand extends DatabaseCommand @@ -66,25 +69,38 @@ protected function getMissingDatabaseOptionPolicy(InputInterface $input): Missin return MissingDatabaseOptionPolicy::RejectIfMultiple; } + protected function createDefaultContext(): RedoContext + { + return new RedoContext($this->migrator, $this->migrationService, $this->downRunner, $this->updateRunner); + } + + protected function createContext( + DatabaseSet $database, + Migrator $migrator, + MigrationService $migrationService, + ): RedoContext { + return new RedoContext( + $migrator, + $migrationService, + new DownRunner($migrator), + new UpdateRunner($migrator), + ); + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, - ?DatabaseContext $context, + object $context, ): int { - $migrator = $context->migrator ?? $this->migrator; - $migrationService = $context->migrationService ?? $this->migrationService; - $downRunner = $context->downRunner ?? $this->downRunner; - $updateRunner = $context->updateRunner ?? $this->updateRunner; - $io = new SymfonyStyle($input, $output); - $migrator->setIo($io); - $migrationService->setIo($io); - $downRunner->setIo($io); - $updateRunner->setIo($io); + $context->migrator->setIo($io); + $context->migrationService->setIo($io); + $context->downRunner->setIo($io); + $context->updateRunner->setIo($io); - $migrationService->databaseConnection(); + $context->migrationService->databaseConnection(); - $migrationService->before($this->getName() ?? ''); + $context->migrationService->before($this->getName() ?? ''); $limit = !$input->getOption('all') ? (int) $input->getOption('limit') @@ -102,9 +118,9 @@ protected function executeForDatabase( $namespaces = $input->getOption('namespace'); if (!empty($paths) || !empty($namespaces)) { - $migrations = $migrator->getHistory(); + $migrations = $context->migrator->getHistory(); $migrations = array_keys($migrations); - $migrations = $migrationService->filterMigrations($migrations, $namespaces, $paths); + $migrations = $context->migrationService->filterMigrations($migrations, $namespaces, $paths); if (empty($migrations)) { $io->warning('No applied migrations found.'); @@ -116,7 +132,7 @@ protected function executeForDatabase( $migrations = array_slice($migrations, 0, $limit); } } else { - $migrations = $migrator->getHistory($limit); + $migrations = $context->migrator->getHistory($limit); if (empty($migrations)) { $io->warning('No migration has been done before.'); @@ -137,12 +153,12 @@ protected function executeForDatabase( } if ($input->getOption('force-yes') || $io->confirm("Redo the above $migrationWord?")) { - $instances = $migrationService->makeRevertibleMigrations($migrations); + $instances = $context->migrationService->makeRevertibleMigrations($migrations); $migrationWas = ($countMigrations === 1 ? 'migration was' : 'migrations were'); foreach ($instances as $i => $instance) { try { - $downRunner->run($instance, $i + 1); + $context->downRunner->run($instance, $i + 1); } catch (Throwable $e) { $output->writeln("\nTotal $i out of $countMigrations $migrationWas reverted.\n"); $io->error($i > 0 ? 'Partially reverted.' : 'Not reverted.'); @@ -153,7 +169,7 @@ protected function executeForDatabase( foreach (array_reverse($instances) as $i => $instance) { try { - $updateRunner->run($instance, $countMigrations - $i); + $context->updateRunner->run($instance, $countMigrations - $i); } catch (Throwable $e) { $output->writeln("\nTotal $i out of $countMigrations $migrationWas applied.\n"); $io->error($i > 0 ? 'Reverted but partially applied.' : 'Reverted but not applied.'); diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php index e2f070a8..dfe6a922 100644 --- a/src/Command/UpdateCommand.php +++ b/src/Command/UpdateCommand.php @@ -11,7 +11,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Throwable; -use Yiisoft\Db\Migration\DatabaseContext; +use Yiisoft\Db\Migration\Command\Context\UpdateContext; +use Yiisoft\Db\Migration\DatabaseSet; use Yiisoft\Db\Migration\DatabaseSetRegistry; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Runner\UpdateRunner; @@ -36,6 +37,8 @@ * ./yii migrate:up --path=@vendor/yiisoft/rbac-db/migrations --path=@vendor/yiisoft/cache-db/migrations * ./yii migrate:up --namespace=Yiisoft\\Rbac\\Db\\Migrations --namespace=Yiisoft\\Cache\\Db\\Migrations * ``` + * + * @extends DatabaseCommand */ #[AsCommand('migrate:up', 'Applies new migrations.')] final class UpdateCommand extends DatabaseCommand @@ -65,21 +68,34 @@ protected function getMissingDatabaseOptionPolicy(InputInterface $input): Missin : MissingDatabaseOptionPolicy::ProcessAllDatabases; } + protected function createDefaultContext(): UpdateContext + { + return new UpdateContext($this->migrator, $this->migrationService, $this->updateRunner); + } + + protected function createContext( + DatabaseSet $database, + Migrator $migrator, + MigrationService $migrationService, + ): UpdateContext { + return new UpdateContext( + $migrator, + $migrationService, + new UpdateRunner($migrator), + ); + } + protected function executeForDatabase( InputInterface $input, OutputInterface $output, - ?DatabaseContext $context, + object $context, ): int { - $migrator = $context->migrator ?? $this->migrator; - $migrationService = $context->migrationService ?? $this->migrationService; - $updateRunner = $context->updateRunner ?? $this->updateRunner; - $io = new SymfonyStyle($input, $output); - $migrator->setIo($io); - $migrationService->setIo($io); - $updateRunner->setIo($io); + $context->migrator->setIo($io); + $context->migrationService->setIo($io); + $context->updateRunner->setIo($io); - $migrationService->databaseConnection(); + $context->migrationService->databaseConnection(); /** @var string[] $paths */ $paths = $input->getOption('path'); @@ -88,11 +104,11 @@ protected function executeForDatabase( $namespaces = $input->getOption('namespace'); if (!empty($paths) || !empty($namespaces)) { - $migrationService->setSourcePaths($paths); - $migrationService->setSourceNamespaces($namespaces); + $context->migrationService->setSourcePaths($paths); + $context->migrationService->setSourceNamespaces($namespaces); } - if ($migrationService->before($this->getName() ?? '') === Command::INVALID) { + if ($context->migrationService->before($this->getName() ?? '') === Command::INVALID) { return Command::INVALID; } @@ -108,7 +124,7 @@ protected function executeForDatabase( } } - $migrations = $migrationService->getNewMigrations(); + $migrations = $context->migrationService->getNewMigrations(); if (empty($migrations)) { $output->writeln("No new migrations found.\n"); @@ -129,7 +145,7 @@ protected function executeForDatabase( } foreach ($migrations as $i => $migration) { - $nameLimit = $migrator->getMigrationNameLimit(); + $nameLimit = $context->migrator->getMigrationNameLimit(); if (strlen($migration) > $nameLimit) { $output->writeln( @@ -144,12 +160,12 @@ protected function executeForDatabase( } if ($input->getOption('force-yes') || $io->confirm("Apply the above $migrationWord?")) { - $instances = $migrationService->makeMigrations($migrations); + $instances = $context->migrationService->makeMigrations($migrations); $migrationWas = ($migrationsCount === 1 ? 'migration was' : 'migrations were'); foreach ($instances as $i => $instance) { try { - $updateRunner->run($instance, $i + 1); + $context->updateRunner->run($instance, $i + 1); } catch (Throwable $e) { $output->writeln("\nTotal $i out of $migrationsCount new $migrationWas applied.\n"); $io->error($i > 0 ? 'Partially updated.' : 'Not updated.'); diff --git a/src/DatabaseSetRegistry.php b/src/DatabaseSetRegistry.php index e2db8369..74502709 100644 --- a/src/DatabaseSetRegistry.php +++ b/src/DatabaseSetRegistry.php @@ -6,9 +6,6 @@ use InvalidArgumentException; use Yiisoft\Db\Migration\Informer\MigrationInformerInterface; -use Yiisoft\Db\Migration\Runner\DownRunner; -use Yiisoft\Db\Migration\Runner\UpdateRunner; -use Yiisoft\Db\Migration\Service\Generate\CreateService; use Yiisoft\Db\Migration\Service\MigrationService; use Yiisoft\Injector\Injector; @@ -63,14 +60,14 @@ public function getNames(): array } /** + * Creates services for a named database. + * * @internal + * + * @psalm-return array{DatabaseSet, Migrator, MigrationService} */ - public function createContext(string $name): ?DatabaseContext + public function createServices(string $name): array { - if ($name === self::DEFAULT_DATABASE) { - return null; - } - $database = $this->databases[$name] ?? throw new InvalidArgumentException("Unknown database set: $name."); $migrator = new Migrator( $database->db, @@ -85,12 +82,6 @@ public function createContext(string $name): ?DatabaseContext $service->setSourceNamespaces($database->sourceNamespaces); $service->setSourcePaths($database->sourcePaths); - return new DatabaseContext( - $migrator, - $service, - new CreateService($database->db, $database->useTablePrefix), - new DownRunner($migrator), - new UpdateRunner($migrator), - ); + return [$database, $migrator, $service]; } } diff --git a/tests/Migration/DatabaseSetRegistryTest.php b/tests/Migration/DatabaseSetRegistryTest.php index 27acfe51..a5e1f080 100644 --- a/tests/Migration/DatabaseSetRegistryTest.php +++ b/tests/Migration/DatabaseSetRegistryTest.php @@ -41,19 +41,22 @@ public function testRejectsDefaultName(): void ]); } - public function testDefaultUsesExistingServices(): void + public function testCannotCreateServicesForDefaultDatabase(): void { $registry = new DatabaseSetRegistry(new Injector(), new NullMigrationInformer()); - self::assertNull($registry->createContext('default')); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unknown database set: default.'); + $registry->createServices('default'); } - public function testCannotCreateContextForUnknownDatabase(): void + public function testCannotCreateServicesForUnknownDatabase(): void { $registry = new DatabaseSetRegistry(new Injector(), new NullMigrationInformer()); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Unknown database set: missing.'); - $registry->createContext('missing'); + $registry->createServices('missing'); } public function testDefaultIsFirstFollowedByConfigurationOrder(): void From ae5a25073956af7d87652f634862ef20485425c7 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 15 Oct 2026 13:20:03 +0300 Subject: [PATCH 03/10] Refactor --- bin/yii-db-migration | 8 +- bin/yii-db-migration.php | 2 +- config/di-console.php | 9 +- docs/guide/en/usage-standalone.md | 6 +- docs/guide/en/usage-with-symfony.md | 16 ++-- docs/guide/en/usage-with-yii-console.md | 4 +- src/Command/CreateCommand.php | 19 ++-- src/Command/DatabaseCommand.php | 23 ++--- src/Command/DownCommand.php | 15 ++-- src/Command/HistoryCommand.php | 15 ++-- src/Command/MarkCommand.php | 15 ++-- src/Command/NewCommand.php | 15 ++-- src/Command/RedoCommand.php | 21 ++--- src/Command/UpdateCommand.php | 19 ++-- src/{DatabaseSet.php => DatabaseConfig.php} | 4 +- ...seSetRegistry.php => DatabaseRegistry.php} | 32 ++++--- src/DatabaseSetup.php | 19 ++++ tests/Driver/Sqlite/DatabaseCommandsTest.php | 30 +++---- tests/Migration/BinTest.php | 4 +- tests/Migration/ConfigTest.php | 4 +- tests/Migration/DatabaseRegistryTest.php | 87 +++++++++++++++++++ tests/Migration/DatabaseSetRegistryTest.php | 71 --------------- 22 files changed, 224 insertions(+), 214 deletions(-) rename src/{DatabaseSet.php => DatabaseConfig.php} (88%) rename src/{DatabaseSetRegistry.php => DatabaseRegistry.php} (65%) create mode 100644 src/DatabaseSetup.php create mode 100644 tests/Migration/DatabaseRegistryTest.php delete mode 100644 tests/Migration/DatabaseSetRegistryTest.php diff --git a/bin/yii-db-migration b/bin/yii-db-migration index 7ebbfd4a..152234ff 100644 --- a/bin/yii-db-migration +++ b/bin/yii-db-migration @@ -13,8 +13,8 @@ use Yiisoft\Db\Migration\Command\MarkCommand; use Yiisoft\Db\Migration\Command\NewCommand; use Yiisoft\Db\Migration\Command\RedoCommand; use Yiisoft\Db\Migration\Command\UpdateCommand; -use Yiisoft\Db\Migration\DatabaseSet; -use Yiisoft\Db\Migration\DatabaseSetRegistry; +use Yiisoft\Db\Migration\DatabaseConfig; +use Yiisoft\Db\Migration\DatabaseRegistry; use Yiisoft\Db\Migration\Informer\ConsoleMigrationInformer; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Runner\DownRunner; @@ -45,7 +45,7 @@ if (!file_exists($configFile)) { * @psalm-var array{ * container: ContainerInterface|null, * db: ConnectionInterface|null, - * databases?: array, + * databases?: array, * useTablePrefix: bool, * historyTable: string, * migrationNameLimit: int|null, @@ -79,7 +79,7 @@ $migrationService->setNewMigrationPath($params['newMigrationPath']); $migrationService->setSourceNamespaces($params['sourceNamespaces']); $migrationService->setSourcePaths($params['sourcePaths']); -$databases = new DatabaseSetRegistry($injector, $migrationInformer, $params['databases'] ?? []); +$databases = new DatabaseRegistry($injector, $migrationInformer, $params['databases'] ?? []); $application = new Application('Yii Database Migration Tool', '1.0.0'); $application->addCommands([ diff --git a/bin/yii-db-migration.php b/bin/yii-db-migration.php index 2039bdac..63b8877c 100644 --- a/bin/yii-db-migration.php +++ b/bin/yii-db-migration.php @@ -18,7 +18,7 @@ 'db' => null, /** - * Additional named databases (name => Yiisoft\Db\Migration\DatabaseSet instance). + * Additional named databases (name => Yiisoft\Db\Migration\DatabaseConfig instance). * The "default" database is configured by the other options and can't be defined here. * See docs/guide/en/multiple-databases.md. */ diff --git a/config/di-console.php b/config/di-console.php index cd84c342..4d78c527 100644 --- a/config/di-console.php +++ b/config/di-console.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use Yiisoft\Db\Migration\DatabaseSet; -use Yiisoft\Db\Migration\DatabaseSetRegistry; +use Yiisoft\Db\Migration\DatabaseConfig; +use Yiisoft\Db\Migration\DatabaseRegistry; use Yiisoft\Db\Migration\Informer\ConsoleMigrationInformer; use Yiisoft\Db\Migration\Informer\MigrationInformerInterface; use Yiisoft\Db\Migration\Service\MigrationService; @@ -13,12 +13,11 @@ /** @var array $params */ return [ - DatabaseSetRegistry::class => [ - 'class' => DatabaseSetRegistry::class, + DatabaseRegistry::class => [ '__construct()' => [ 'databases' => array_map( static fn(array $database): DynamicReference => DynamicReference::to([ - 'class' => DatabaseSet::class, + 'class' => DatabaseConfig::class, '__construct()' => ['db' => Reference::to($database['db'])] + $database, ]), $params['yiisoft/db-migration']['databases'], diff --git a/docs/guide/en/usage-standalone.md b/docs/guide/en/usage-standalone.md index 9a7a1a90..22fcf39c 100644 --- a/docs/guide/en/usage-standalone.md +++ b/docs/guide/en/usage-standalone.md @@ -34,16 +34,16 @@ each additional database in `databases`. The example below assumes you have alre `$analyticsConnection` in the same way as `db`, as described in [With configuration file](#with-configuration-file). ```php -use Yiisoft\Db\Migration\DatabaseSet; +use Yiisoft\Db\Migration\DatabaseConfig; return [ // ... 'databases' => [ - 'maps' => new DatabaseSet( + 'maps' => new DatabaseConfig( $mapsConnection, newMigrationPath: __DIR__ . '/config/migrations/maps', ), - 'analytics' => new DatabaseSet( + 'analytics' => new DatabaseConfig( $analyticsConnection, newMigrationPath: __DIR__ . '/config/migrations/analytics', ), diff --git a/docs/guide/en/usage-with-symfony.md b/docs/guide/en/usage-with-symfony.md index 69651ebd..f977bbf2 100644 --- a/docs/guide/en/usage-with-symfony.md +++ b/docs/guide/en/usage-with-symfony.md @@ -18,8 +18,8 @@ services: Yiisoft\Db\Migration\: resource: '../vendor/yiisoft/db-migration/src/' exclude: - - '../vendor/yiisoft/db-migration/src/DatabaseSetRegistry.php' - - '../vendor/yiisoft/db-migration/src/DatabaseSet.php' + - '../vendor/yiisoft/db-migration/src/DatabaseRegistry.php' + - '../vendor/yiisoft/db-migration/src/DatabaseConfig.php' - '../vendor/yiisoft/db-migration/src/Command/Context/' Yiisoft\Db\Migration\Informer\MigrationInformerInterface: @@ -66,27 +66,27 @@ That's it. Now you can use `bin/console migrate:*` commands. ## Multiple databases The `default` database is configured as described above: it uses the `ConnectionInterface` service and the -`MigrationService` configuration. Add a `DatabaseSet` service for each additional database and pass them to -`DatabaseSetRegistry`. The commands receive the registry through autowiring. The example below assumes that the -`app.db.maps` and `app.db.analytics` connection services already exist. +`MigrationService` configuration. Add a `DatabaseConfig` service for each additional database and pass them +to `DatabaseRegistry`. The commands receive the registry through autowiring. The example below assumes that +the `app.db.maps` and `app.db.analytics` connection services already exist. ```yaml services: # ... app.migrations.maps: - class: Yiisoft\Db\Migration\DatabaseSet + class: Yiisoft\Db\Migration\DatabaseConfig arguments: $db: '@app.db.maps' $newMigrationPath: '%kernel.project_dir%/config/migrations/maps' app.migrations.analytics: - class: Yiisoft\Db\Migration\DatabaseSet + class: Yiisoft\Db\Migration\DatabaseConfig arguments: $db: '@app.db.analytics' $newMigrationPath: '%kernel.project_dir%/config/migrations/analytics' - Yiisoft\Db\Migration\DatabaseSetRegistry: + Yiisoft\Db\Migration\DatabaseRegistry: arguments: $databases: maps: '@app.migrations.maps' diff --git a/docs/guide/en/usage-with-yii-console.md b/docs/guide/en/usage-with-yii-console.md index 151ec568..f67dd96d 100644 --- a/docs/guide/en/usage-with-yii-console.md +++ b/docs/guide/en/usage-with-yii-console.md @@ -70,8 +70,8 @@ a DI container service ID: ], ``` -Each set is an array of `Yiisoft\Db\Migration\DatabaseSet` constructor arguments, where `db` is the ID of a connection -service. Define these connections in the DI container, for example, in `config/common/db.php`: +Each set is an array of `Yiisoft\Db\Migration\DatabaseConfig` constructor arguments, where `db` is the ID of +a connection service. Define these connections in the DI container, for example, in `config/common/db.php`: ```php use Yiisoft\Db\Sqlite\Connection as SqliteConnection; diff --git a/src/Command/CreateCommand.php b/src/Command/CreateCommand.php index 10aa7b82..4e005883 100644 --- a/src/Command/CreateCommand.php +++ b/src/Command/CreateCommand.php @@ -13,8 +13,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Yiisoft\Db\Migration\Command\Context\CreateContext; -use Yiisoft\Db\Migration\DatabaseSet; -use Yiisoft\Db\Migration\DatabaseSetRegistry; +use Yiisoft\Db\Migration\DatabaseRegistry; +use Yiisoft\Db\Migration\DatabaseSetup; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Service\Generate\CreateService; use Yiisoft\Db\Migration\Service\MigrationService; @@ -78,7 +78,7 @@ public function __construct( private readonly CreateService $createService, private readonly MigrationService $migrationService, private readonly Migrator $migrator, - ?DatabaseSetRegistry $databases = null, + ?DatabaseRegistry $databases = null, ) { parent::__construct($databases); } @@ -106,15 +106,12 @@ protected function createDefaultContext(): CreateContext return new CreateContext($this->migrator, $this->migrationService, $this->createService); } - protected function createContext( - DatabaseSet $database, - Migrator $migrator, - MigrationService $migrationService, - ): CreateContext { + protected function createContext(DatabaseSetup $setup): CreateContext + { return new CreateContext( - $migrator, - $migrationService, - new CreateService($database->db, $database->useTablePrefix), + $setup->migrator, + $setup->migrationService, + new CreateService($setup->config->db, $setup->config->useTablePrefix), ); } diff --git a/src/Command/DatabaseCommand.php b/src/Command/DatabaseCommand.php index e3eda3e7..27bb9fb0 100644 --- a/src/Command/DatabaseCommand.php +++ b/src/Command/DatabaseCommand.php @@ -10,10 +10,8 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use Yiisoft\Db\Migration\DatabaseSet; -use Yiisoft\Db\Migration\DatabaseSetRegistry; -use Yiisoft\Db\Migration\Migrator; -use Yiisoft\Db\Migration\Service\MigrationService; +use Yiisoft\Db\Migration\DatabaseRegistry; +use Yiisoft\Db\Migration\DatabaseSetup; use function count; use function implode; @@ -27,7 +25,7 @@ */ abstract class DatabaseCommand extends Command { - public function __construct(private readonly ?DatabaseSetRegistry $databases = null) + public function __construct(private readonly ?DatabaseRegistry $databases = null) { parent::__construct(); $this->addOption('db', null, InputOption::VALUE_REQUIRED, 'Database configuration to use for migrations.'); @@ -35,7 +33,7 @@ public function __construct(private readonly ?DatabaseSetRegistry $databases = n final protected function execute(InputInterface $input, OutputInterface $output): int { - $availableNames = $this->databases?->getNames() ?? [DatabaseSetRegistry::DEFAULT_DATABASE]; + $availableNames = [DatabaseRegistry::DEFAULT_DATABASE, ...($this->databases?->getNames() ?? [])]; $multipleDatabases = count($availableNames) > 1; $io = new SymfonyStyle($input, $output); @@ -48,11 +46,10 @@ final protected function execute(InputInterface $input, OutputInterface $output) $hasResults = false; foreach ($names as $name) { - if ($this->databases === null || $name === DatabaseSetRegistry::DEFAULT_DATABASE) { + if ($this->databases === null || $name === DatabaseRegistry::DEFAULT_DATABASE) { $context = $this->createDefaultContext(); } else { - [$database, $migrator, $migrationService] = $this->databases->createServices($name); - $context = $this->createContext($database, $migrator, $migrationService); + $context = $this->createContext($this->databases->createSetup($name)); } if ($multipleDatabases) { @@ -89,11 +86,7 @@ abstract protected function createDefaultContext(): object; * * @psalm-return TContext */ - abstract protected function createContext( - DatabaseSet $database, - Migrator $migrator, - MigrationService $migrationService, - ): object; + abstract protected function createContext(DatabaseSetup $setup): object; /** * Executes the command for a single database. @@ -141,7 +134,7 @@ private function resolveDatabaseNames( } return $policy === MissingDatabaseOptionPolicy::ProcessDefaultDatabase - ? [DatabaseSetRegistry::DEFAULT_DATABASE] + ? [DatabaseRegistry::DEFAULT_DATABASE] : $availableNames; } diff --git a/src/Command/DownCommand.php b/src/Command/DownCommand.php index 2df54f80..9e34baad 100644 --- a/src/Command/DownCommand.php +++ b/src/Command/DownCommand.php @@ -12,8 +12,8 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Throwable; use Yiisoft\Db\Migration\Command\Context\DownContext; -use Yiisoft\Db\Migration\DatabaseSet; -use Yiisoft\Db\Migration\DatabaseSetRegistry; +use Yiisoft\Db\Migration\DatabaseRegistry; +use Yiisoft\Db\Migration\DatabaseSetup; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Runner\DownRunner; use Yiisoft\Db\Migration\Service\MigrationService; @@ -48,7 +48,7 @@ public function __construct( private readonly DownRunner $downRunner, private readonly MigrationService $migrationService, private readonly Migrator $migrator, - ?DatabaseSetRegistry $databases = null, + ?DatabaseRegistry $databases = null, ) { parent::__construct($databases); } @@ -73,12 +73,9 @@ protected function createDefaultContext(): DownContext return new DownContext($this->migrator, $this->migrationService, $this->downRunner); } - protected function createContext( - DatabaseSet $database, - Migrator $migrator, - MigrationService $migrationService, - ): DownContext { - return new DownContext($migrator, $migrationService, new DownRunner($migrator)); + protected function createContext(DatabaseSetup $setup): DownContext + { + return new DownContext($setup->migrator, $setup->migrationService, new DownRunner($setup->migrator)); } protected function executeForDatabase( diff --git a/src/Command/HistoryCommand.php b/src/Command/HistoryCommand.php index 2b3338c5..2bc03edd 100644 --- a/src/Command/HistoryCommand.php +++ b/src/Command/HistoryCommand.php @@ -11,8 +11,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Yiisoft\Db\Migration\Command\Context\MigrationContext; -use Yiisoft\Db\Migration\DatabaseSet; -use Yiisoft\Db\Migration\DatabaseSetRegistry; +use Yiisoft\Db\Migration\DatabaseRegistry; +use Yiisoft\Db\Migration\DatabaseSetup; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Service\MigrationService; @@ -40,7 +40,7 @@ final class HistoryCommand extends DatabaseCommand public function __construct( private readonly MigrationService $migrationService, private readonly Migrator $migrator, - ?DatabaseSetRegistry $databases = null, + ?DatabaseRegistry $databases = null, ) { parent::__construct($databases); } @@ -62,12 +62,9 @@ protected function createDefaultContext(): MigrationContext return new MigrationContext($this->migrator, $this->migrationService); } - protected function createContext( - DatabaseSet $database, - Migrator $migrator, - MigrationService $migrationService, - ): MigrationContext { - return new MigrationContext($migrator, $migrationService); + protected function createContext(DatabaseSetup $setup): MigrationContext + { + return new MigrationContext($setup->migrator, $setup->migrationService); } protected function executeForDatabase( diff --git a/src/Command/MarkCommand.php b/src/Command/MarkCommand.php index 5662e91a..f00c7128 100644 --- a/src/Command/MarkCommand.php +++ b/src/Command/MarkCommand.php @@ -13,8 +13,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Yiisoft\Db\Migration\Command\Context\MigrationContext; -use Yiisoft\Db\Migration\DatabaseSet; -use Yiisoft\Db\Migration\DatabaseSetRegistry; +use Yiisoft\Db\Migration\DatabaseRegistry; +use Yiisoft\Db\Migration\DatabaseSetup; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Service\MigrationService; @@ -54,7 +54,7 @@ final class MarkCommand extends DatabaseCommand public function __construct( private readonly MigrationService $migrationService, private readonly Migrator $migrator, - ?DatabaseSetRegistry $databases = null, + ?DatabaseRegistry $databases = null, ) { parent::__construct($databases); } @@ -76,12 +76,9 @@ protected function createDefaultContext(): MigrationContext return new MigrationContext($this->migrator, $this->migrationService); } - protected function createContext( - DatabaseSet $database, - Migrator $migrator, - MigrationService $migrationService, - ): MigrationContext { - return new MigrationContext($migrator, $migrationService); + protected function createContext(DatabaseSetup $setup): MigrationContext + { + return new MigrationContext($setup->migrator, $setup->migrationService); } protected function executeForDatabase( diff --git a/src/Command/NewCommand.php b/src/Command/NewCommand.php index 7d6ca705..e2d72e9a 100644 --- a/src/Command/NewCommand.php +++ b/src/Command/NewCommand.php @@ -11,8 +11,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Yiisoft\Db\Migration\Command\Context\MigrationContext; -use Yiisoft\Db\Migration\DatabaseSet; -use Yiisoft\Db\Migration\DatabaseSetRegistry; +use Yiisoft\Db\Migration\DatabaseRegistry; +use Yiisoft\Db\Migration\DatabaseSetup; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Service\MigrationService; @@ -46,7 +46,7 @@ final class NewCommand extends DatabaseCommand public function __construct( private readonly MigrationService $migrationService, private readonly Migrator $migrator, - ?DatabaseSetRegistry $databases = null, + ?DatabaseRegistry $databases = null, ) { parent::__construct($databases); } @@ -72,12 +72,9 @@ protected function createDefaultContext(): MigrationContext return new MigrationContext($this->migrator, $this->migrationService); } - protected function createContext( - DatabaseSet $database, - Migrator $migrator, - MigrationService $migrationService, - ): MigrationContext { - return new MigrationContext($migrator, $migrationService); + protected function createContext(DatabaseSetup $setup): MigrationContext + { + return new MigrationContext($setup->migrator, $setup->migrationService); } protected function executeForDatabase( diff --git a/src/Command/RedoCommand.php b/src/Command/RedoCommand.php index 13cb4922..bdc5b024 100644 --- a/src/Command/RedoCommand.php +++ b/src/Command/RedoCommand.php @@ -12,8 +12,8 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Throwable; use Yiisoft\Db\Migration\Command\Context\RedoContext; -use Yiisoft\Db\Migration\DatabaseSet; -use Yiisoft\Db\Migration\DatabaseSetRegistry; +use Yiisoft\Db\Migration\DatabaseRegistry; +use Yiisoft\Db\Migration\DatabaseSetup; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Runner\DownRunner; use Yiisoft\Db\Migration\Runner\UpdateRunner; @@ -49,7 +49,7 @@ public function __construct( private readonly Migrator $migrator, private readonly DownRunner $downRunner, private readonly UpdateRunner $updateRunner, - ?DatabaseSetRegistry $databases = null, + ?DatabaseRegistry $databases = null, ) { parent::__construct($databases); } @@ -74,16 +74,13 @@ protected function createDefaultContext(): RedoContext return new RedoContext($this->migrator, $this->migrationService, $this->downRunner, $this->updateRunner); } - protected function createContext( - DatabaseSet $database, - Migrator $migrator, - MigrationService $migrationService, - ): RedoContext { + protected function createContext(DatabaseSetup $setup): RedoContext + { return new RedoContext( - $migrator, - $migrationService, - new DownRunner($migrator), - new UpdateRunner($migrator), + $setup->migrator, + $setup->migrationService, + new DownRunner($setup->migrator), + new UpdateRunner($setup->migrator), ); } diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php index dfe6a922..bdf24a88 100644 --- a/src/Command/UpdateCommand.php +++ b/src/Command/UpdateCommand.php @@ -12,8 +12,8 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Throwable; use Yiisoft\Db\Migration\Command\Context\UpdateContext; -use Yiisoft\Db\Migration\DatabaseSet; -use Yiisoft\Db\Migration\DatabaseSetRegistry; +use Yiisoft\Db\Migration\DatabaseRegistry; +use Yiisoft\Db\Migration\DatabaseSetup; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Runner\UpdateRunner; use Yiisoft\Db\Migration\Service\MigrationService; @@ -47,7 +47,7 @@ public function __construct( private readonly UpdateRunner $updateRunner, private readonly MigrationService $migrationService, private readonly Migrator $migrator, - ?DatabaseSetRegistry $databases = null, + ?DatabaseRegistry $databases = null, ) { parent::__construct($databases); } @@ -73,15 +73,12 @@ protected function createDefaultContext(): UpdateContext return new UpdateContext($this->migrator, $this->migrationService, $this->updateRunner); } - protected function createContext( - DatabaseSet $database, - Migrator $migrator, - MigrationService $migrationService, - ): UpdateContext { + protected function createContext(DatabaseSetup $setup): UpdateContext + { return new UpdateContext( - $migrator, - $migrationService, - new UpdateRunner($migrator), + $setup->migrator, + $setup->migrationService, + new UpdateRunner($setup->migrator), ); } diff --git a/src/DatabaseSet.php b/src/DatabaseConfig.php similarity index 88% rename from src/DatabaseSet.php rename to src/DatabaseConfig.php index 7625032f..28138055 100644 --- a/src/DatabaseSet.php +++ b/src/DatabaseConfig.php @@ -7,9 +7,9 @@ use Yiisoft\Db\Connection\ConnectionInterface; /** - * Configuration of a named database and its migrations. + * Configuration of an additional named database and its migrations. */ -final class DatabaseSet +final class DatabaseConfig { /** * @param string[] $sourceNamespaces diff --git a/src/DatabaseSetRegistry.php b/src/DatabaseRegistry.php similarity index 65% rename from src/DatabaseSetRegistry.php rename to src/DatabaseRegistry.php index 74502709..a24dae0d 100644 --- a/src/DatabaseSetRegistry.php +++ b/src/DatabaseRegistry.php @@ -14,21 +14,21 @@ use function trim; /** - * Additional named database sets, in execution order. The "default" database is always configured by the existing + * Additional named databases, in execution order. The "default" database is always configured by the existing * command dependencies, so it can't be defined here. */ -final class DatabaseSetRegistry +final class DatabaseRegistry { /** - * Name of the default database configured by the existing command dependencies. + * Reserved name of the default database configured by the existing command dependencies. */ public const DEFAULT_DATABASE = 'default'; - /** @var array */ + /** @var array */ private readonly array $databases; /** - * @param array $databases Additional named sets, excluding "default". + * @param array $databases Additional named databases, excluding "default". */ public function __construct( private readonly Injector $injector, @@ -52,23 +52,27 @@ public function __construct( } /** - * @return list + * Returns names of additional databases, excluding "default". + * + * @return string[] + * + * @psalm-return list */ public function getNames(): array { - return array_keys([self::DEFAULT_DATABASE => null, ...$this->databases]); + return array_keys($this->databases); } /** - * Creates services for a named database. - * - * @internal + * Creates a setup of an additional database: its configuration and migration services. Each call creates new + * service instances. * - * @psalm-return array{DatabaseSet, Migrator, MigrationService} + * @throws InvalidArgumentException If the additional database isn't configured. */ - public function createServices(string $name): array + public function createSetup(string $name): DatabaseSetup { - $database = $this->databases[$name] ?? throw new InvalidArgumentException("Unknown database set: $name."); + $database = $this->databases[$name] + ?? throw new InvalidArgumentException("Unknown additional database: $name."); $migrator = new Migrator( $database->db, $this->informer, @@ -82,6 +86,6 @@ public function createServices(string $name): array $service->setSourceNamespaces($database->sourceNamespaces); $service->setSourcePaths($database->sourcePaths); - return [$database, $migrator, $service]; + return new DatabaseSetup($database, $migrator, $service); } } diff --git a/src/DatabaseSetup.php b/src/DatabaseSetup.php new file mode 100644 index 00000000..66aef3e7 --- /dev/null +++ b/src/DatabaseSetup.php @@ -0,0 +1,19 @@ + */ private array $connections; - /** @var array */ + /** @var array */ private array $sets; protected function setUp(): void @@ -52,7 +52,7 @@ protected function setUp(): void foreach (['default', 'maps', 'analytics'] as $name) { FileHelper::ensureDirectory($this->directory . '/' . $name); $this->connections[$name] = new Connection(new Driver('sqlite::memory:'), new SchemaCache(new MemorySimpleCache())); - $this->sets[$name] = new DatabaseSet( + $this->sets[$name] = new DatabaseConfig( $this->connections[$name], newMigrationPath: $this->directory . '/' . $name, ); @@ -105,7 +105,7 @@ public function testSelectDatabaseForUpDownAndRedo(): void public function testMarkRequiresDatabaseAndChangesOnlyItsHistory(): void { $class = $this->writeMigration('maps'); - $this->sets['maps'] = new DatabaseSet( + $this->sets['maps'] = new DatabaseConfig( $this->connections['maps'], newMigrationPath: $this->directory . '/maps', historyTable: 'maps_history', @@ -126,7 +126,7 @@ public function testMarkRequiresDatabaseAndChangesOnlyItsHistory(): void public function testMarkRejectsLongNamesBeforeAddingAnyHistory(): void { $class = $this->writeMigration('maps'); - $this->sets['maps'] = new DatabaseSet( + $this->sets['maps'] = new DatabaseConfig( $this->connections['maps'], newMigrationPath: $this->directory . '/maps', migrationNameLimit: 10, @@ -294,7 +294,7 @@ public function testStopsOnExceptionAndRollsBackOnlyTheFailingDatabase(): void public function testStopsOnNonzeroExitCode(): void { $this->writeMigration('default'); - $this->sets['maps'] = new DatabaseSet($this->connections['maps']); + $this->sets['maps'] = new DatabaseConfig($this->connections['maps']); $this->writeMigration('analytics'); self::assertSame(Command::INVALID, $this->command('up')->execute(['--force-yes' => true])); self::assertCount(1, $this->migrator('default')->getHistory()); @@ -369,7 +369,7 @@ public function testListingStopsOnErrorEvenAfterFindingResults(string $name): vo if ($name === 'history') { $this->command('up')->execute(['--db' => 'default', '--force-yes' => true]); } - $this->sets['maps'] = new DatabaseSet( + $this->sets['maps'] = new DatabaseConfig( new Connection( new Driver('sqlite:' . $this->directory . '/missing/maps.sqlite'), new SchemaCache(new MemorySimpleCache()), @@ -411,7 +411,7 @@ public function testLimitAppliesToEachDatabase(): void public function testSetsCanShareAConnectionWithSeparateHistoryTables(): void { $this->connections['maps'] = $this->connections['default']; - $this->sets['maps'] = new DatabaseSet( + $this->sets['maps'] = new DatabaseConfig( $this->connections['maps'], newMigrationPath: $this->directory . '/maps', historyTable: 'maps_history', @@ -427,7 +427,7 @@ public function testNamedSetUsesItsConnectionForGeneration(): void { $this->connections['default']->createCommand('CREATE TABLE parent (default_key INTEGER PRIMARY KEY)')->execute(); $this->connections['maps']->createCommand('CREATE TABLE parent (maps_key INTEGER PRIMARY KEY)')->execute(); - $this->sets['maps'] = new DatabaseSet( + $this->sets['maps'] = new DatabaseConfig( $this->connections['maps'], newMigrationPath: $this->directory . '/maps', useTablePrefix: false, @@ -448,7 +448,7 @@ public function testNamedSetUsesItsConnectionForGeneration(): void public function testCreateUsesNamedSetMigrationNameLimit(): void { - $this->sets['maps'] = new DatabaseSet( + $this->sets['maps'] = new DatabaseConfig( $this->connections['maps'], newMigrationPath: $this->directory . '/maps', migrationNameLimit: 5, @@ -485,7 +485,7 @@ public function testNamedSetDefaultGenerationSettings(): void public function testNamedSetDiscoversMigrationsInItsNewMigrationNamespace(): void { - $this->sets['maps'] = new DatabaseSet( + $this->sets['maps'] = new DatabaseConfig( $this->connections['maps'], newMigrationNamespace: 'Yiisoft\Db\Migration\Tests\Support\MigrationsExtra', ); @@ -497,7 +497,7 @@ public function testNamedSetDiscoversMigrationsInItsNewMigrationNamespace(): voi public function testNamedSetRespectsItsMigrationNameLimit(): void { $this->writeMigration('maps'); - $this->sets['maps'] = new DatabaseSet( + $this->sets['maps'] = new DatabaseConfig( $this->connections['maps'], newMigrationPath: $this->directory . '/maps', migrationNameLimit: 5, @@ -511,7 +511,7 @@ public function testNamedSetsRetainMultipleMigrationSources(): void { $class = $this->writeMigration('maps'); $extraClass = $this->writeMigration('analytics', 'extra_example'); - $this->sets['maps'] = new DatabaseSet( + $this->sets['maps'] = new DatabaseConfig( $this->connections['maps'], newMigrationPath: $this->directory . '/maps', sourceNamespaces: ['Yiisoft\\Db\\Migration\\Tests\\Support\\MigrationsExtra'], @@ -574,7 +574,7 @@ private function command(string $name): CommandTester { $sets = $this->sets; unset($sets['default']); - $registry = new DatabaseSetRegistry(new Injector(), new NullMigrationInformer(), $sets); + $registry = new DatabaseRegistry(new Injector(), new NullMigrationInformer(), $sets); $migrator = $this->migrator('default'); $service = new MigrationService($this->connections['default'], new Injector(), $migrator); $service->setNewMigrationPath($this->sets['default']->newMigrationPath); diff --git a/tests/Migration/BinTest.php b/tests/Migration/BinTest.php index 96e483a4..0e1be578 100644 --- a/tests/Migration/BinTest.php +++ b/tests/Migration/BinTest.php @@ -84,7 +84,7 @@ public function testNamedDatabase(): void PHP); $this->replaceParams("'databases' => [],", <<<'PHP' 'databases' => [ - 'maps' => new \Yiisoft\Db\Migration\DatabaseSet( + 'maps' => new \Yiisoft\Db\Migration\DatabaseConfig( new \Yiisoft\Db\Sqlite\Connection( new \Yiisoft\Db\Sqlite\Driver('sqlite:' . __DIR__ . '/maps.sqlite'), new \Yiisoft\Db\Cache\SchemaCache(new \Yiisoft\Test\Support\SimpleCache\MemorySimpleCache()) @@ -130,7 +130,7 @@ public function testRejectsDefaultDatabase(): void $this->replaceParams("'db' => null,", "'db' => \$db,"); $this->replaceParams( "'databases' => [],", - "'databases' => ['default' => new \\Yiisoft\\Db\\Migration\\DatabaseSet(\$db)],", + "'databases' => ['default' => new \\Yiisoft\\Db\\Migration\\DatabaseConfig(\$db)],", ); [$output, $exitCode] = $this->runYiiDbMigration(); diff --git a/tests/Migration/ConfigTest.php b/tests/Migration/ConfigTest.php index 99a00b0a..5310da15 100644 --- a/tests/Migration/ConfigTest.php +++ b/tests/Migration/ConfigTest.php @@ -12,7 +12,7 @@ use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; use Yiisoft\Test\Support\SimpleCache\MemorySimpleCache; -use Yiisoft\Db\Migration\DatabaseSetRegistry; +use Yiisoft\Db\Migration\DatabaseRegistry; use Symfony\Component\Console\Tester\CommandTester; use Yiisoft\Db\Migration\Command\CreateCommand; use Yiisoft\Db\Migration\Command\DownCommand; @@ -85,7 +85,7 @@ public function testAdditionalDatabaseConfiguration(): void ], ], ); - $this->assertSame(['default', 'maps'], $container->get(DatabaseSetRegistry::class)->getNames()); + $this->assertSame(['maps'], $container->get(DatabaseRegistry::class)->getNames()); $command = new CommandTester($container->get(DownCommand::class)); $this->assertSame(2, $command->execute([])); diff --git a/tests/Migration/DatabaseRegistryTest.php b/tests/Migration/DatabaseRegistryTest.php new file mode 100644 index 00000000..31157460 --- /dev/null +++ b/tests/Migration/DatabaseRegistryTest.php @@ -0,0 +1,87 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Database names must be non-empty strings.'); + new DatabaseRegistry(new Injector(), new NullMigrationInformer(), [ + $name => new DatabaseConfig($this->createMock(ConnectionInterface::class)), + ]); + } + + public function testRejectsDefaultName(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'The "default" database is configured by the existing migration settings and can\'t be redefined.', + ); + new DatabaseRegistry(new Injector(), new NullMigrationInformer(), [ + 'default' => new DatabaseConfig($this->createMock(ConnectionInterface::class)), + ]); + } + + public function testCannotCreateSetupForDefaultDatabase(): void + { + $registry = new DatabaseRegistry(new Injector(), new NullMigrationInformer()); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unknown additional database: default.'); + $registry->createSetup('default'); + } + + public function testCannotCreateSetupForUnknownDatabase(): void + { + $registry = new DatabaseRegistry(new Injector(), new NullMigrationInformer()); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unknown additional database: missing.'); + $registry->createSetup('missing'); + } + + public function testNamesInConfigurationOrder(): void + { + $config = new DatabaseConfig($this->createMock(ConnectionInterface::class)); + $registry = new DatabaseRegistry(new Injector(), new NullMigrationInformer(), [ + 'maps' => $config, + 'analytics' => $config, + ]); + self::assertSame(['maps', 'analytics'], $registry->getNames()); + } + + public function testCreateSetup(): void + { + $config = new DatabaseConfig( + $this->createMock(ConnectionInterface::class), + historyTable: '{{%maps_migration}}', + migrationNameLimit: 100, + ); + $registry = new DatabaseRegistry(new Injector(), new NullMigrationInformer(), ['maps' => $config]); + + $setup = $registry->createSetup('maps'); + + self::assertSame($config, $setup->config); + self::assertSame(100, $setup->migrator->getMigrationNameLimit()); + self::assertNotSame($setup->migrator, $registry->createSetup('maps')->migrator); + } +} diff --git a/tests/Migration/DatabaseSetRegistryTest.php b/tests/Migration/DatabaseSetRegistryTest.php deleted file mode 100644 index a5e1f080..00000000 --- a/tests/Migration/DatabaseSetRegistryTest.php +++ /dev/null @@ -1,71 +0,0 @@ -expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Database names must be non-empty strings.'); - new DatabaseSetRegistry(new Injector(), new NullMigrationInformer(), [ - $name => new DatabaseSet($this->createMock(ConnectionInterface::class)), - ]); - } - - public function testRejectsDefaultName(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage( - 'The "default" database is configured by the existing migration settings and can\'t be redefined.', - ); - new DatabaseSetRegistry(new Injector(), new NullMigrationInformer(), [ - 'default' => new DatabaseSet($this->createMock(ConnectionInterface::class)), - ]); - } - - public function testCannotCreateServicesForDefaultDatabase(): void - { - $registry = new DatabaseSetRegistry(new Injector(), new NullMigrationInformer()); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Unknown database set: default.'); - $registry->createServices('default'); - } - - public function testCannotCreateServicesForUnknownDatabase(): void - { - $registry = new DatabaseSetRegistry(new Injector(), new NullMigrationInformer()); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Unknown database set: missing.'); - $registry->createServices('missing'); - } - - public function testDefaultIsFirstFollowedByConfigurationOrder(): void - { - $set = new DatabaseSet($this->createMock(ConnectionInterface::class)); - $registry = new DatabaseSetRegistry(new Injector(), new NullMigrationInformer(), [ - 'maps' => $set, - 'analytics' => $set, - ]); - self::assertSame(['default', 'maps', 'analytics'], $registry->getNames()); - } -} From 75d365a95d9a0a303c39fbae41448edff6ed98f2 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 15 Oct 2026 13:22:48 +0300 Subject: [PATCH 04/10] Refactor --- src/DatabaseConfig.php | 7 ++++--- src/Migrator.php | 14 ++++++++++++-- src/Service/Generate/CreateService.php | 7 ++++++- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/DatabaseConfig.php b/src/DatabaseConfig.php index 28138055..1de32eb7 100644 --- a/src/DatabaseConfig.php +++ b/src/DatabaseConfig.php @@ -5,6 +5,7 @@ namespace Yiisoft\Db\Migration; use Yiisoft\Db\Connection\ConnectionInterface; +use Yiisoft\Db\Migration\Service\Generate\CreateService; /** * Configuration of an additional named database and its migrations. @@ -21,9 +22,9 @@ public function __construct( public readonly string $newMigrationPath = '', public readonly array $sourceNamespaces = [], public readonly array $sourcePaths = [], - public readonly string $historyTable = '{{%migration}}', - public readonly ?int $migrationNameLimit = 180, - public readonly bool $useTablePrefix = true, + public readonly string $historyTable = Migrator::DEFAULT_HISTORY_TABLE, + public readonly ?int $migrationNameLimit = Migrator::DEFAULT_MIGRATION_NAME_LIMIT, + public readonly bool $useTablePrefix = CreateService::DEFAULT_USE_TABLE_PREFIX, public readonly ?int $maxSqlOutputLength = null, ) {} } diff --git a/src/Migrator.php b/src/Migrator.php index 03ea5522..50b5d0c9 100644 --- a/src/Migrator.php +++ b/src/Migrator.php @@ -15,13 +15,23 @@ final class Migrator { + /** + * Default name of the migration history table. + */ + public const DEFAULT_HISTORY_TABLE = '{{%migration}}'; + + /** + * Default maximum length of a migration name. + */ + public const DEFAULT_MIGRATION_NAME_LIMIT = 180; + private bool $checkMigrationHistoryTable = true; public function __construct( private readonly ConnectionInterface $db, private readonly MigrationInformerInterface $informer, - private readonly string $historyTable = '{{%migration}}', - private ?int $migrationNameLimit = 180, + private readonly string $historyTable = self::DEFAULT_HISTORY_TABLE, + private ?int $migrationNameLimit = self::DEFAULT_MIGRATION_NAME_LIMIT, private readonly ?int $maxSqlOutputLength = null, ) {} diff --git a/src/Service/Generate/CreateService.php b/src/Service/Generate/CreateService.php index 30181d6d..0a7015b5 100644 --- a/src/Service/Generate/CreateService.php +++ b/src/Service/Generate/CreateService.php @@ -13,6 +13,11 @@ final class CreateService { + /** + * Default value of whether the generated table names should consider the `tablePrefix` setting. + */ + public const DEFAULT_USE_TABLE_PREFIX = true; + private PhpRenderer $phpRenderer; private ?SymfonyStyle $io = null; @@ -27,7 +32,7 @@ final class CreateService */ public function __construct( private readonly ConnectionInterface $db, - private readonly bool $useTablePrefix = true, + private readonly bool $useTablePrefix = self::DEFAULT_USE_TABLE_PREFIX, ) { $this->phpRenderer = new PhpRenderer(); } From ddeac165058ad4e70aa5e5ff17f59be1554bdf71 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 15 Oct 2026 13:26:56 +0300 Subject: [PATCH 05/10] Refactor --- src/DatabaseRegistry.php | 63 +++++++++++++++--------- tests/Migration/DatabaseRegistryTest.php | 23 +++++++-- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/src/DatabaseRegistry.php b/src/DatabaseRegistry.php index a24dae0d..1cca3474 100644 --- a/src/DatabaseRegistry.php +++ b/src/DatabaseRegistry.php @@ -10,8 +10,9 @@ use Yiisoft\Injector\Injector; use function array_keys; -use function is_string; -use function trim; +use function get_debug_type; +use function is_int; +use function sprintf; /** * Additional named databases, in execution order. The "default" database is always configured by the existing @@ -25,7 +26,7 @@ final class DatabaseRegistry public const DEFAULT_DATABASE = 'default'; /** @var array */ - private readonly array $databases; + private readonly array $configs; /** * @param array $databases Additional named databases, excluding "default". @@ -35,20 +36,34 @@ public function __construct( private readonly MigrationInformerInterface $informer, array $databases = [], ) { - $sets = []; - foreach ($databases as $name => $database) { - if (!is_string($name) || trim($name) === '') { - throw new InvalidArgumentException('Database names must be non-empty strings.'); + $configs = []; + foreach ($databases as $name => $config) { + if (is_int($name)) { + throw new InvalidArgumentException( + sprintf('Database names must be non-numeric strings, "%s" given.', $name), + ); } if ($name === self::DEFAULT_DATABASE) { throw new InvalidArgumentException( - 'The "' . self::DEFAULT_DATABASE - . '" database is configured by the existing migration settings and can\'t be redefined.', + sprintf( + 'The "%s" database is configured by the existing migration settings and can\'t be redefined.', + self::DEFAULT_DATABASE, + ), + ); + } + if (!$config instanceof DatabaseConfig) { + throw new InvalidArgumentException( + sprintf( + 'Configuration of the "%s" database must be an instance of "%s", "%s" given.', + $name, + DatabaseConfig::class, + get_debug_type($config), + ), ); } - $sets[$name] = $database; + $configs[$name] = $config; } - $this->databases = $sets; + $this->configs = $configs; } /** @@ -60,7 +75,7 @@ public function __construct( */ public function getNames(): array { - return array_keys($this->databases); + return array_keys($this->configs); } /** @@ -71,21 +86,21 @@ public function getNames(): array */ public function createSetup(string $name): DatabaseSetup { - $database = $this->databases[$name] - ?? throw new InvalidArgumentException("Unknown additional database: $name."); + $config = $this->configs[$name] + ?? throw new InvalidArgumentException(sprintf('Unknown additional database: %s.', $name)); $migrator = new Migrator( - $database->db, + $config->db, $this->informer, - $database->historyTable, - $database->migrationNameLimit, - $database->maxSqlOutputLength, + $config->historyTable, + $config->migrationNameLimit, + $config->maxSqlOutputLength, ); - $service = new MigrationService($database->db, $this->injector, $migrator); - $service->setNewMigrationNamespace($database->newMigrationNamespace); - $service->setNewMigrationPath($database->newMigrationPath); - $service->setSourceNamespaces($database->sourceNamespaces); - $service->setSourcePaths($database->sourcePaths); + $service = new MigrationService($config->db, $this->injector, $migrator); + $service->setNewMigrationNamespace($config->newMigrationNamespace); + $service->setNewMigrationPath($config->newMigrationPath); + $service->setSourceNamespaces($config->sourceNamespaces); + $service->setSourcePaths($config->sourcePaths); - return new DatabaseSetup($database, $migrator, $service); + return new DatabaseSetup($config, $migrator, $service); } } diff --git a/tests/Migration/DatabaseRegistryTest.php b/tests/Migration/DatabaseRegistryTest.php index 31157460..95ab1bb7 100644 --- a/tests/Migration/DatabaseRegistryTest.php +++ b/tests/Migration/DatabaseRegistryTest.php @@ -15,21 +15,34 @@ final class DatabaseRegistryTest extends TestCase { - public static function invalidNames(): array + public static function numericNames(): array { - return [[''], [' '], [0]]; + return [ + 'int' => [0, '0'], + 'numeric string' => ['1', '1'], + ]; } - #[DataProvider('invalidNames')] - public function testRejectsInvalidNames(string|int $name): void + #[DataProvider('numericNames')] + public function testRejectsNumericNames(string|int $name, string $expectedName): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Database names must be non-empty strings.'); + $this->expectExceptionMessage('Database names must be non-numeric strings, "' . $expectedName . '" given.'); new DatabaseRegistry(new Injector(), new NullMigrationInformer(), [ $name => new DatabaseConfig($this->createMock(ConnectionInterface::class)), ]); } + public function testRejectsInvalidConfiguration(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Configuration of the "maps" database must be an instance of "' . DatabaseConfig::class + . '", "array" given.', + ); + new DatabaseRegistry(new Injector(), new NullMigrationInformer(), ['maps' => []]); + } + public function testRejectsDefaultName(): void { $this->expectException(InvalidArgumentException::class); From 807676469bd74dcbfea45f0ae40d6d3d88538b52 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 15 Oct 2026 13:29:58 +0300 Subject: [PATCH 06/10] Refactor --- docs/guide/en/usage-with-symfony.md | 1 + src/DatabaseRegistry.php | 2 ++ src/DatabaseSetup.php | 2 ++ 3 files changed, 5 insertions(+) diff --git a/docs/guide/en/usage-with-symfony.md b/docs/guide/en/usage-with-symfony.md index f977bbf2..57c5a456 100644 --- a/docs/guide/en/usage-with-symfony.md +++ b/docs/guide/en/usage-with-symfony.md @@ -20,6 +20,7 @@ services: exclude: - '../vendor/yiisoft/db-migration/src/DatabaseRegistry.php' - '../vendor/yiisoft/db-migration/src/DatabaseConfig.php' + - '../vendor/yiisoft/db-migration/src/DatabaseSetup.php' - '../vendor/yiisoft/db-migration/src/Command/Context/' Yiisoft\Db\Migration\Informer\MigrationInformerInterface: diff --git a/src/DatabaseRegistry.php b/src/DatabaseRegistry.php index 1cca3474..feea4350 100644 --- a/src/DatabaseRegistry.php +++ b/src/DatabaseRegistry.php @@ -83,6 +83,8 @@ public function getNames(): array * service instances. * * @throws InvalidArgumentException If the additional database isn't configured. + * + * @internal */ public function createSetup(string $name): DatabaseSetup { diff --git a/src/DatabaseSetup.php b/src/DatabaseSetup.php index 66aef3e7..428b5b99 100644 --- a/src/DatabaseSetup.php +++ b/src/DatabaseSetup.php @@ -8,6 +8,8 @@ /** * Configuration and migration services of an additional named database. + * + * @internal */ final class DatabaseSetup { From f996b62876db33bf67592308e45c91d63e4c74f1 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 15 Oct 2026 13:32:05 +0300 Subject: [PATCH 07/10] Refactor --- src/Command/DatabaseCommand.php | 20 ++++++++++---------- src/Command/MissingDatabaseOptionPolicy.php | 6 ------ src/Command/NewCommand.php | 7 ++++++- src/Command/UpdateCommand.php | 7 ++++++- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/Command/DatabaseCommand.php b/src/Command/DatabaseCommand.php index 27bb9fb0..eed71df8 100644 --- a/src/Command/DatabaseCommand.php +++ b/src/Command/DatabaseCommand.php @@ -74,6 +74,14 @@ final protected function execute(InputInterface $input, OutputInterface $output) */ abstract protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy; + /** + * Returns the error message shown when the `--db` option is required, but isn't specified. + */ + protected function getMissingDatabaseOptionError(): string + { + return 'The --db option is required when multiple databases are configured.'; + } + /** * Creates the command context for the default database from the command dependencies. * @@ -121,16 +129,8 @@ private function resolveDatabaseNames( if ($name === null) { $policy = $this->getMissingDatabaseOptionPolicy($input); - if ($multipleDatabases) { - $error = match ($policy) { - MissingDatabaseOptionPolicy::RejectIfMultiple => 'The --db option is required when multiple databases are configured.', - MissingDatabaseOptionPolicy::RejectIfMultipleWithFilter => 'The --db option is required with --path or --namespace when multiple databases are configured.', - MissingDatabaseOptionPolicy::ProcessDefaultDatabase, - MissingDatabaseOptionPolicy::ProcessAllDatabases => null, - }; - if ($error !== null) { - return $error; - } + if ($multipleDatabases && $policy === MissingDatabaseOptionPolicy::RejectIfMultiple) { + return $this->getMissingDatabaseOptionError(); } return $policy === MissingDatabaseOptionPolicy::ProcessDefaultDatabase diff --git a/src/Command/MissingDatabaseOptionPolicy.php b/src/Command/MissingDatabaseOptionPolicy.php index ce74e39d..b8066269 100644 --- a/src/Command/MissingDatabaseOptionPolicy.php +++ b/src/Command/MissingDatabaseOptionPolicy.php @@ -25,10 +25,4 @@ enum MissingDatabaseOptionPolicy * Reject the command when multiple databases are configured. With a single database, it is processed. */ case RejectIfMultiple; - - /** - * Reject the command when multiple databases are configured and `--path` or `--namespace` is used. - * Otherwise, all configured databases are processed. - */ - case RejectIfMultipleWithFilter; } diff --git a/src/Command/NewCommand.php b/src/Command/NewCommand.php index e2d72e9a..12c50e6f 100644 --- a/src/Command/NewCommand.php +++ b/src/Command/NewCommand.php @@ -63,10 +63,15 @@ protected function configure(): void protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy { return $input->getOption('path') !== [] || $input->getOption('namespace') !== [] - ? MissingDatabaseOptionPolicy::RejectIfMultipleWithFilter + ? MissingDatabaseOptionPolicy::RejectIfMultiple : MissingDatabaseOptionPolicy::ProcessAllDatabases; } + protected function getMissingDatabaseOptionError(): string + { + return 'The --db option is required with --path or --namespace when multiple databases are configured.'; + } + protected function createDefaultContext(): MigrationContext { return new MigrationContext($this->migrator, $this->migrationService); diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php index bdf24a88..72f26512 100644 --- a/src/Command/UpdateCommand.php +++ b/src/Command/UpdateCommand.php @@ -64,10 +64,15 @@ protected function configure(): void protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy { return $input->getOption('path') !== [] || $input->getOption('namespace') !== [] - ? MissingDatabaseOptionPolicy::RejectIfMultipleWithFilter + ? MissingDatabaseOptionPolicy::RejectIfMultiple : MissingDatabaseOptionPolicy::ProcessAllDatabases; } + protected function getMissingDatabaseOptionError(): string + { + return 'The --db option is required with --path or --namespace when multiple databases are configured.'; + } + protected function createDefaultContext(): UpdateContext { return new UpdateContext($this->migrator, $this->migrationService, $this->updateRunner); From d3c96ff57f4c69149d424d69bc859536c3d060c2 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 15 Oct 2026 14:02:50 +0300 Subject: [PATCH 08/10] fix --- src/DatabaseRegistry.php | 4 ++++ tests/Migration/DatabaseRegistryTest.php | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/DatabaseRegistry.php b/src/DatabaseRegistry.php index feea4350..ecebba73 100644 --- a/src/DatabaseRegistry.php +++ b/src/DatabaseRegistry.php @@ -13,6 +13,7 @@ use function get_debug_type; use function is_int; use function sprintf; +use function trim; /** * Additional named databases, in execution order. The "default" database is always configured by the existing @@ -43,6 +44,9 @@ public function __construct( sprintf('Database names must be non-numeric strings, "%s" given.', $name), ); } + if (trim($name) === '') { + throw new InvalidArgumentException('Database names must be non-empty strings.'); + } if ($name === self::DEFAULT_DATABASE) { throw new InvalidArgumentException( sprintf( diff --git a/tests/Migration/DatabaseRegistryTest.php b/tests/Migration/DatabaseRegistryTest.php index 95ab1bb7..456658e8 100644 --- a/tests/Migration/DatabaseRegistryTest.php +++ b/tests/Migration/DatabaseRegistryTest.php @@ -33,6 +33,24 @@ public function testRejectsNumericNames(string|int $name, string $expectedName): ]); } + public static function blankNames(): array + { + return [ + 'empty' => [''], + 'whitespace' => [' '], + ]; + } + + #[DataProvider('blankNames')] + public function testRejectsBlankNames(string $name): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Database names must be non-empty strings.'); + new DatabaseRegistry(new Injector(), new NullMigrationInformer(), [ + $name => new DatabaseConfig($this->createMock(ConnectionInterface::class)), + ]); + } + public function testRejectsInvalidConfiguration(): void { $this->expectException(InvalidArgumentException::class); From 38e96e21a24d15e7a26842edbd51f3ff1ac3997c Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 15 Oct 2026 19:09:11 +0300 Subject: [PATCH 09/10] improve --- README.md | 2 +- docs/guide/en/multiple-databases.md | 8 ++++---- docs/guide/en/usage-with-symfony.md | 2 +- docs/guide/en/usage-with-yii-console.md | 4 ++-- src/Command/CreateCommand.php | 4 ++-- src/Command/DatabaseCommand.php | 8 ++++---- src/Command/DownCommand.php | 10 +++++----- src/Command/HistoryCommand.php | 4 ++-- src/Command/MarkCommand.php | 4 ++-- src/Command/NewCommand.php | 4 ++-- src/Command/RedoCommand.php | 10 +++++----- src/Command/UpdateCommand.php | 4 ++-- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index aa1dfc45..134eb84b 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ migrate:up Applies new migrations. The create command allows defining fields for the table being created. -Use `--db=name` to select a [named database migration set](docs/guide/en/multiple-databases.md). +Use `--db=name` to select one of [multiple databases](docs/guide/en/multiple-databases.md). ## Documentation diff --git a/docs/guide/en/multiple-databases.md b/docs/guide/en/multiple-databases.md index 61b6d873..1a1b9710 100644 --- a/docs/guide/en/multiple-databases.md +++ b/docs/guide/en/multiple-databases.md @@ -1,9 +1,9 @@ # Multiple databases -If your application uses separate databases such as `maps` and `analytics`, you can organize their migrations -into isolated sets. Each set has its own migration directory and a separate migration history table in its database, -so migrations applied to one set do not affect the recorded status of migrations in another. Use `--db` to work with -one set, or apply migrations to all sets with a single command. +If your application uses separate databases such as `maps` and `analytics`, you can keep their migrations +isolated. Each database has its own migration directory and a separate migration history table, so migrations applied +to one database do not affect the recorded status of migrations in another. Use `--db` to work with one database, +or apply migrations to all databases with a single command. ## Configure your databases diff --git a/docs/guide/en/usage-with-symfony.md b/docs/guide/en/usage-with-symfony.md index 57c5a456..8392deed 100644 --- a/docs/guide/en/usage-with-symfony.md +++ b/docs/guide/en/usage-with-symfony.md @@ -96,7 +96,7 @@ services: The name `default` is reserved and can't be used in the registry. -Select one set with `--db`, or apply all sets with `migrate:up`: +Select one database with `--db`, or apply migrations to all databases with `migrate:up`: ```shell php bin/console migrate:create create_places --db=maps diff --git a/docs/guide/en/usage-with-yii-console.md b/docs/guide/en/usage-with-yii-console.md index f67dd96d..6bc1e71c 100644 --- a/docs/guide/en/usage-with-yii-console.md +++ b/docs/guide/en/usage-with-yii-console.md @@ -70,7 +70,7 @@ a DI container service ID: ], ``` -Each set is an array of `Yiisoft\Db\Migration\DatabaseConfig` constructor arguments, where `db` is the ID of +Each database configuration is an array of `Yiisoft\Db\Migration\DatabaseConfig` constructor arguments, where `db` is the ID of a connection service. Define these connections in the DI container, for example, in `config/common/db.php`: ```php @@ -96,7 +96,7 @@ return [ The name `default` is reserved and can't be used in `databases`. -Select one set with `--db`, or apply all sets with `migrate:up`: +Select one database with `--db`, or apply migrations to all databases with `migrate:up`: ```shell ./yii migrate:create create_places --db=maps diff --git a/src/Command/CreateCommand.php b/src/Command/CreateCommand.php index 4e005883..d1f14d71 100644 --- a/src/Command/CreateCommand.php +++ b/src/Command/CreateCommand.php @@ -78,9 +78,9 @@ public function __construct( private readonly CreateService $createService, private readonly MigrationService $migrationService, private readonly Migrator $migrator, - ?DatabaseRegistry $databases = null, + ?DatabaseRegistry $additionalDatabases = null, ) { - parent::__construct($databases); + parent::__construct($additionalDatabases); } protected function configure(): void diff --git a/src/Command/DatabaseCommand.php b/src/Command/DatabaseCommand.php index eed71df8..ade86aff 100644 --- a/src/Command/DatabaseCommand.php +++ b/src/Command/DatabaseCommand.php @@ -25,7 +25,7 @@ */ abstract class DatabaseCommand extends Command { - public function __construct(private readonly ?DatabaseRegistry $databases = null) + public function __construct(private readonly ?DatabaseRegistry $additionalDatabases = null) { parent::__construct(); $this->addOption('db', null, InputOption::VALUE_REQUIRED, 'Database configuration to use for migrations.'); @@ -33,7 +33,7 @@ public function __construct(private readonly ?DatabaseRegistry $databases = null final protected function execute(InputInterface $input, OutputInterface $output): int { - $availableNames = [DatabaseRegistry::DEFAULT_DATABASE, ...($this->databases?->getNames() ?? [])]; + $availableNames = [DatabaseRegistry::DEFAULT_DATABASE, ...($this->additionalDatabases?->getNames() ?? [])]; $multipleDatabases = count($availableNames) > 1; $io = new SymfonyStyle($input, $output); @@ -46,10 +46,10 @@ final protected function execute(InputInterface $input, OutputInterface $output) $hasResults = false; foreach ($names as $name) { - if ($this->databases === null || $name === DatabaseRegistry::DEFAULT_DATABASE) { + if ($this->additionalDatabases === null || $name === DatabaseRegistry::DEFAULT_DATABASE) { $context = $this->createDefaultContext(); } else { - $context = $this->createContext($this->databases->createSetup($name)); + $context = $this->createContext($this->additionalDatabases->createSetup($name)); } if ($multipleDatabases) { diff --git a/src/Command/DownCommand.php b/src/Command/DownCommand.php index 9e34baad..3b0c721c 100644 --- a/src/Command/DownCommand.php +++ b/src/Command/DownCommand.php @@ -48,9 +48,9 @@ public function __construct( private readonly DownRunner $downRunner, private readonly MigrationService $migrationService, private readonly Migrator $migrator, - ?DatabaseRegistry $databases = null, + ?DatabaseRegistry $additionalDatabases = null, ) { - parent::__construct($databases); + parent::__construct($additionalDatabases); } protected function configure(): void @@ -82,7 +82,7 @@ protected function executeForDatabase( InputInterface $input, OutputInterface $output, object $context, - ): int { + ): ?int { $io = new SymfonyStyle($input, $output); $context->migrator->setIo($io); $context->migrationService->setIo($io); @@ -115,7 +115,7 @@ protected function executeForDatabase( if (empty($migrations)) { $io->warning('No applied migrations found.'); - return Command::FAILURE; + return null; } if ($limit !== null) { @@ -128,7 +128,7 @@ protected function executeForDatabase( $output->writeln("Apply at least one migration first.\n"); $io->warning('No migration has been done before.'); - return Command::FAILURE; + return null; } $migrations = array_keys($migrations); diff --git a/src/Command/HistoryCommand.php b/src/Command/HistoryCommand.php index 2bc03edd..055798d3 100644 --- a/src/Command/HistoryCommand.php +++ b/src/Command/HistoryCommand.php @@ -40,9 +40,9 @@ final class HistoryCommand extends DatabaseCommand public function __construct( private readonly MigrationService $migrationService, private readonly Migrator $migrator, - ?DatabaseRegistry $databases = null, + ?DatabaseRegistry $additionalDatabases = null, ) { - parent::__construct($databases); + parent::__construct($additionalDatabases); } protected function configure(): void diff --git a/src/Command/MarkCommand.php b/src/Command/MarkCommand.php index f00c7128..96b931d7 100644 --- a/src/Command/MarkCommand.php +++ b/src/Command/MarkCommand.php @@ -54,9 +54,9 @@ final class MarkCommand extends DatabaseCommand public function __construct( private readonly MigrationService $migrationService, private readonly Migrator $migrator, - ?DatabaseRegistry $databases = null, + ?DatabaseRegistry $additionalDatabases = null, ) { - parent::__construct($databases); + parent::__construct($additionalDatabases); } protected function configure(): void diff --git a/src/Command/NewCommand.php b/src/Command/NewCommand.php index 12c50e6f..e4381e3d 100644 --- a/src/Command/NewCommand.php +++ b/src/Command/NewCommand.php @@ -46,9 +46,9 @@ final class NewCommand extends DatabaseCommand public function __construct( private readonly MigrationService $migrationService, private readonly Migrator $migrator, - ?DatabaseRegistry $databases = null, + ?DatabaseRegistry $additionalDatabases = null, ) { - parent::__construct($databases); + parent::__construct($additionalDatabases); } protected function configure(): void diff --git a/src/Command/RedoCommand.php b/src/Command/RedoCommand.php index bdc5b024..cd839889 100644 --- a/src/Command/RedoCommand.php +++ b/src/Command/RedoCommand.php @@ -49,9 +49,9 @@ public function __construct( private readonly Migrator $migrator, private readonly DownRunner $downRunner, private readonly UpdateRunner $updateRunner, - ?DatabaseRegistry $databases = null, + ?DatabaseRegistry $additionalDatabases = null, ) { - parent::__construct($databases); + parent::__construct($additionalDatabases); } protected function configure(): void @@ -88,7 +88,7 @@ protected function executeForDatabase( InputInterface $input, OutputInterface $output, object $context, - ): int { + ): ?int { $io = new SymfonyStyle($input, $output); $context->migrator->setIo($io); $context->migrationService->setIo($io); @@ -122,7 +122,7 @@ protected function executeForDatabase( if (empty($migrations)) { $io->warning('No applied migrations found.'); - return Command::FAILURE; + return null; } if ($limit !== null) { @@ -134,7 +134,7 @@ protected function executeForDatabase( if (empty($migrations)) { $io->warning('No migration has been done before.'); - return Command::FAILURE; + return null; } $migrations = array_keys($migrations); diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php index 72f26512..a0a797e6 100644 --- a/src/Command/UpdateCommand.php +++ b/src/Command/UpdateCommand.php @@ -47,9 +47,9 @@ public function __construct( private readonly UpdateRunner $updateRunner, private readonly MigrationService $migrationService, private readonly Migrator $migrator, - ?DatabaseRegistry $databases = null, + ?DatabaseRegistry $additionalDatabases = null, ) { - parent::__construct($databases); + parent::__construct($additionalDatabases); } protected function configure(): void From 3474f24d7397243ca9b9862737d9d86cf0ac0b6f Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Fri, 16 Oct 2026 17:01:31 +0700 Subject: [PATCH 10/10] Removed `MissingDatabaseOptionPolicy` (#372) * Removed `MissingDatabaseOptionPolicy` * Fix * Rename `getAllDatabaseNames()` to `getAvailableDatabaseNames()` --- src/Command/CreateCommand.php | 4 +- src/Command/DatabaseCommand.php | 64 ++++++++++++--------- src/Command/DownCommand.php | 5 -- src/Command/HistoryCommand.php | 4 +- src/Command/MarkCommand.php | 5 -- src/Command/MissingDatabaseOptionPolicy.php | 28 --------- src/Command/NewCommand.php | 15 +++-- src/Command/RedoCommand.php | 5 -- src/Command/UpdateCommand.php | 15 +++-- 9 files changed, 54 insertions(+), 91 deletions(-) delete mode 100644 src/Command/MissingDatabaseOptionPolicy.php diff --git a/src/Command/CreateCommand.php b/src/Command/CreateCommand.php index d1f14d71..a1f1fddd 100644 --- a/src/Command/CreateCommand.php +++ b/src/Command/CreateCommand.php @@ -96,9 +96,9 @@ protected function configure(): void ->setHelp('This command generates new migration file.'); } - protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy + protected function resolveDatabaseNamesWithMissingOption(InputInterface $input): array { - return MissingDatabaseOptionPolicy::ProcessDefaultDatabase; + return [DatabaseRegistry::DEFAULT_DATABASE]; } protected function createDefaultContext(): CreateContext diff --git a/src/Command/DatabaseCommand.php b/src/Command/DatabaseCommand.php index ade86aff..8d271563 100644 --- a/src/Command/DatabaseCommand.php +++ b/src/Command/DatabaseCommand.php @@ -13,7 +13,6 @@ use Yiisoft\Db\Migration\DatabaseRegistry; use Yiisoft\Db\Migration\DatabaseSetup; -use function count; use function implode; use function in_array; use function is_string; @@ -33,22 +32,22 @@ public function __construct(private readonly ?DatabaseRegistry $additionalDataba final protected function execute(InputInterface $input, OutputInterface $output): int { - $availableNames = [DatabaseRegistry::DEFAULT_DATABASE, ...($this->additionalDatabases?->getNames() ?? [])]; - $multipleDatabases = count($availableNames) > 1; $io = new SymfonyStyle($input, $output); - $names = $this->resolveDatabaseNames($input, $availableNames, $multipleDatabases); + $names = $this->resolveDatabaseNames($input); if (is_string($names)) { $io->error($names); return Command::INVALID; } + $multipleDatabases = $this->isMultipleDatabases(); $hasResults = false; foreach ($names as $name) { - if ($this->additionalDatabases === null || $name === DatabaseRegistry::DEFAULT_DATABASE) { + if (!$multipleDatabases || $name === DatabaseRegistry::DEFAULT_DATABASE) { $context = $this->createDefaultContext(); } else { + /** @psalm-var DatabaseRegistry $this->additionalDatabases */ $context = $this->createContext($this->additionalDatabases->createSetup($name)); } @@ -70,15 +69,20 @@ final protected function execute(InputInterface $input, OutputInterface $output) } /** - * Returns how the command behaves when the `--db` option isn't specified. - */ - abstract protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy; - - /** - * Returns the error message shown when the `--db` option is required, but isn't specified. + * Resolves the database names or returns a message indicating that an option is missing if multiple databases + * are configured. + * + * @param InputInterface $input The input interface containing the command options and arguments. + * + * @return string[]|string + * @psalm-return non-empty-list|string */ - protected function getMissingDatabaseOptionError(): string + protected function resolveDatabaseNamesWithMissingOption(InputInterface $input): array|string { + if (!$this->isMultipleDatabases()) { + return [DatabaseRegistry::DEFAULT_DATABASE]; + } + return 'The --db option is required when multiple databases are configured.'; } @@ -110,34 +114,38 @@ abstract protected function executeForDatabase( object $context, ): ?int; + /** + * @return string[] + * @psalm-return non-empty-list + */ + protected function getAvailableDatabaseNames(): array + { + return [DatabaseRegistry::DEFAULT_DATABASE, ...($this->additionalDatabases?->getNames() ?? [])]; + } + + protected function isMultipleDatabases(): bool + { + return $this->additionalDatabases !== null && $this->additionalDatabases->getNames() !== []; + } + /** * Resolves names of databases to process. * - * @param string[] $availableNames * @return string[]|string The database names, or an error message if the databases can't be resolved. * - * @psalm-param list $availableNames - * @psalm-return list|string + * @psalm-return non-empty-list|string */ - private function resolveDatabaseNames( - InputInterface $input, - array $availableNames, - bool $multipleDatabases, - ): array|string { + private function resolveDatabaseNames(InputInterface $input): array|string + { /** @var string|null $name */ $name = $input->getOption('db'); if ($name === null) { - $policy = $this->getMissingDatabaseOptionPolicy($input); - if ($multipleDatabases && $policy === MissingDatabaseOptionPolicy::RejectIfMultiple) { - return $this->getMissingDatabaseOptionError(); - } - - return $policy === MissingDatabaseOptionPolicy::ProcessDefaultDatabase - ? [DatabaseRegistry::DEFAULT_DATABASE] - : $availableNames; + return $this->resolveDatabaseNamesWithMissingOption($input); } + $availableNames = $this->getAvailableDatabaseNames(); + if (!in_array($name, $availableNames, true)) { return 'Unknown database "' . OutputFormatter::escape($name) . '". Available databases: ' . OutputFormatter::escape(implode(', ', $availableNames)) . '.'; diff --git a/src/Command/DownCommand.php b/src/Command/DownCommand.php index 3b0c721c..0277929b 100644 --- a/src/Command/DownCommand.php +++ b/src/Command/DownCommand.php @@ -63,11 +63,6 @@ protected function configure(): void ->addOption('force-yes', 'y', InputOption::VALUE_NONE, 'Force yes to all questions.'); } - protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy - { - return MissingDatabaseOptionPolicy::RejectIfMultiple; - } - protected function createDefaultContext(): DownContext { return new DownContext($this->migrator, $this->migrationService, $this->downRunner); diff --git a/src/Command/HistoryCommand.php b/src/Command/HistoryCommand.php index 055798d3..196446af 100644 --- a/src/Command/HistoryCommand.php +++ b/src/Command/HistoryCommand.php @@ -52,9 +52,9 @@ protected function configure(): void ->addOption('all', 'a', InputOption::VALUE_NONE, 'All migrations.'); } - protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy + protected function resolveDatabaseNamesWithMissingOption(InputInterface $input): array { - return MissingDatabaseOptionPolicy::ProcessAllDatabases; + return $this->getAvailableDatabaseNames(); } protected function createDefaultContext(): MigrationContext diff --git a/src/Command/MarkCommand.php b/src/Command/MarkCommand.php index 96b931d7..1553f4dd 100644 --- a/src/Command/MarkCommand.php +++ b/src/Command/MarkCommand.php @@ -66,11 +66,6 @@ protected function configure(): void ->addOption('force-yes', 'y', InputOption::VALUE_NONE, 'Force yes to all questions.'); } - protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy - { - return MissingDatabaseOptionPolicy::RejectIfMultiple; - } - protected function createDefaultContext(): MigrationContext { return new MigrationContext($this->migrator, $this->migrationService); diff --git a/src/Command/MissingDatabaseOptionPolicy.php b/src/Command/MissingDatabaseOptionPolicy.php deleted file mode 100644 index b8066269..00000000 --- a/src/Command/MissingDatabaseOptionPolicy.php +++ /dev/null @@ -1,28 +0,0 @@ -addOption('namespace', 'ns', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Namespace of migrations to display.'); } - protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy + protected function resolveDatabaseNamesWithMissingOption(InputInterface $input): array|string { - return $input->getOption('path') !== [] || $input->getOption('namespace') !== [] - ? MissingDatabaseOptionPolicy::RejectIfMultiple - : MissingDatabaseOptionPolicy::ProcessAllDatabases; - } + if (!$this->isMultipleDatabases()) { + return [DatabaseRegistry::DEFAULT_DATABASE]; + } - protected function getMissingDatabaseOptionError(): string - { - return 'The --db option is required with --path or --namespace when multiple databases are configured.'; + return $input->getOption('path') !== [] || $input->getOption('namespace') !== [] + ? 'The --db option is required with --path or --namespace when multiple databases are configured.' + : $this->getAvailableDatabaseNames(); } protected function createDefaultContext(): MigrationContext diff --git a/src/Command/RedoCommand.php b/src/Command/RedoCommand.php index cd839889..6b5a6ee9 100644 --- a/src/Command/RedoCommand.php +++ b/src/Command/RedoCommand.php @@ -64,11 +64,6 @@ protected function configure(): void ->addOption('force-yes', 'y', InputOption::VALUE_NONE, 'Force yes to all questions.'); } - protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy - { - return MissingDatabaseOptionPolicy::RejectIfMultiple; - } - protected function createDefaultContext(): RedoContext { return new RedoContext($this->migrator, $this->migrationService, $this->downRunner, $this->updateRunner); diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php index a0a797e6..97a3d214 100644 --- a/src/Command/UpdateCommand.php +++ b/src/Command/UpdateCommand.php @@ -61,16 +61,15 @@ protected function configure(): void ->addOption('force-yes', 'y', InputOption::VALUE_NONE, 'Force yes to all questions.'); } - protected function getMissingDatabaseOptionPolicy(InputInterface $input): MissingDatabaseOptionPolicy + protected function resolveDatabaseNamesWithMissingOption(InputInterface $input): array|string { - return $input->getOption('path') !== [] || $input->getOption('namespace') !== [] - ? MissingDatabaseOptionPolicy::RejectIfMultiple - : MissingDatabaseOptionPolicy::ProcessAllDatabases; - } + if (!$this->isMultipleDatabases()) { + return [DatabaseRegistry::DEFAULT_DATABASE]; + } - protected function getMissingDatabaseOptionError(): string - { - return 'The --db option is required with --path or --namespace when multiple databases are configured.'; + return $input->getOption('path') !== [] || $input->getOption('namespace') !== [] + ? 'The --db option is required with --path or --namespace when multiple databases are configured.' + : $this->getAvailableDatabaseNames(); } protected function createDefaultContext(): UpdateContext