Doctrine Migrations

Generate a migration by diffing your entity mapping against the real database, read the SQL, then apply it.

Syntaxphp bin/console make:migration # then: php bin/console doctrine:migrations:migrate

A 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

Example · php
<?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.

Example · bash
$ 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:migrate

Applying 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/.

Example · bash
$ 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 queries

Where 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.

Example · yaml
# 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

  • Be the first to comment on this lesson.