Doctrine Migrations could be awesome

Jun 12, 2018 00:00 · 764 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 yesterdays 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 kind of is the framework equivalent of Batman’s utility belt: it comes with tons of different gadgets, and whenever you move out to save the city, it is up to you what items will be in that belt and at your disposal. Of course, you can switch out items as you go!

Translated to Symfony terms that means, we have access to an almost endless supply of components. Each can be used on its’ own but also be paired with more to create something bigger.

This allows you to build small things and grow as needed and most of all you should keep in mind the ability to swap out components and even external dependencies.

What you build should survive change, which means you should be writing tests to prove your architecture is sound and safe but also make sure what you build can be enhanced or replaced.

Driving home for christmas

Still with me? I hope so! There are multiple answer to what is weird in the migration:

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

Now you could be calling me names because MySQL and MariaDB are the same, and what the hell is wrong with only supporting the MySQL platform abstraction?

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 differing database types.

Fixing the car

Remember Batman’s utility belt? Correct, 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 so good because we implemented the spirit of Batman’s utility belt aka. Symfony!

  • we no longer bind our migratons 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