Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +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: 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
Expand Down
98 changes: 53 additions & 45 deletions bin/yii-db-migration
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,21 @@ 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\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\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);
Expand All @@ -32,56 +43,53 @@ if (!file_exists($configFile)) {
/**
* @psalm-suppress MissingFile
* @psalm-var array{
* container?: ContainerInterface|null,
* db?: ConnectionInterface|null,
* container: ContainerInterface|null,
* db: ConnectionInterface|null,
* databases?: array<string, DatabaseSet>,
* useTablePrefix?: bool,
* historyTable?: string,
* migrationNameLimit?: int|null,
* maxSqlOutputLength?: int|null,
* newMigrationNamespace?: string,
* newMigrationPath?: string,
* sourceNamespaces?: list<string>,
* sourcePaths?: list<string>,
* useTablePrefix: bool,
* historyTable: string,
* migrationNameLimit: int|null,
* maxSqlOutputLength: int|null,
* newMigrationNamespace: string,
* newMigrationPath: string,
* sourceNamespaces: list<string>,
* sourcePaths: list<string>,
* } $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', 'mark', '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 MarkCommand($migrationService, $migrator, $databases),
new NewCommand($migrationService, $migrator, $databases),
new RedoCommand($migrationService, $migrator, $downRunner, $updateRunner, $databases),
new UpdateCommand($updateRunner, $migrationService, $migrator, $databases),
]);

$application->run();
5 changes: 2 additions & 3 deletions bin/yii-db-migration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => [],
Expand Down
7 changes: 7 additions & 0 deletions composer-dependency-analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 15 additions & 23 deletions config/di-console.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can db be missing?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. db is required key:

'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',
        ],
    ],
],

But databases whole can be empty array:

'yiisoft/db-migration' => [
    'newMigrationNamespace' => 'App\\Migration',
    'sourceNamespaces' => ['App\\Migration'],
    'databases' => [],
],

]),
$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', 'Mark' => 'mark', '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;
4 changes: 4 additions & 0 deletions config/params.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
],

'yiisoft/db-migration' => [
'newMigrationNamespace' => '',
'newMigrationPath' => '',
'sourceNamespaces' => [],
'sourcePaths' => [],
'databases' => [],
],
];
8 changes: 3 additions & 5 deletions docs/guide/en/multiple-databases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
```
Expand Down
19 changes: 5 additions & 14 deletions docs/guide/en/usage-standalone.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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`:

Expand Down
67 changes: 8 additions & 59 deletions docs/guide/en/usage-with-symfony.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -66,85 +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\MarkCommand:
factory: ['@Yiisoft\Db\Migration\Command\CommandFactory', 'create']
arguments: ['mark']

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`:

Expand Down
Loading
Loading