Migrations

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We have a beautiful new Question entity class that is supposed to map to a question table in the database. But... that table does not exist yet. How can we create it?

Well, because Doctrine has all of this configuration about the entity, like the fields and field types, it should - in theory - be able to create the table for us. And... it absolutely can!

Hello make:migration

The mechanism we use to make database structure changes is called migrations. At your terminal, run:

php bin/console make:migration

And... that fails:

Access denied for user db_user.

Of course: the command doesn't have access to the Docker environment variables. I meant to run:

symfony console make:migration

This time... cool! It generated a new file inside of a migrations/ directory. Let's go check it out! In migrations/ open the one new file and... awesome! It has an up() method with the exact SQL we need!

... lines 1 - 4
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20200707173854 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE question (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, slug VARCHAR(100) NOT NULL, question LONGTEXT NOT NULL, asked_at DATETIME DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE question');
}
}

CREATE TABLE question...

and then all of the columns.

The make:migration command is smart: it compares the actual database - which has zero tables at the moment - with all of our entity classes - just one right now - and then generates the SQL needed to make the database match those entities.

It saw the one Question entity... but no question table, and so, it generated the CREATE TABLE statement.

Executing Migrations

But this query has not been executed yet. To do that, run:

php bin/console doctrine:migrations:migrate

Ah, but be careful: we can't use bin/console directly. Instead run:

symfony console doctrine:migrations:migrate

And... congratulations! We have a new question table in the database!

How Executed Migrations are Tracked

The way the migration system works is really cool. Run another command:

symfony console doctrine:migrations:list

This shows all the migrations in your app, which is just one right now. Next to that migration is says "Status Migrated". How does it know that?

Behind the scenes, the migration system created a table in the database called doctrine_migration_versions. Each time it executes a migration file, it adds a new row to that table that records that it was executed.

That means that later, if you run

symfony console doctrine:migrations:migrate

again... it's smart enough to not execute the same migration twice. It looks at the table, sees that it already ran this, and skips it.

When you deploy to production, you'll also run doctrine:migrations:migrate. When you do that, it will check the doctrine_migration_versions table in the production database and execute any new migrations.

Making a Column Unique

Before we keep going, you know what? When we created the Question entity, I forgot to do something. The slug column should really be unique in the database because we will eventually use that part of the URL to query for the one Question that matches.

One of the options you can pass to @ORM\Column() is unique=true.

... lines 1 - 10
class Question
{
... lines 13 - 24
/**
* @ORM\Column(type="string", length=100, unique=true)
*/
private $slug;
... lines 29 - 91
}

That won't change how our PHP code behaves - this doesn't relate to form validation or anything like that. This simply tells Doctrine:

Hey! I want this column to have a unique constraint in the database

Of course... just making this change did not somehow magically add the unique constraint to the database. To do that, we need to generate another migration.

Cool! At your terminal, once again run:

symfony console make:migration

to generate a second migration file. Let's go check it out.

... lines 1 - 4
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20200707174149 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE UNIQUE INDEX UNIQ_B6F7494E989D9B62 ON question (slug)');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP INDEX UNIQ_B6F7494E989D9B62 ON question');
}
}

And... woh! It's a CREATE UNIQUE INDEX statement for the slug column! The migrations system compared the question table in the database to the Question entity, determined that the only difference was a missing unique index and then generated the SQL to add it. Honestly, that's amazing.

Let's go run it:

symfony console doctrine:migrations:migrate

This sees both migrations, but only runs the one that hasn't been executed yet. The slug column is now unique in the database.

So this is the workflow: create a new entity or change an existing entity, run make:migration, and then execute it with doctrine:migrations:migrate. This keeps your database in sync with your entity classes and give you a set of migrations that you can run when you deploy to production.

Next: it's time to create some Question objects in PHP and see how we can save those to the question table.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.8", // 1.8.2
        "doctrine/doctrine-bundle": "^2.1", // 2.1.0
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.1
        "doctrine/orm": "^2.7", // v2.7.3
        "knplabs/knp-markdown-bundle": "^1.8", // 1.8.1
        "knplabs/knp-time-bundle": "^1.11", // v1.12.0
        "sensio/framework-extra-bundle": "^5.5", // v5.6.1
        "sentry/sentry-symfony": "^3.4", // 3.5.2
        "stof/doctrine-extensions-bundle": "^1.4", // v1.4.0
        "symfony/asset": "5.1.*", // v5.1.2
        "symfony/console": "5.1.*", // v5.1.2
        "symfony/dotenv": "5.1.*", // v5.1.2
        "symfony/flex": "^1.3.1", // v1.9.0
        "symfony/framework-bundle": "5.1.*", // v5.1.2
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/stopwatch": "5.1.*", // v5.1.2
        "symfony/twig-bundle": "5.1.*", // v5.1.2
        "symfony/webpack-encore-bundle": "^1.7", // v1.7.3
        "symfony/yaml": "5.1.*", // v5.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.0.4
        "twig/twig": "^2.12|^3.0" // v3.0.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.1
        "symfony/debug-bundle": "5.1.*", // v5.1.2
        "symfony/maker-bundle": "^1.15", // v1.20.0
        "symfony/var-dumper": "5.1.*", // v5.1.2
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.2
        "zenstruck/foundry": "^1.1" // v1.1.0
    }
}