Doctrine Migrations
Generate a migration by diffing your entity mapping against the real database, read the SQL, then apply it.
php bin/console make:migration # then: php bin/console doctrine:migrations:migrateA migration is a PHP class holding the SQL that moves a database from one shape to the next. One class per change. The class is named after the moment it was created, like Version20260615120000, because migrations are sorted by name as strings and that format sorts in time order. Doctrine records applied versions in a table called doctrine_migration_versions, so it knows which ones are still pending.
Generating one
You do not write the first draft. php bin/console make:migration reads your entity mapping, reads the live schema from the database that DATABASE_URL points at, compares the two, and writes the statements that would close the gap into migrations/.
That command is shorthand. php bin/console doctrine:migrations:diff is the longhand, and it is the same comparison producing the same file; make:migration is MakerBundle's wrapper around it. Recognise either in someone else's project.
Read the SQL before running it
The diff sees two schemas, not your intention. Renaming a property looks exactly like dropping one column and adding another, which loses the data. Open the generated file and edit it. It is ordinary code.
Applying
Use php bin/console doctrine:migrations:migrate. It runs every pending version, oldest first. --dry-run executes nothing, and --write-sql=FILE writes the statements to a file for review instead of running them.
Example
<?php
// migrations/Version20260615120000.php
declare(strict_types=1);
// The namespace comes from migrations_paths in config/packages/doctrine_migrations.yaml.
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260615120000 extends AbstractMigration
{
public function getDescription(): string
{
// Generated as an empty string. Fill it in; it shows up in migrations:status.
return 'Create the article table';
}
public function up(Schema $schema): void
{
// addSql collects statements. They are executed after up() returns.
$this->addSql(<<<'SQL'
CREATE TABLE article (
id INT AUTO_INCREMENT NOT NULL,
title VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL,
published_at DATETIME DEFAULT NULL,
PRIMARY KEY(id)
)
SQL);
$this->addSql('CREATE UNIQUE INDEX UNIQ_23A0E66989D9B62 ON article (slug)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE article');
}
}
When to use it
- Add a column to an existing table without hand-writing the ALTER statement
- Check into version control the exact SQL that will run on the production database
- Bring a teammate's checkout up to date after pulling entity changes
- Review what a schema change will do before it touches real data
More examples
What make:migration prints
The command tells you to review the file before applying it, and that instruction is the whole point. Add --formatted to get the generated SQL wrapped across lines instead of one long string per statement, which makes the review possible at a glance.
$ php bin/console make:migration
created: migrations/Version20260615120000.php
SUCCESS!
Next: Review the new migration "migrations/Version20260615120000.php"
Then: Run the migration with php bin/console doctrine:migrations:migrateApplying pending migrations
Without --no-interaction the command asks for confirmation first, which is why it appears to hang in a script or a CI job. The counts at the end are the check that something happened: "0 migrations executed" means the version table already listed everything in migrations/.
$ php bin/console doctrine:migrations:migrate --no-interaction
[notice] Migrating up to DoctrineMigrations\Version20260615120000
[notice] finished in 118.4ms, used 20M memory, 1 migrations executed, 2 sql queriesWhere migrations live and what tracks them
Useful when a migration file exists but Doctrine reports nothing pending: either the file sits outside a configured path, or its namespace does not match the key here. Reach for this file also when a project keeps migrations under src/Migrations instead of the default directory.
# config/packages/doctrine_migrations.yaml
doctrine_migrations:
migrations_paths:
# The namespace 'DoctrineMigrations' maps to this directory.
'DoctrineMigrations': '%kernel.project_dir%/migrations'
storage:
table_storage:
# The table that records which versions have run.
table_name: 'doctrine_migration_versions'
version_column_name: 'version'
executed_at_column_name: 'executed_at'
# Fails a migration if it is run against a different database type
# than the one it was generated for. Leave this on.
check_database_platform: true
enable_profiler: false
Discussion