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:
vendor/bin/doctrine-migrations generatecreates an empty migration class in the directory of the migrations, with theup()anddown()methods to complete.vendor/bin/doctrine-migrations diffcreates the migration by comparing the database with the entities oform.paths: itsup()has the SQL that makes the database like the entities (aCREATE TABLEfor an entity whose table does not exist).vendor/bin/doctrine-migrations statusshows the database that it uses and the executed and available migrations.vendor/bin/doctrine-migrations migrateexecutes the pending migrations and ends with[OK] Successfully migrated to version: App\Migrations\Version20260101000000.vendor/bin/doctrine-migrations up-to-datetells 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.