From 3c7b579d663de486ea72dccb28d21eb325d35ddb Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 4 Oct 2026 20:45:50 +0300 Subject: [PATCH 1/3] Update package configuration from #360 --- CHANGELOG.md | 3 +- bin/yii-db-migration | 96 +++++++++++--------- bin/yii-db-migration.php | 5 +- composer-dependency-analyser.php | 7 ++ composer.json | 1 + config/di-console.php | 38 +++----- config/params.php | 4 + docs/guide/en/multiple-databases.md | 8 +- docs/guide/en/usage-standalone.md | 19 +--- docs/guide/en/usage-with-symfony.md | 63 ++----------- docs/guide/en/usage-with-yii-console.md | 72 ++++++++------- src/Command/CommandFactory.php | 35 ------- src/Command/DatabaseCommand.php | 4 +- src/DatabaseSetRegistry.php | 20 +++- tests/Driver/Sqlite/DatabaseCommandsTest.php | 44 +-------- tests/Migration/BinTest.php | 71 ++++----------- tests/Migration/CommandFactoryTest.php | 36 -------- tests/Migration/ConfigTest.php | 60 +++++------- tests/Migration/DatabaseSetRegistryTest.php | 22 ++--- 19 files changed, 211 insertions(+), 397 deletions(-) delete mode 100644 src/Command/CommandFactory.php delete mode 100644 tests/Migration/CommandFactoryTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a103133..11bf8bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## 2.1.2 under development -- New #360: Support named database migration sets with `--db` selection, a single configuration source for `default`, and separate migration history (@samdark) +- New #360, #366: 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/bin/yii-db-migration b/bin/yii-db-migration index 55730ba9..3597a2b1 100644 --- a/bin/yii-db-migration +++ b/bin/yii-db-migration @@ -6,10 +6,20 @@ declare(strict_types=1); use Psr\Container\ContainerInterface; use Symfony\Component\Console\Application; use Yiisoft\Db\Connection\ConnectionInterface; -use Yiisoft\Db\Migration\Command\CommandFactory; +use Yiisoft\Db\Migration\Command\CreateCommand; +use Yiisoft\Db\Migration\Command\DownCommand; +use Yiisoft\Db\Migration\Command\HistoryCommand; +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\Informer\ConsoleMigrationInformer; +use Yiisoft\Db\Migration\Migrator; +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; $rootPath = dirname(__DIR__, 4); @@ -32,56 +42,52 @@ if (!file_exists($configFile)) { /** * @psalm-suppress MissingFile * @psalm-var array{ - * container?: ContainerInterface|null, - * db?: ConnectionInterface|null, + * container: ContainerInterface|null, + * db: ConnectionInterface|null, * databases?: array, - * useTablePrefix?: bool, - * historyTable?: string, - * migrationNameLimit?: int|null, - * maxSqlOutputLength?: int|null, - * newMigrationNamespace?: string, - * newMigrationPath?: string, - * sourceNamespaces?: list, - * sourcePaths?: list, + * useTablePrefix: bool, + * historyTable: string, + * migrationNameLimit: int|null, + * maxSqlOutputLength: int|null, + * newMigrationNamespace: string, + * newMigrationPath: string, + * sourceNamespaces: list, + * sourcePaths: list, * } $params */ $params = require $configFile; -$sets = $params['databases'] ?? []; -$legacyOptions = [ - 'db', 'newMigrationNamespace', 'newMigrationPath', 'sourceNamespaces', 'sourcePaths', - 'historyTable', 'migrationNameLimit', 'useTablePrefix', 'maxSqlOutputLength', -]; -if (isset($sets['default'])) { - foreach ($legacyOptions as $option) { - if (array_key_exists($option, $params)) { - throw new LogicException('Configure default migrations either in databases["default"] or with top-level options, not both. Conflicting option: ' . $option . '.'); - } - } -} else { - $sets['default'] = new DatabaseSet( - $params['db'] ?? throw new LogicException('DB connection is not configured.'), - newMigrationNamespace: $params['newMigrationNamespace'] ?? '', - newMigrationPath: $params['newMigrationPath'] ?? '', - sourceNamespaces: $params['sourceNamespaces'] ?? [], - sourcePaths: $params['sourcePaths'] ?? [], - historyTable: $params['historyTable'] ?? '{{%migration}}', - migrationNameLimit: array_key_exists('migrationNameLimit', $params) ? $params['migrationNameLimit'] : 180, - useTablePrefix: $params['useTablePrefix'] ?? true, - maxSqlOutputLength: $params['maxSqlOutputLength'] ?? null, - ); -} +$db = $params['db'] ?? throw new LogicException('DB connection is not configured.'); -$databases = new DatabaseSetRegistry( - new Injector($params['container'] ?? null), - new ConsoleMigrationInformer(), - $sets, +$injector = new Injector($params['container']); +$migrationInformer = new ConsoleMigrationInformer(); +$migrator = new Migrator( + $db, + $migrationInformer, + $params['historyTable'], + $params['migrationNameLimit'], + $params['maxSqlOutputLength'] ); -$factory = new CommandFactory($databases); +$createService = new CreateService($db, $params['useTablePrefix']); +$downRunner = new DownRunner($migrator); +$updateRunner = new UpdateRunner($migrator); + +$migrationService = new MigrationService($db, $injector, $migrator); +$migrationService->setNewMigrationNamespace($params['newMigrationNamespace']); +$migrationService->setNewMigrationPath($params['newMigrationPath']); +$migrationService->setSourceNamespaces($params['sourceNamespaces']); +$migrationService->setSourcePaths($params['sourcePaths']); + +$databases = new DatabaseSetRegistry($injector, $migrationInformer, $params['databases'] ?? []); + $application = new Application('Yii Database Migration Tool', '1.0.0'); -$commands = []; -foreach (['create', 'down', 'history', 'new', 'redo', 'up'] as $name) { - $commands[] = $factory->create($name); -} -$application->addCommands($commands); +$application->addCommands([ + new CreateCommand($createService, $migrationService, $migrator, $databases), + new DownCommand($downRunner, $migrationService, $migrator, $databases), + new HistoryCommand($migrationService, $migrator, $databases), + new NewCommand($migrationService, $migrator, $databases), + new RedoCommand($migrationService, $migrator, $downRunner, $updateRunner, $databases), + new UpdateCommand($updateRunner, $migrationService, $migrator, $databases), +]); + $application->run(); diff --git a/bin/yii-db-migration.php b/bin/yii-db-migration.php index 3fa96244..2039bdac 100644 --- a/bin/yii-db-migration.php +++ b/bin/yii-db-migration.php @@ -18,9 +18,8 @@ 'db' => null, /** - * Named database sets (name => Yiisoft\Db\Migration\DatabaseSet instance). - * For a named "default" set, move the legacy options below and "db" into that set, removing their top-level entries. - * Without a "default" entry, the top-level options configure default migrations. Do not supply both forms. + * Additional named databases (name => Yiisoft\Db\Migration\DatabaseSet instance). + * The "default" database is configured by the other options and can't be defined here. * See docs/guide/en/multiple-databases.md. */ 'databases' => [], diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php index 8a372da9..b01160da 100644 --- a/composer-dependency-analyser.php +++ b/composer-dependency-analyser.php @@ -13,6 +13,13 @@ ->addPathToScan(__DIR__ . '/resources', isDev: false) ->addPathToScan(__DIR__ . '/src', isDev: false) ->addPathToScan(__DIR__ . '/tests', isDev: true) + // The DI configuration is loaded only by yiisoft/config in Yii applications, where yiisoft/definitions is always + // installed as a dependency of yiisoft/di, so it isn't required by standalone and Symfony usage. + ->ignoreErrorsOnPackageAndPath( + 'yiisoft/definitions', + __DIR__ . '/config/di-console.php', + [ErrorType::DEV_DEPENDENCY_IN_PROD], + ) // These test suites only run in CI after their DBMS driver package (yiisoft/db-mssql, // yiisoft/db-mysql, yiisoft/db-oracle, yiisoft/db-pgsql) is installed on demand for the matching // per-DBMS workflow (see mssql.yml, mysql.yml, mariadb.yml, oracle.yml, pgsql.yml); the drivers are diff --git a/composer.json b/composer.json index 5a551b96..ca4b7e4b 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,7 @@ "rector/rector": "^2.4.2", "shipmonk/composer-dependency-analyser": "^1.8", "yiisoft/db-sqlite": "^2.0", + "yiisoft/definitions": "^3.4.1", "yiisoft/di": "^1.4.1", "yiisoft/files": "^2.1", "yiisoft/psr-dummy-provider": "^1.0.2", diff --git a/config/di-console.php b/config/di-console.php index e3dba47c..cd84c342 100644 --- a/config/di-console.php +++ b/config/di-console.php @@ -2,45 +2,37 @@ declare(strict_types=1); +use Yiisoft\Db\Migration\DatabaseSet; use Yiisoft\Db\Migration\DatabaseSetRegistry; -use Yiisoft\Db\Migration\Command\CommandFactory; -use Yiisoft\Db\Migration\Command\DatabaseCommand; use Yiisoft\Db\Migration\Informer\ConsoleMigrationInformer; use Yiisoft\Db\Migration\Informer\MigrationInformerInterface; use Yiisoft\Db\Migration\Service\MigrationService; +use Yiisoft\Definitions\DynamicReference; +use Yiisoft\Definitions\Reference; /** @var array $params */ -$definitions = [ +return [ DatabaseSetRegistry::class => [ 'class' => DatabaseSetRegistry::class, '__construct()' => [ - 'databases' => $params['yiisoft/db-migration']['databases'] ?? [], + 'databases' => array_map( + static fn(array $database): DynamicReference => DynamicReference::to([ + 'class' => DatabaseSet::class, + '__construct()' => ['db' => Reference::to($database['db'])] + $database, + ]), + $params['yiisoft/db-migration']['databases'], + ), ], ], MigrationService::class => [ 'class' => MigrationService::class, - 'setNewMigrationNamespace()' => [($params['yiisoft/db-migration']['newMigrationNamespace'] ?? '')], - 'setSourceNamespaces()' => [($params['yiisoft/db-migration']['sourceNamespaces'] ?? [])], - 'setNewMigrationPath()' => [($params['yiisoft/db-migration']['newMigrationPath'] ?? '')], - 'setSourcePaths()' => [($params['yiisoft/db-migration']['sourcePaths'] ?? [])], + 'setNewMigrationNamespace()' => [$params['yiisoft/db-migration']['newMigrationNamespace']], + 'setSourceNamespaces()' => [$params['yiisoft/db-migration']['sourceNamespaces']], + 'setNewMigrationPath()' => [$params['yiisoft/db-migration']['newMigrationPath']], + 'setSourcePaths()' => [$params['yiisoft/db-migration']['sourcePaths']], ], MigrationInformerInterface::class => ConsoleMigrationInformer::class, ]; - -if (isset($params['yiisoft/db-migration']['databases']['default'])) { - foreach (['newMigrationNamespace', 'newMigrationPath', 'sourceNamespaces', 'sourcePaths'] as $option) { - if (\array_key_exists($option, $params['yiisoft/db-migration'])) { - throw new LogicException('Configure default migrations either in databases["default"] or with migration parameters, not both. Conflicting option: ' . $option . '.'); - } - } - unset($definitions[MigrationService::class]); - foreach (['Create' => 'create', 'Down' => 'down', 'History' => 'history', 'New' => 'new', 'Redo' => 'redo', 'Update' => 'up'] as $class => $name) { - $definitions['Yiisoft\\Db\\Migration\\Command\\' . $class . 'Command'] - = static fn(CommandFactory $factory): DatabaseCommand => $factory->create($name); - } -} - -return $definitions; diff --git a/config/params.php b/config/params.php index d68d3067..a34ca983 100644 --- a/config/params.php +++ b/config/params.php @@ -22,6 +22,10 @@ ], 'yiisoft/db-migration' => [ + 'newMigrationNamespace' => '', + 'newMigrationPath' => '', + 'sourceNamespaces' => [], + 'sourcePaths' => [], 'databases' => [], ], ]; diff --git a/docs/guide/en/multiple-databases.md b/docs/guide/en/multiple-databases.md index 42c2e781..f40e5aed 100644 --- a/docs/guide/en/multiple-databases.md +++ b/docs/guide/en/multiple-databases.md @@ -7,13 +7,11 @@ one set, or apply migrations to all sets with a single command. ## Configure your databases -Your existing database is named `default`. Give each additional database a name, such as `maps` or `analytics`, and a -directory for its migrations. Define `default` in the same way when using named sets, with its own connection, -migration directory, and history table. Alternatively, keep your existing configuration for `default` and configure only -the additional sets. Choose one form for `default`; do not configure it in both places. Create the directories before generating migrations: +Your existing database is named `default` and keeps its current configuration. Give each additional database a name, +such as `maps` or `analytics`, its own connection, and a directory for its migrations. The name `default` is reserved +and can't be used for an additional database. Create the directories before generating migrations: ```text -config/migrations/default/ config/migrations/maps/ config/migrations/analytics/ ``` diff --git a/docs/guide/en/usage-standalone.md b/docs/guide/en/usage-standalone.md index dd60e65b..9a7a1a90 100644 --- a/docs/guide/en/usage-standalone.md +++ b/docs/guide/en/usage-standalone.md @@ -29,19 +29,16 @@ ## Multiple databases -Define all migration sets, including `default`, in `databases`. The example below assumes you have already created -`$defaultConnection`, `$mapsConnection`, and `$analyticsConnection`, as described in -[With configuration file](#with-configuration-file). +The `default` database is configured by the top-level `db` and migration options of the configuration file. Define +each additional database in `databases`. The example below assumes you have already created `$mapsConnection` and +`$analyticsConnection` in the same way as `db`, as described in [With configuration file](#with-configuration-file). ```php use Yiisoft\Db\Migration\DatabaseSet; return [ + // ... 'databases' => [ - 'default' => new DatabaseSet( - $defaultConnection, - newMigrationPath: __DIR__ . '/config/migrations/default', - ), 'maps' => new DatabaseSet( $mapsConnection, newMigrationPath: __DIR__ . '/config/migrations/maps', @@ -54,13 +51,7 @@ return [ ]; ``` -When moving an existing configuration to this form, move `db` and its migration options into the `default` set and -remove the top-level entries. Supplying both forms raises a configuration error, including when a top-level option has -an empty or default value. The optional `container` setting remains at the top level. - -Existing configurations remain supported: if `databases` does not contain `default`, the top-level `db`, migration -paths, namespaces, history, and generation options configure that set. You can still add `maps` and `analytics` to -`databases` while keeping this legacy form for `default`. +The name `default` is reserved and can't be used in `databases`. Create the migration directories before generating migrations. Then select a database with `--db`: diff --git a/docs/guide/en/usage-with-symfony.md b/docs/guide/en/usage-with-symfony.md index 6b7b20b2..a804d811 100644 --- a/docs/guide/en/usage-with-symfony.md +++ b/docs/guide/en/usage-with-symfony.md @@ -21,7 +21,6 @@ services: - '../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/CommandFactory.php' Yiisoft\Db\Migration\Informer\MigrationInformerInterface: class: 'Yiisoft\Db\Migration\Informer\ConsoleMigrationInformer' @@ -66,81 +65,35 @@ That's it. Now you can use `bin/console migrate:*` commands. ## Multiple databases -Use the following migration service configuration in place of the migration resource registration and -`MigrationService` configuration above. It assumes the three connection services already exist. Each connection, -migration directory, and history setting is defined in its `DatabaseSet` only; no separate default migration services -or application-wide `ConnectionInterface` binding are needed. - -Do not register the migration package through a broad `resource` rule alongside these definitions. A later resource -registration can replace explicit service definitions, including the configured registry. +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. ```yaml services: - _defaults: - autowire: true - autoconfigure: true - - Yiisoft\Injector\Injector: - arguments: ['@service_container'] - - Yiisoft\Db\Migration\Informer\MigrationInformerInterface: - class: Yiisoft\Db\Migration\Informer\ConsoleMigrationInformer - - app.migrations.default: - class: Yiisoft\Db\Migration\DatabaseSet - arguments: - $db: '@yii3.connections.default' - $newMigrationPath: '%kernel.project_dir%/config/migrations/default' + # ... app.migrations.maps: class: Yiisoft\Db\Migration\DatabaseSet arguments: - $db: '@yii3.connections.maps' + $db: '@app.db.maps' $newMigrationPath: '%kernel.project_dir%/config/migrations/maps' app.migrations.analytics: class: Yiisoft\Db\Migration\DatabaseSet arguments: - $db: '@yii3.connections.analytics' + $db: '@app.db.analytics' $newMigrationPath: '%kernel.project_dir%/config/migrations/analytics' Yiisoft\Db\Migration\DatabaseSetRegistry: arguments: $databases: - default: '@app.migrations.default' maps: '@app.migrations.maps' analytics: '@app.migrations.analytics' - - Yiisoft\Db\Migration\Command\CommandFactory: ~ - - Yiisoft\Db\Migration\Command\CreateCommand: - factory: ['@Yiisoft\Db\Migration\Command\CommandFactory', 'create'] - arguments: ['create'] - - Yiisoft\Db\Migration\Command\DownCommand: - factory: ['@Yiisoft\Db\Migration\Command\CommandFactory', 'create'] - arguments: ['down'] - - Yiisoft\Db\Migration\Command\HistoryCommand: - factory: ['@Yiisoft\Db\Migration\Command\CommandFactory', 'create'] - arguments: ['history'] - - Yiisoft\Db\Migration\Command\NewCommand: - factory: ['@Yiisoft\Db\Migration\Command\CommandFactory', 'create'] - arguments: ['new'] - - Yiisoft\Db\Migration\Command\RedoCommand: - factory: ['@Yiisoft\Db\Migration\Command\CommandFactory', 'create'] - arguments: ['redo'] - - Yiisoft\Db\Migration\Command\UpdateCommand: - factory: ['@Yiisoft\Db\Migration\Command\CommandFactory', 'create'] - arguments: ['up'] ``` -To retain your legacy default configuration instead, keep the single-database setup above, omit the `default` entry -from the registry, and use the original autowired command services instead of `CommandFactory`. The registry can still -contain additional sets. Choose one wiring style; do not keep both configurations for the default migration 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`: diff --git a/docs/guide/en/usage-with-yii-console.md b/docs/guide/en/usage-with-yii-console.md index a5686889..151ec568 100644 --- a/docs/guide/en/usage-with-yii-console.md +++ b/docs/guide/en/usage-with-yii-console.md @@ -11,14 +11,15 @@ declare(strict_types=1); use Yiisoft\Db\Connection\ConnectionInterface; use Yiisoft\Db\Sqlite\Connection as SqliteConnection; +use Yiisoft\Db\Sqlite\Driver as SqliteDriver; return [ ConnectionInterface::class => [ 'class' => SqliteConnection::class, '__construct()' => [ - 'dsn' => 'sqlite:' . __DIR__ . '/Data/yiitest.sq3' - ] - ] + 'driver' => new SqliteDriver('sqlite:' . __DIR__ . '/Data/yiitest.sq3'), + ], + ], ]; ``` @@ -48,43 +49,52 @@ View the list of available commands with `./yii list`: ## Multiple databases -Define your migration sets, including `default`, in the `databases` parameter: +The `default` database is configured as described above: it uses the `ConnectionInterface` service and the +migration parameters. Define each additional database in the `databases` parameter, referencing its connection by +a DI container service ID: + +```php +'yiisoft/db-migration' => [ + 'newMigrationNamespace' => 'App\\Migration', + 'sourceNamespaces' => ['App\\Migration'], + 'databases' => [ + 'maps' => [ + 'db' => 'db.maps', + 'newMigrationPath' => dirname(__DIR__, 2) . '/migrations/maps', + ], + 'analytics' => [ + 'db' => 'db.analytics', + 'newMigrationPath' => dirname(__DIR__, 2) . '/migrations/analytics', + ], + ], +], +``` + +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`: ```php -use Yiisoft\Db\Migration\DatabaseSet; +use Yiisoft\Db\Sqlite\Connection as SqliteConnection; +use Yiisoft\Db\Sqlite\Driver as SqliteDriver; -// These connection objects implement ConnectionInterface. return [ - 'yiisoft/db-migration' => [ - 'databases' => [ - 'default' => new DatabaseSet( - $defaultConnection, - newMigrationPath: __DIR__ . '/migrations/default', - ), - 'maps' => new DatabaseSet( - $mapsConnection, - newMigrationPath: __DIR__ . '/migrations/maps', - ), - 'analytics' => new DatabaseSet( - $analyticsConnection, - newMigrationPath: __DIR__ . '/migrations/analytics', - ), + // ... + 'db.maps' => [ + 'class' => SqliteConnection::class, + '__construct()' => [ + 'driver' => new SqliteDriver('sqlite:' . dirname(__DIR__, 2) . '/runtime/maps.sq3'), + ], + ], + 'db.analytics' => [ + 'class' => SqliteConnection::class, + '__construct()' => [ + 'driver' => new SqliteDriver('sqlite:' . dirname(__DIR__, 2) . '/runtime/analytics.sq3'), ], ], ]; ``` -Move existing `newMigrationNamespace`, `newMigrationPath`, `sourceNamespaces`, and `sourcePaths` parameters into the -`default` set and remove the original entries. Supplying both forms raises a configuration error. No application-wide -`ConnectionInterface` binding is required for migration commands in this form; each set supplies its connection. -Other parts of your application can continue to use their own database services. - -If you keep the legacy form, omit `databases['default']`. The existing migration service configuration and connection -then supply `default`, and `databases` can contain additional sets. - -For custom DI wiring, use `CommandFactory` with a registry containing `default` to build commands directly from the -sets. Replace the old migration service wiring when switching to this form; configuring those services separately -would leave unused settings. The [Symfony example](usage-with-symfony.md#multiple-databases) shows this approach. +The name `default` is reserved and can't be used in `databases`. Select one set with `--db`, or apply all sets with `migrate:up`: diff --git a/src/Command/CommandFactory.php b/src/Command/CommandFactory.php deleted file mode 100644 index c1264c47..00000000 --- a/src/Command/CommandFactory.php +++ /dev/null @@ -1,35 +0,0 @@ -databases->createContext('default') - ?? throw new LogicException('Configure databases["default"] when using CommandFactory.'); - $service = $context->migrationService; - $migrator = $context->migrator; - - return match ($name) { - 'create' => new CreateCommand($context->createService, $service, $migrator, $this->databases), - 'down' => new DownCommand($context->downRunner, $service, $migrator, $this->databases), - 'history' => new HistoryCommand($service, $migrator, $this->databases), - 'new' => new NewCommand($service, $migrator, $this->databases), - 'redo' => new RedoCommand($service, $migrator, $context->downRunner, $context->updateRunner, $this->databases), - 'up' => new UpdateCommand($context->updateRunner, $service, $migrator, $this->databases), - default => throw new InvalidArgumentException('Unknown migration command: ' . $name . '.'), - }; - } -} diff --git a/src/Command/DatabaseCommand.php b/src/Command/DatabaseCommand.php index 34d85c6e..f8767021 100644 --- a/src/Command/DatabaseCommand.php +++ b/src/Command/DatabaseCommand.php @@ -32,7 +32,7 @@ final protected function execute(InputInterface $input, OutputInterface $output) { /** @var string|null $name */ $name = $input->getOption('db'); - $names = $this->databases?->getNames() ?? ['default']; + $names = $this->databases?->getNames() ?? [DatabaseSetRegistry::DEFAULT_DATABASE]; $multipleDatabases = count($names) > 1; $io = new SymfonyStyle($input, $output); @@ -63,7 +63,7 @@ final protected function execute(InputInterface $input, OutputInterface $output) } if ($name !== null || $this instanceof CreateCommand) { - $names = [$name ?? 'default']; + $names = [$name ?? DatabaseSetRegistry::DEFAULT_DATABASE]; } $isListing = $this instanceof NewCommand || $this instanceof HistoryCommand; diff --git a/src/DatabaseSetRegistry.php b/src/DatabaseSetRegistry.php index 8ffd5699..e2db8369 100644 --- a/src/DatabaseSetRegistry.php +++ b/src/DatabaseSetRegistry.php @@ -17,15 +17,21 @@ use function trim; /** - * Named database sets, in execution order. Existing command dependencies are used when no default set is configured. + * Additional named database sets, in execution order. The "default" database is always configured by the existing + * command dependencies, so it can't be defined here. */ final class DatabaseSetRegistry { + /** + * Name of the default database configured by the existing command dependencies. + */ + public const DEFAULT_DATABASE = 'default'; + /** @var array */ private readonly array $databases; /** - * @param array $databases Named sets, optionally including "default". + * @param array $databases Additional named sets, excluding "default". */ public function __construct( private readonly Injector $injector, @@ -37,6 +43,12 @@ public function __construct( if (!is_string($name) || trim($name) === '') { throw new InvalidArgumentException('Database names must be non-empty strings.'); } + 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.', + ); + } $sets[$name] = $database; } $this->databases = $sets; @@ -47,7 +59,7 @@ public function __construct( */ public function getNames(): array { - return array_keys(['default' => null, ...$this->databases]); + return array_keys([self::DEFAULT_DATABASE => null, ...$this->databases]); } /** @@ -55,7 +67,7 @@ public function getNames(): array */ public function createContext(string $name): ?DatabaseContext { - if ($name === 'default' && !isset($this->databases[$name])) { + if ($name === self::DEFAULT_DATABASE) { return null; } diff --git a/tests/Driver/Sqlite/DatabaseCommandsTest.php b/tests/Driver/Sqlite/DatabaseCommandsTest.php index 3459c45c..930deedd 100644 --- a/tests/Driver/Sqlite/DatabaseCommandsTest.php +++ b/tests/Driver/Sqlite/DatabaseCommandsTest.php @@ -11,7 +11,6 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; use Yiisoft\Db\Migration\Command\CreateCommand; -use Yiisoft\Db\Migration\Command\CommandFactory; use Yiisoft\Db\Migration\Command\DownCommand; use Yiisoft\Db\Migration\Command\HistoryCommand; use Yiisoft\Db\Migration\Command\NewCommand; @@ -102,41 +101,6 @@ public function testSelectDatabaseForUpDownAndRedo(): void self::assertNull($this->connections['maps']->getSchema()->getTableSchema('example', true)); } - #[DataProvider('defaultSelection')] - public function testNamedDefaultSuppliesConnectionSourcesAndHistory(bool $selectDefault): void - { - $class = $this->writeMigration('maps'); - $default = new DatabaseSet( - $this->connections['maps'], - newMigrationPath: $this->directory . '/maps', - historyTable: 'custom_history', - ); - unset($this->sets['maps'], $this->sets['analytics']); - $input = $selectDefault ? ['--db' => 'default'] : []; - $new = $this->command('new', $default); - self::assertSame(Command::SUCCESS, $new->execute($input)); - self::assertStringContainsString($class, $new->getDisplay()); - self::assertSame(Command::SUCCESS, $this->command('up', $default)->execute($input + ['--force-yes' => true])); - $history = $this->command('history', $default); - self::assertSame(Command::SUCCESS, $history->execute($input)); - self::assertStringContainsString($class, $history->getDisplay()); - self::assertNotNull($this->connections['maps']->getSchema()->getTableSchema('custom_history')); - self::assertNull($this->connections['maps']->getSchema()->getTableSchema('migration')); - self::assertSame(Command::SUCCESS, $this->command('redo', $default)->execute($input + ['--force-yes' => true])); - self::assertNotNull($this->connections['maps']->getSchema()->getTableSchema('example', true)); - self::assertSame(Command::SUCCESS, $this->command('down', $default)->execute($input + ['--force-yes' => true])); - self::assertNull($this->connections['maps']->getSchema()->getTableSchema('example', true)); - self::assertSame(Command::SUCCESS, $this->command('create', $default)->execute($input + ['name' => 'CustomDefault'])); - self::assertCount(1, glob($this->directory . '/maps/*CustomDefault.php')); - self::assertSame([], glob($this->directory . '/default/*.php')); - self::assertSame([], $this->connections['default']->getSchema()->getTableNames()); - } - - public static function defaultSelection(): array - { - return [[false], [true]]; - } - public static function commands(): array { return [['up'], ['down'], ['redo'], ['new'], ['history'], ['create']]; @@ -557,17 +521,11 @@ private function migrator(string $name): Migrator return new Migrator($this->connections[$name], new NullMigrationInformer(), $this->sets[$name]->historyTable); } - private function command(string $name, ?DatabaseSet $default = null): CommandTester + private function command(string $name): CommandTester { $sets = $this->sets; unset($sets['default']); - if ($default !== null) { - $sets['default'] = $default; - } $registry = new DatabaseSetRegistry(new Injector(), new NullMigrationInformer(), $sets); - if ($default !== null) { - return new CommandTester((new CommandFactory($registry))->create($name)); - } $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 ca8f6092..96e483a4 100644 --- a/tests/Migration/BinTest.php +++ b/tests/Migration/BinTest.php @@ -5,7 +5,6 @@ namespace Yiisoft\Db\Migration\Tests\Migration; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Attributes\DataProvider; use Yiisoft\Files\FileHelper; use Yiisoft\Db\Cache\SchemaCache; use Yiisoft\Db\Sqlite\Connection; @@ -119,60 +118,28 @@ public function up(\Yiisoft\Db\Migration\MigrationBuilder $b): void $this->assertSame(0, (int) $db->createCommand('SELECT COUNT(*) FROM example')->queryScalar()); } - public function testNamedDefaultWithoutLegacyOptions(): void + public function testRejectsDefaultDatabase(): void { - $directory = dirname(__DIR__, 2) . '/runtime/tests/bin'; - FileHelper::ensureDirectory($directory . '/migrations'); - file_put_contents($directory . '/yii-db-migration.php', $this->namedDefaultConfig()); - [$output, $exitCode] = $this->runYiiDbMigration(['migrate:create', 'Example']); - self::assertSame(0, $exitCode, $output); - self::assertCount(1, glob($directory . '/migrations/*Example.php')); - [$output, $exitCode] = $this->runYiiDbMigration(['migrate:up', '-y']); - self::assertSame(0, $exitCode, $output); - $db = new Connection(new Driver('sqlite:' . $directory . '/named.sqlite'), new SchemaCache(new MemorySimpleCache())); - self::assertCount(1, $db->createCommand('SELECT name FROM named_history')->queryColumn()); - } - - public static function legacyOptions(): array - { - return array_map(static fn(string $option): array => [$option], [ - 'db', 'newMigrationNamespace', 'newMigrationPath', 'sourceNamespaces', 'sourcePaths', - 'historyTable', 'migrationNameLimit', 'useTablePrefix', 'maxSqlOutputLength', - ]); - } + $this->replaceParams('return [', <<<'PHP' + $db = new \Yiisoft\Db\Sqlite\Connection( + new \Yiisoft\Db\Sqlite\Driver('sqlite::memory:'), + new \Yiisoft\Db\Cache\SchemaCache(new \Yiisoft\Test\Support\SimpleCache\MemorySimpleCache()) + ); + return [ + PHP); + $this->replaceParams("'db' => null,", "'db' => \$db,"); + $this->replaceParams( + "'databases' => [],", + "'databases' => ['default' => new \\Yiisoft\\Db\\Migration\\DatabaseSet(\$db)],", + ); - #[DataProvider('legacyOptions')] - public function testRejectsDuplicateDefaultConfiguration(string $option): void - { - $directory = dirname(__DIR__, 2) . '/runtime/tests/bin'; - file_put_contents($directory . '/yii-db-migration.php', str_replace( - 'return [', - "return ['$option' => null,", - $this->namedDefaultConfig(), - )); - [$output, $exitCode] = $this->runYiiDbMigration(['migrate:up', '-y']); - self::assertNotSame(0, $exitCode); - self::assertStringContainsString('Conflicting option: ' . $option . '.', $output); - self::assertFileDoesNotExist($directory . '/named.sqlite'); - } + [$output, $exitCode] = $this->runYiiDbMigration(); - private function namedDefaultConfig(): string - { - return <<<'PHP' - [ - 'default' => new \Yiisoft\Db\Migration\DatabaseSet( - new \Yiisoft\Db\Sqlite\Connection( - new \Yiisoft\Db\Sqlite\Driver('sqlite:' . __DIR__ . '/named.sqlite'), - new \Yiisoft\Db\Cache\SchemaCache(new \Yiisoft\Test\Support\SimpleCache\MemorySimpleCache()) - ), - newMigrationPath: __DIR__ . '/migrations', - historyTable: 'named_history', - ), - ], - ]; - PHP; + $this->assertSame(255, $exitCode); + $this->assertStringContainsString( + 'The "default" database is configured by the existing migration settings and can\'t be redefined.', + $output, + ); } private function replaceParams($search, $replace): void diff --git a/tests/Migration/CommandFactoryTest.php b/tests/Migration/CommandFactoryTest.php deleted file mode 100644 index d704b7b5..00000000 --- a/tests/Migration/CommandFactoryTest.php +++ /dev/null @@ -1,36 +0,0 @@ -expectException(LogicException::class); - $this->expectExceptionMessage('Configure databases["default"] when using CommandFactory.'); - $factory->create('up'); - } - - public function testUnknownCommand(): void - { - $factory = new CommandFactory(new DatabaseSetRegistry(new Injector(), new NullMigrationInformer(), [ - 'default' => new DatabaseSet($this->createMock(ConnectionInterface::class)), - ])); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Unknown migration command: unknown.'); - $factory->create('unknown'); - } -} diff --git a/tests/Migration/ConfigTest.php b/tests/Migration/ConfigTest.php index c0498c04..38a1e12b 100644 --- a/tests/Migration/ConfigTest.php +++ b/tests/Migration/ConfigTest.php @@ -5,17 +5,13 @@ namespace Yiisoft\Db\Migration\Tests\Migration; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Attributes\DataProvider; -use LogicException; use Psr\SimpleCache\CacheInterface; -use Yiisoft\Db\Cache\SchemaCache; use Yiisoft\Db\Connection\ConnectionInterface; use Yiisoft\Db\Sqlite\Connection as SqLiteConnection; use Yiisoft\Db\Sqlite\Driver as SqLiteDriver; use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; use Yiisoft\Test\Support\SimpleCache\MemorySimpleCache; -use Yiisoft\Db\Migration\DatabaseSet; use Yiisoft\Db\Migration\DatabaseSetRegistry; use Symfony\Component\Console\Tester\CommandTester; use Yiisoft\Db\Migration\Command\CreateCommand; @@ -70,47 +66,38 @@ public function testBase(): void public function testAdditionalDatabaseConfiguration(): void { - $set = new DatabaseSet(new SqLiteConnection(new SqLiteDriver('sqlite::memory:'), new SchemaCache(new MemorySimpleCache()))); - $container = $this->createConsoleContainer(['maps' => $set]); + $container = $this->createConsoleContainer( + [ + 'maps' => [ + 'db' => 'db.maps', + 'newMigrationPath' => dirname(__DIR__) . '/Support/MigrationsExtra', + 'historyTable' => 'maps_history', + ], + ], + [ + 'db.maps' => [ + 'class' => SqLiteConnection::class, + '__construct()' => [ + 'driver' => new SqLiteDriver('sqlite::memory:'), + ], + ], + ], + ); $this->assertSame(['default', 'maps'], $container->get(DatabaseSetRegistry::class)->getNames()); + $command = new CommandTester($container->get(DownCommand::class)); $this->assertSame(2, $command->execute([])); $this->assertStringContainsString('--db option is required', $command->getDisplay()); - } - public function testNamedDefaultNeedsNoLegacyConnection(): void - { - $db = new SqLiteConnection(new SqLiteDriver('sqlite::memory:'), new SchemaCache(new MemorySimpleCache())); - $container = new Container(ContainerConfig::create()->withDefinitions($this->getConsoleDefinitions([ - 'default' => new DatabaseSet($db, newMigrationPath: dirname(__DIR__) . '/Support/MigrationsExtra'), - ]))); - foreach ([CreateCommand::class, DownCommand::class, HistoryCommand::class, NewCommand::class, RedoCommand::class, UpdateCommand::class] as $class) { - self::assertInstanceOf($class, $container->get($class)); - } $command = new CommandTester($container->get(NewCommand::class)); - self::assertSame(0, $command->execute([])); - self::assertStringContainsString('M231108183919Empty', $command->getDisplay()); - } - - public static function legacyMigrationOptions(): array - { - return [['newMigrationNamespace'], ['newMigrationPath'], ['sourceNamespaces'], ['sourcePaths']]; - } - - #[DataProvider('legacyMigrationOptions')] - public function testRejectsDuplicateDefaultConfiguration(string $option): void - { - $params = $this->getParams(); - $params['yiisoft/db-migration']['databases']['default'] = new DatabaseSet( - new SqLiteConnection(new SqLiteDriver('sqlite::memory:'), new SchemaCache(new MemorySimpleCache())), + $this->assertSame(0, $command->execute(['--db' => 'maps'])); + $this->assertStringContainsString('M231108183919Empty', $command->getDisplay()); + $this->assertNotNull( + $container->get('db.maps')->getSchema()->getTableSchema('maps_history'), ); - $params['yiisoft/db-migration'][$option] = ''; - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Conflicting option: ' . $option . '.'); - require dirname(__DIR__, 2) . '/config/di-console.php'; } - private function createConsoleContainer(array $databases = []): Container + private function createConsoleContainer(array $databases = [], array $definitions = []): Container { $config = ContainerConfig::create() ->withDefinitions(array_merge( @@ -126,6 +113,7 @@ private function createConsoleContainer(array $databases = []): Container ], ], ], + $definitions, $this->getConsoleDefinitions($databases), )); return new Container($config); diff --git a/tests/Migration/DatabaseSetRegistryTest.php b/tests/Migration/DatabaseSetRegistryTest.php index 19c28863..27acfe51 100644 --- a/tests/Migration/DatabaseSetRegistryTest.php +++ b/tests/Migration/DatabaseSetRegistryTest.php @@ -30,12 +30,18 @@ public function testRejectsInvalidNames(string|int $name): void ]); } - public static function defaultConfiguration(): array + public function testRejectsDefaultName(): void { - return [[false], [true]]; + $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 testUnconfiguredDefaultUsesExistingServices(): void + public function testDefaultUsesExistingServices(): void { $registry = new DatabaseSetRegistry(new Injector(), new NullMigrationInformer()); self::assertNull($registry->createContext('default')); @@ -50,21 +56,13 @@ public function testCannotCreateContextForUnknownDatabase(): void $registry->createContext('missing'); } - #[DataProvider('defaultConfiguration')] - public function testDefaultIsFirstFollowedByConfigurationOrder(bool $explicitDefault): void + public function testDefaultIsFirstFollowedByConfigurationOrder(): void { $set = new DatabaseSet($this->createMock(ConnectionInterface::class)); $registry = new DatabaseSetRegistry(new Injector(), new NullMigrationInformer(), [ 'maps' => $set, 'analytics' => $set, ]); - if ($explicitDefault) { - $registry = new DatabaseSetRegistry(new Injector(), new NullMigrationInformer(), [ - 'maps' => $set, - 'default' => $set, - 'analytics' => $set, - ]); - } self::assertSame(['default', 'maps', 'analytics'], $registry->getNames()); } } From 161d6f1a0ea97dc0e52efd2ef8122e84c3af2795 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Tue, 13 Oct 2026 16:30:18 +0300 Subject: [PATCH 2/3] fix merge --- src/Command/CommandFactory.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/Command/CommandFactory.php diff --git a/src/Command/CommandFactory.php b/src/Command/CommandFactory.php deleted file mode 100644 index e69de29b..00000000 From 05568767a313ef82e81027224eaa15e5bb780231 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Tue, 13 Oct 2026 16:32:58 +0300 Subject: [PATCH 3/3] fix changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b7fb212..fe32a236 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,9 @@ ## 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 #363: Add `migrate:mark` to move migration history to a version without executing migrations (@samdark) -- New #360: Support named database migration sets with `--db` selection, a single configuration source for `default`, and separate migration history (@samdark) - Enh #255: Show friendly error on run `./vendor/bin/yii-db-migration` without configuration file (@KalimeroMK) ## 2.1.1 August 10, 2026