Doctrine Migrations could be awesome

Jun 12, 2018 00:00 · 782 words · 4 minutes read symfony

Yesterday we talked about a beautiful friendship between Doctrine and MariaDB (see this post). Did anyone spot something weird in the examples?

I’ll give you a moment to examine at yesterday’s sample migration:

final class Version20180612115042 extends AbstractMigration
{
    public function up(Schema $schema): void
    {
        // this up() migration is auto-generated, please modify it to your needs
        $this->abortIf(
            'mysql' !== $this->connection->getDatabasePlatform()->getName(),
            'Migration can only be executed safely on \'mysql\'.'
        );

        $this->addSql(
            'CREATE TABLE users (id INT UNSIGNED AUTO_INCREMENT NOT NULL, username VARCHAR(255) NOT NULL, password VARCHAR(255) DEFAULT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'
        );
    }

    ...
}

Before I tell you what it is, let me take a small detour into what I believe should be a guiding principle when building applications with Symfony.

Batman’s utility belt

Symfony is the framework equivalent of Batman’s utility belt. The belt has tons of compatible gadgets that work with a system. When it comes time to fight crime and save a city, it is up to you to determine what tools are equipped on that belt. Better yet, you can switch out items on that belt if you are fighting the Joker one day, and the Penguin the next.

This means we have an endless supply of components within Symfony to work with. Each component can be used on its own, or they can be paired to create something more powerful.

This allows you to build small things and scale as needed. Keep in mind your ability to swap out components and external dependencies.

What you build should survive changing conditions, which means you should be writing tests to prove your architecture is stable while ensuring it can be enhanced or replaced.

Driving home for Christmas

Still with me? I hope so! There are several answers to what is weird about the migrations I mentioned previously.

  1. the migration is tied to MySQL as a platform
  2. the migration uses MySQL/MariaDB grammar

You may be questioning my sanity by asking “aren’t MySQL and MariaDB the same?” You might also be asking “What the hell is wrong with supporting the MySQL platform abstraction?” I invite you to read on before you get too upset with me.

DBAL platform support

Let’s start with database platform support:

MySQL and MariaDB platform support in Doctrine already is in the early stages of diverging, as both databases now have incompatible information schemas. How long do you think we have until both databases further cement their differences? Weeks, months, years?

Platform-specific grammar

This comes including the same trap: grammar will diverge sooner or later, rest assured you will see Doctrine types mapped to different database types.

Fixing the car

Remember Batman’s utility belt? We already have a solution for this:

Doctrine DBAL to the Batmobile! Look again:

    public function up(Schema $schema): void
    {
        ...
    }

    public function down(Schema $schema): void
    {
        ...
    }
}

We already inject a Schema instance into every migration step. If you use PHP CodeSniffer along with the Symfony coding standard, you probably noticed it sheds a few tears in every migration.

Let’s rewrite our migration:

final class Version20180612115042 extends AbstractMigration
{
    public function up(Schema $schema): void
    {
        $usersTable = $schema->createTable('users');
        $usersTable->addColumn('id', 'integer', ['notnull' => true, 'unsigned' => true, 'autoincrement' => true]);
        $usersTable->addColumn('username', 'string', ['notnull' => true, 'length' => 255]);
        $usersTable->addColumn('password', 'string', ['notnull' => false, 'length' => 255, 'default' => null]);
        $usersTable->addColumn('email', 'string', ['notnull' => true, 'length' => 255]);

        $usersTable->addUniqueIndex(['username'], 'users_username_unique');
        $usersTable->addUniqueIndex(['email'], 'users_email_unique');

        $usersTable->setPrimaryKey(['id']);
    }

    /**
     * @param Schema $schema
     */
    public function down(Schema $schema): void
    {
        $schema->dropTable('users');
    }
}

Instead of expecting any specific platform, we use the configured schema to create, modify and delete tables and columns.

This is good because we implemented the spirit of Batman’s utility belt: Symfony!

  • we no longer bind our migrations to any specific database backend. This migration could be used with SQLite, Postgres, etc.
  • the code we wrote suddenly became explicit as it tells exactly what we want to achieve.
  • this could actually be tested. You do not have to but in certain cases like your company being subject to some certifications you might need to.
  • you just evaded the failed CI run that surely will come when MySQL and MariaDB diverge too far. Your migration will continue to work.

TL;DR

Writing migrations should be handled the same way we handle everything in Symfony application development. Do not couple your code too tight; one level is totally fine because we’re still inside Symfony. One too far, and we limit our options and introduce further points of failure.

Filed under pet peeve: this migration is readable. Isn’t that much nicer than a 500 character long line?

References

Comments powered by Disqus