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 andexecuting schema changes to your database.
- Developers using Doctrine can use Atlas to automatically plan schema migrationsfor 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 likeSymfony.Doctrine allows users to manage their database schemas using itsorm: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 aversioned migrations methodology.Doctrine has amigration tool that can be used to manage versioned migrations.Doctrine can alsogenerate a migrationfor 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 haveundergone 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 ora 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 asGitOpsorInfrastructure-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 thecurrent state of the database.
In the context ofversioned migrations,the current state can be thought of as the database schema that would havebeen created by applying all previous migration scripts.
The desired schema of your application can be provided to Atlas via anExternal Schema Datasource,which is any program that can output a SQL schema definition to stdout.
To use Atlas with Doctrine, users can utilize theDoctrine 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 localDoctrine project.
If you don't have a Doctrine project, you can usesymfony/demo as a starting point:
git clone git@github.com:symfony/demo.git
Using the Atlas Doctrine Provider
In this guide, we will use theDoctrine Atlas Providerto 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 withHomebrew:
brewinstall 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 desireddirectory:
docker run--rm--net=host\
-v$(pwd)/migrations:/migrations\
arigaio/atlas migrate apply
--url"mysql://root:pass@:3306/test"
Download thelatest release andmove the atlas binary to a file location on your system PATH.
Install the provider, depending on thedoctrine/dbal
version you are using in thecomposer.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 theDoctrine Console file. - Symfony Bundle - If you use aSymfony 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 theatlas-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 namedatlas.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 yourconfig/bundles.php
file:
<?php
require "vendor/autoload.php";
return [
...
+ Ariga\AtlasDoctrineBundle::class => ['all' => true],
];
Next, in your project directory, create a new file namedatlas.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 viaentity listeners.To load Symfony schema with entity listeners, use theDoctrine 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 namedatlas.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 namedatlas.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 theatlas migrate diff
command to automatically generate a migration file that will migrate the databasefrom its latest revision to the current Doctrine schema.
Suppose we have the following files in oursrc/entities
directory:
- User.php
- Bug.php
<?php
namespaceentities;
useBug;
useDoctrine\Common\Collections\Collection;
useDoctrine\ORM\MappingasORM;
#[ORM\Entity]
#[ORM\Table(name:'users')]
classUser
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type:'integer')]
privateint|null$id=null;
#[ORM\Column(type:'string')]
privatestring$name;
#[ORM\OneToMany(targetEntity:Bug::class,mappedBy:'reporter')]
privateCollection$reportedBugs;
#[ORM\OneToMany(targetEntity:Bug::class,mappedBy:'engineer')]
private$assignedBugs;
}
<?php
namespaceentities;
useDoctrine\ORM\MappingasORM;
useUser;
#[ORM\Entity]
#[ORM\Table(name:'bugs')]
classBug
{
#[ORM\Id]
#[ORM\Column(type:'integer')]
#[ORM\GeneratedValue]
privateint|null$id=null;
#[ORM\Column(type:'string')]
privatestring$description;
#[ORM\Column(type:'datetime')]
privateDateTime$created;
#[ORM\Column(type:'string')]
privatestring$status;
#[ORM\ManyToOne(targetEntity:User::class,inversedBy:'assignedBugs')]
privateUser|null$engineer=null;
#[ORM\ManyToOne(targetEntity:User::class,inversedBy:'reportedBugs')]
privateUser|null$reporter;
}
We can generate a migration file by running the following command:
atlas migratediff--env doctrine
Running this command will generate files in themigrations
directory, similarly to:
migrations
|-- 20240310094824.sql
`-- atlas.sum
0 directories, 2 files
Examining the contents of the20240310094824.sql
:
-- Create "users" table
CREATETABLE`users`(
`id`intNOTNULLAUTO_INCREMENT,
PRIMARYKEY(`id`)
)CHARSET utf8mb4COLLATE utf8mb4_0900_ai_ci;
-- Create "bugs" table
CREATETABLE`bugs`(
`id`intNOTNULLAUTO_INCREMENT,
`description`varchar(255)NOTNULL,
`created`datetimeNOTNULL,
`status`varchar(255)NOTNULL,
`engineer_id`intNULL,
`reporter_id`intNULL,
PRIMARYKEY(`id`),
INDEX`IDX_1E197C9E1CFE6F5`(`reporter_id`),
INDEX`IDX_1E197C9F8D8CDF1`(`engineer_id`),
CONSTRAINT`FK_1E197C9E1CFE6F5`FOREIGNKEY(`reporter_id`)REFERENCES`users`(`id`)ONUPDATENOACTIONONDELETENOACTION,
CONSTRAINT`FK_1E197C9F8D8CDF1`FOREIGNKEY(`engineer_id`)REFERENCES`users`(`id`)ONUPDATENOACTIONONDELETENOACTION
)CHARSET utf8mb4COLLATE utf8mb4_0900_ai_ci;
Amazing! Atlas automatically generated a migration file that will create theusers
andbugs
tables in our database.
Next, alter theUser
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 migratediff--env doctrine
Observe that a new migration file is generated:
-- Modify "users" table
ALTERTABLE`users`ADDCOLUMN`name`varchar(255)NOTNULL;
Conclusion
In this guide, we demonstrated how projects using Dcotrine can use Atlas to automaticallyplan schema migrations based only on their data model. To learn more about executingmigrations against your production database, read the documentation about themigrate apply
command.
Have questions? Feedback? Find our teamon our Discord server