Migrations

The CLI of Doctrine Migrations, vendor/bin/doctrine-migrations, has to know the database that it connects to and the directory of the migrations. In an application with doctrine-migrations-bundle the bundle gives it, and its commands are used instead. Without a framework, the convention of Doctrine is a cli-config.php file that returns a DependencyFactory. Without that file the command stops with It was not possible to locate any configuration file.

DoctrineCliConfigurationFactory creates that DependencyFactory from a DSN or from an array, so the file is a single line.

The CLI looks for a file called cli-config.php in the directory where the command runs, and then in its config/ subdirectory, so it can be in the root of the project or in config/. The file returns the factory:

use Derafu\Orm\Factory\DoctrineCliConfigurationFactory;

return (new DoctrineCliConfigurationFactory())->create('postgresql://app:[email protected]:5432/app?serverVersion=16&charset=utf8');

The credentials do not have to be in the file: create((string) getenv('DATABASE_URL')) reads the DSN from the environment.

DSN

Database DSN
PostgreSQL postgresql://app:[email protected]:5432/app?serverVersion=16&charset=utf8
MySQL, MariaDB mysql://app:[email protected]:3306/app?serverVersion=8.0&charset=utf8mb4
SQLite sqlite:///var/data.db, relative to the current directory, or sqlite:////path/to/data.db, absolute

The DSN is read with DBAL’s DsnParser, as the url option of the entity manager, so the other schemes and the rules are the same: pgsql and postgres are the same driver as postgresql, the parameters after ? are parameters of the connection (serverVersion, charset, sslmode…), and a special character in the user or the password is written URL-encoded (p@ss is p%40ss).

Array

With an array, which has the sections dbal and orm of the entity manager and migrations:

return (new DoctrineCliConfigurationFactory())->create([
    'migrations' => [
        'paths' => ['App\\Migrations' => __DIR__ . '/migrations'],
        'all_or_nothing' => true,
        'transactional' => true,
        'check_database_platform' => true,
    ],
    'dbal' => ['driver' => 'pdo_sqlite', 'path' => __DIR__ . '/var/data.db'],
    'orm' => ['paths' => [__DIR__ . '/src/Entity']],
]);

In config/cli-config.php, __DIR__ is the config/ directory, so the paths are built from dirname(__DIR__).

The three options of migrations are enabled by default. With a DSN, the migrations are in migrations/ and the entities in src/Entity/, relative to the current directory, so the commands are run from the root of the project. The directories of the migrations, the entities and the SQLite file are created if they do not exist. The commands are those of Doctrine: vendor/bin/doctrine-migrations diff, migrate and status.

Usage

The commands are run from the root of the project, with the file in place:

  1. vendor/bin/doctrine-migrations generate creates an empty migration class in the directory of the migrations, with the up() and down() methods to complete.
  2. vendor/bin/doctrine-migrations diff creates the migration by comparing the database with the entities of orm.paths: its up() has the SQL that makes the database like the entities (a CREATE TABLE for an entity whose table does not exist).
  3. vendor/bin/doctrine-migrations status shows the database that it uses and the executed and available migrations.
  4. vendor/bin/doctrine-migrations migrate executes the pending migrations and ends with [OK] Successfully migrated to version: App\Migrations\Version20260101000000.
  5. vendor/bin/doctrine-migrations up-to-date tells if there is nothing left to execute: [OK] Up-to-date! No migrations to execute.

The executed migrations are recorded in the table doctrine_migration_versions of the database, which Doctrine creates. No entity maps that table, so diff leaves it out of the comparison and never proposes to drop it, and the other commands always see it, even if the schema_filter of dbal would leave it out.

migrate always asks for a confirmation, and diff asks for one when there are migrations that are not executed. Add --no-interaction to run them in a script or in a CI, where nobody can answer.

Command Purpose
migrations:generate (generate) Create an empty migration
migrations:diff (diff) Create a migration from the differences between the entities and the database
migrations:migrate (migrate) Execute the migrations up to a version, or the latest
migrations:execute (execute) Execute one or more versions, up or down
migrations:status (status) Show the configuration and the state of the migrations
migrations:list (list-migrations) List the migrations and their state
migrations:current (current), migrations:latest (latest) Show the current version and the latest one
migrations:up-to-date (up-to-date) Tell if the database is up to date
migrations:version (version) Add or remove versions in the table of versions, without executing them
migrations:rollup (rollup) Replace the recorded versions with a single one
migrations:dump-schema (dump-schema) Write the schema of the database as a migration
migrations:sync-metadata-storage (sync-metadata-storage) Update the table of versions to the latest format

Tables that no entity maps

diff makes the database like the entities, so it proposes to drop the tables of the database that no entity maps (a DROP TABLE legacy_x for a table of another application, for example). To keep it away from them, limit the tables that Doctrine reads with the schema_filter of the dbal section, a regular expression of the tables to keep:

'dbal' => [
    'url' => 'postgresql://app:[email protected]:5432/app?serverVersion=16&charset=utf8',
    'schema_filter' => '~^(?!legacy_)~',
],

The --filter-expression option of diff does the same for one run. When both are used, a table has to pass both.

With SQLite, a table whose identifier is a single integer without a generated value is proposed again by every diff, as Doctrine reads that table back as an auto-increment key. It is a behavior of Doctrine with SQLite, not of the factory.

On this page

Last updated on 26/10/2026 by Anonymous