Skip to main content

Automatic migration planning for Doctrine

TL;DR

  • Doctrine is an ORM library that's widely used in the PHP community.
  • Atlas is an open-source tool for inspecting, planning, linting and executing schema changes to your database.
  • Developers using Doctrine can use Atlas to automatically plan schema migrations for them, based on the desired state of their schema instead of crafting them by hand.

Automatic migration planning for Doctrine

Doctrine is a popular ORM widely used in the PHP community, used by projects like Symfony. Doctrine allows users to manage their database schemas using its orm:schema-tool Console Command, which is usually sufficient during development and in many simple cases.

However, at some point, teams need more control and decide to employ a versioned migrations methodology. Doctrine has a migration tool that can be used to manage versioned migrations. Doctrine can also generate a migration for you by comparing the current state of your database schema to the state defined by using the ORM.

A downside of this approach is that in order for it to work, you must be connected to a pre-existing database with the current version of the schema. In many production environments, databases should generally not be reachable from developer workstations, which means this comparison is normally done against a local copy of the database which may have undergone some changes that aren't reflected in the existing migrations.

More of the limitations of the Doctrine migration tool include:

  • Ensuring Migration Safety. Migrations are a risky business. If you're not careful, you can easily cause data loss or a production outage. The Doctrine migration tool does not provide a native way to ensure that a migration is safe to apply.
  • Modern Deployments. Doctrine does not provide native integrations with modern deployment practices such as GitOps or Infrastructure-as-Code.

Using Atlas, you can enjoy automatic migration planning, automatic code review and integrations with your favorite CI/CD tools.

Atlas works in a similar way to the Doctrine migration tool, by calculating the diff between the current state of the database.

In the context of versioned migrations, the current state can be thought of as the database schema that would have been created by applying all previous migration scripts.

The desired schema of your application can be provided to Atlas via an External Schema Datasource, which is any program that can output a SQL schema definition to stdout.

To use Atlas with Doctrine, users can utilize the Doctrine Atlas Provider, which is a small program that loads the schema of a Doctrine project into Atlas.

In this guide, we will show how to use Atlas to automatically plan schema migrations for Doctrine users.

Prerequisites

If you don't have a Doctrine project, you can use symfony/demo as a starting point:

git clone git@github.com:symfony/demo.git

Using the Atlas Doctrine Provider

In this guide, we will use the Doctrine Atlas Provider to automatically plan schema migrations for a Doctrine project.

Installation

To download and install the latest release of the Atlas CLI, simply run the following in your terminal:

curl -sSf https://atlasgo.sh | sh

Install the provider, depending on the doctrine/dbal version you are using in the composer.json file:

composer require ariga/atlas-provider-doctrine:^4

Configuration

The Atlas Doctrine Provider can be used in three modes:

  • Doctrine Command - If all of your Doctrine entities exist under a single directory, you can add the atlas-provider command to the Doctrine Console file.
  • Symfony Bundle - If you use a Symfony project, you can register the provider as a Symfony bundle.
  • Script - In other cases, you can use the provider as a PHP script to load your Doctrine schema into Atlas.

Using the Doctrine Command

Add the atlas-provider command to the Doctrine Console file:

#!/usr/bin/env php
<?php

use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;

require 'bootstrap.php';
+ require "vendor/ariga/atlas-provider-doctrine/src/Command.php";

ConsoleRunner::run(
new SingleManagerProvider($entityManager),
+ [new AtlasCommand()]
);

Next, in your project directory, create a new file named atlas.hcl with the following contents:

data "external_schema" "doctrine" {
program = [
"php",
"bin/doctrine", // path to your Doctrine Console file
"atlas:schema",
"--path", "./path/to/entities",
"--dialect", "mysql" // mariadb | postgres | sqlite | sqlserver
]
}

env "doctrine" {
src = data.external_schema.doctrine.url
dev = "docker://mysql/8/dev"
migration {
dir = "file://migrations"
}
format {
migrate {
diff = "{{ sql . \" \" }}"
}
}
}

As Symfony Bundle

