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
- A local Doctrine project.
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
- macOS + Linux
- Homebrew
- Docker
- Windows
- Manual 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
Get the latest release with Homebrew:
brew install ariga/tap/atlas
To pull the Atlas image and run it as a Docker container:
docker pull arigaio/atlas
docker run --rm arigaio/atlas --help
If the container needs access to the host network or a local directory, use the --net=host
flag and mount the desired
directory:
docker run --rm --net=host \
-v $(pwd)/migrations:/migrations \
arigaio/atlas migrate apply
--url "mysql://root:pass@:3306/test"
Download the latest release and move the atlas binary to a file location on your system PATH.
Install the provider, depending on the doctrine/dbal
version you are using in the composer.json
file:
- Doctrine DBAL v4
- Dcotrine DBAL v3
composer require ariga/atlas-provider-doctrine:^4
composer require ariga/atlas-provider-doctrine:^3
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 . \" \" }}"
}
}
}
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:
- User.php
- Bug.php
<?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;
}
<?php
namespace entities;
use Doctrine\ORM\Mapping as ORM;
use User;
#[ORM\Entity]
#[ORM\Table(name: 'bugs')]
class Bug
{
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue]
private int|null $id = null;
#[ORM\Column(type: 'string')]
private string $description;
#[ORM\Column(type: 'datetime')]
private DateTime $created;
#[ORM\Column(type: 'string')]
private string $status;
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'assignedBugs')]
private User|null $engineer = null;
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'reportedBugs')]
private User|null $reporter;
}
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