Add the following bundle to your config/bundles.php file:

<?php

require "vendor/autoload.php";

return [
...
+ Ariga\AtlasDoctrineBundle::class => ['all' => true],
];

Next, in your project directory, create a new file named atlas.hcl with the following contents:

data "external_schema" "doctrine" {
program = [
"php",
"bin/console",
"atlas:schema"
]
}

env "doctrine" {
src = data.external_schema.doctrine.url
dev = "docker://mysql/8/dev"
migration {
dir = "file://migrations"
}
format {
migrate {
diff = "{{ sql . \" \" }}"
}
}
}
note

The provider does not support loading entities via entity listeners. To load Symfony schema with entity listeners, use the Doctrine Bundle directly:

data "external_schema" "doctrine" {
program = [
"php",
"bin/console",
- "atlas:schema"
+ "doctrine:schema:create",
+ "--dump-sql"
]
}

As a PHP Script

Create a new file named atlas.php with the following contents:

<?php

require "vendor/autoload.php";
require "vendor/ariga/atlas-provider-doctrine/src/LoadEntities.php";

print (DumpDDL(["./path/to/first/entities", "./path/to/more/entities"], "mysql"));

Next, in your project directory, create a new file named atlas.hcl with the following contents:

data "external_schema" "doctrine" {
program = [
"php",
"atlas.php"
]
}

env "doctrine" {
src = data.external_schema.doctrine.url
dev = "docker://mysql/8/dev"
migration {
dir = "file://migrations"
}
format {
migrate {
diff = "{{ sql . \" \" }}"
}
}
}

Usage

Atlas supports a versioned migrations workflow, where each change to the database is versioned and recorded in a migration file. You can use the atlas migrate diff command to automatically generate a migration file that will migrate the database from its latest revision to the current Doctrine schema.

Suppose we have the following files in our src/entities directory:

<?php

namespace entities;

use Bug;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'users')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int|null $id = null;

#[ORM\Column(type: 'string')]
private string $name;

#[ORM\OneToMany(targetEntity: Bug::class, mappedBy: 'reporter')]
private Collection $reportedBugs;

#[ORM\OneToMany(targetEntity: Bug::class, mappedBy: 'engineer')]
private $assignedBugs;
}

We can generate a migration file by running the following command:

atlas migrate diff --env doctrine

Running this command will generate files in the migrations directory, similarly to:

migrations
|-- 20240310094824.sql
`-- atlas.sum

0 directories, 2 files

Examining the contents of the 20240310094824.sql:

-- Create "users" table
CREATE TABLE `users` (
`id` int NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- Create "bugs" table
CREATE TABLE `bugs` (
`id` int NOT NULL AUTO_INCREMENT,
`description` varchar(255) NOT NULL,
`created` datetime NOT NULL,
`status` varchar(255) NOT NULL,
`engineer_id` int NULL,
`reporter_id` int NULL,
PRIMARY KEY (`id`),
INDEX `IDX_1E197C9E1CFE6F5` (`reporter_id`),
INDEX `IDX_1E197C9F8D8CDF1` (`engineer_id`),
CONSTRAINT `FK_1E197C9E1CFE6F5` FOREIGN KEY (`reporter_id`) REFERENCES `users` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT `FK_1E197C9F8D8CDF1` FOREIGN KEY (`engineer_id`) REFERENCES `users` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION
) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

Amazing! Atlas automatically generated a migration file that will create the users and bugs tables in our database.

Next, alter the User entity to include a $name property.

    #[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int|null $id = null;

+ #[ORM\Column(type: 'string')]
+ private string $name;

Re-run this command:

atlas migrate diff --env doctrine

Observe that a new migration file is generated:

-- Modify "users" table
ALTER TABLE `users` ADD COLUMN `name` varchar(255) NOT NULL;

Conclusion

In this guide, we demonstrated how projects using Dcotrine can use Atlas to automatically plan schema migrations based only on their data model. To learn more about executing migrations against your production database, read the documentation about the migrate apply command.

Have questions? Feedback? Find our team on our Discord server