Migrations com Doctrine Migrations
1. Introdução às Migrations no Doctrine
Migrations são uma forma controlada e versionada de gerenciar alterações no esquema do banco de dados ao longo do tempo. Em vez de executar scripts SQL manualmente ou depender de sincronização entre ambientes, as migrations permitem que cada alteração seja registrada como uma unidade atômica, com capacidade de avanço (up) e reversão (down).
No ecossistema PHP, o Doctrine Migrations é a biblioteca mais madura e amplamente adotada para esse propósito. Ela se integra perfeitamente com o Doctrine ORM e o Doctrine DBAL, oferecendo uma API rica para manipulação de esquemas de forma programática.
Problemas que as migrations resolvem:
- Versionamento do schema do banco de dados
- Colaboração em equipe sem conflitos de estrutura
- Deploy consistente entre ambientes (dev, staging, produção)
- Capacidade de reverter alterações problemáticas
- Rastreabilidade de todas as mudanças estruturais
2. Instalação e Configuração Inicial
Para começar, instale o pacote via Composer:
composer require doctrine/migrations
Crie um arquivo de configuração migrations.php na raiz do projeto:
<?php
return [
'table_storage' => [
'table_name' => 'doctrine_migration_versions',
'version_column_name' => 'version',
'version_column_length' => 191,
'executed_at_column_name' => 'executed_at',
'execution_time_column_name' => 'execution_time',
],
'migrations_paths' => [
'App\Migrations' => __DIR__ . '/migrations',
],
'all_or_nothing' => true,
'transactional' => true,
'check_database_platform' => true,
'organize_migrations' => 'by_year_and_month',
];
Configure a conexão com o banco de dados. Se estiver usando Doctrine ORM:
<?php
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMSetup;
use Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager;
use Doctrine\Migrations\DependencyFactory;
$config = ORMSetup::createAttributeMetadataConfiguration(
paths: [__DIR__ . '/src/Entity'],
isDevMode: true
);
$connection = [
'driver' => 'pdo_mysql',
'host' => 'localhost',
'dbname' => 'meu_banco',
'user' => 'root',
'password' => 'senha',
];
$entityManager = EntityManager::create($connection, $config);
return DependencyFactory::fromEntityManager(
new ExistingEntityManager($entityManager)
);
Estrutura de diretórios recomendada:
projeto/
├── migrations/
│ └── Version20240101000000.php
├── src/
│ └── Entity/
├── vendor/
├── migrations.php
└── composer.json
3. Criando e Executando Migrations
Para gerar uma nova migration vazia:
vendor/bin/doctrine migrations:generate
Isso criará um arquivo como Version20240101000000.php no diretório migrations/. A estrutura básica de uma migration:
<?php
declare(strict_types=1);
namespace App\Migrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20240101000000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Cria a tabela de usuários';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE users (
id INT AUTO_INCREMENT NOT NULL,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE users');
}
}
Comandos essenciais:
# Executar todas as migrations pendentes
vendor/bin/doctrine migrations:migrate
# Executar uma migration específica
vendor/bin/doctrine migrations:execute Version20240101000000 --up
# Reverter uma migration
vendor/bin/doctrine migrations:execute Version20240101000000 --down
# Verificar status das migrations
vendor/bin/doctrine migrations:status
# Listar todas as migrations
vendor/bin/doctrine migrations:list
4. Operações Avançadas com Schema
O Doctrine Migrations permite manipular o schema programaticamente usando objetos Schema e Table:
<?php
public function up(Schema $schema): void
{
// Criar tabela usando Schema API
$table = $schema->createTable('products');
$table->addColumn('id', 'integer', ['autoincrement' => true]);
$table->addColumn('name', 'string', ['length' => 100]);
$table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]);
$table->addColumn('category_id', 'integer', ['notnull' => false]);
$table->setPrimaryKey(['id']);
$table->addIndex(['name'], 'idx_product_name');
$table->addForeignKeyConstraint('categories', ['category_id'], ['id']);
}
public function down(Schema $schema): void
{
$schema->dropTable('products');
}
Alterações em tabelas existentes:
<?php
public function up(Schema $schema): void
{
$table = $schema->getTable('users');
// Adicionar coluna
$table->addColumn('phone', 'string', ['length' => 20, 'notnull' => false]);
// Renomear coluna
$table->changeColumn('email', ['length' => 191]);
// Remover coluna
$table->dropColumn('old_field');
// Adicionar índice
$table->addIndex(['email'], 'idx_users_email');
}
public function down(Schema $schema): void
{
$table = $schema->getTable('users');
$table->dropColumn('phone');
$table->dropIndex('idx_users_email');
}
Migrations seguras com verificação:
<?php
public function up(Schema $schema): void
{
if (!$schema->hasTable('users')) {
$table = $schema->createTable('users');
// ...
}
$table = $schema->getTable('users');
if (!$table->hasColumn('phone')) {
$table->addColumn('phone', 'string', ['length' => 20]);
}
}
5. Versionamento e Controle de Migrations
O Doctrine rastreia automaticamente as migrations executadas na tabela doctrine_migration_versions. Cada execução registra:
- version: Nome da classe da migration
- executed_at: Timestamp da execução
- execution_time: Tempo gasto na execução
Gerenciamento manual:
# Marcar migration como executada sem executá-la
vendor/bin/doctrine migrations:version Version20240101000000 --add
# Desmarcar migration como executada
vendor/bin/doctrine migrations:version Version20240101000000 --delete
Boas práticas de versionamento:
- Ordenação por data/hora: Use prefixos com timestamp (ex:
Version20240101120000) - Nomes descritivos: Adicione o método
getDescription()com uma descrição clara - Atomicidade: Cada migration deve representar uma única mudança lógica
- Commits frequentes: Faça commit das migrations junto com as alterações de código
Resolução de conflitos em equipe:
- Use rebase para manter a ordem cronológica
- Em caso de conflito, resolva manualmente mantendo a sequência correta
- Evite modificar migrations já executadas em produção
6. Migrations em Diferentes Ambientes
Estratégia para múltiplos ambientes:
<?php
// migrations.php
$env = getenv('APP_ENV') ?: 'dev';
$config = [
'table_storage' => [
'table_name' => 'doctrine_migration_versions',
],
'migrations_paths' => [
'App\Migrations' => __DIR__ . '/migrations',
],
'all_or_nothing' => true,
];
// Configurações específicas por ambiente
if ($env === 'production') {
$config['transactional'] = true;
$config['check_database_platform'] = true;
}
return $config;
Integração com CI/CD (GitHub Actions):
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install --no-dev
- run: |
php vendor/bin/doctrine migrations:migrate --no-interaction
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Rollback seguro em produção:
# Reverter apenas uma migration específica
vendor/bin/doctrine migrations:execute Version20240101000000 --down
# Reverter até uma versão específica
vendor/bin/doctrine migrations:migrate Version20231201000000
# Reverter a última migration
vendor/bin/doctrine migrations:migrate prev
7. Boas Práticas e Troubleshooting
Migrations idempotentes e reversíveis:
<?php
public function up(Schema $schema): void
{
// Verificar antes de criar
if (!$schema->hasTable('logs')) {
$table = $schema->createTable('logs');
$table->addColumn('id', 'integer', ['autoincrement' => true]);
$table->addColumn('message', 'text');
$table->setPrimaryKey(['id']);
}
}
public function down(Schema $schema): void
{
// Nunca dropar tabelas sem verificação
if ($schema->hasTable('logs')) {
$schema->dropTable('logs');
}
}
Evitando alterações destrutivas em produção:
- Nunca use
DROP TABLEsem verificar a existência - Prefira
ALTER TABLEcom verificações de segurança - Use
--dry-runpara simular antes de executar - Sempre faça backup antes de migrations em produção
Problemas comuns e soluções:
| Problema | Causa | Solução |
|---|---|---|
| Conflito de versão | Migrations com mesmo timestamp | Renomear arquivo |
| Timeout | Migration muito longa | Aumentar max_execution_time |
| Lock no banco | Tabela bloqueada | Usar --lock-timeout |
| Erro de sintaxe | SQL inválido | Validar com --dry-run |
Ferramentas complementares:
Para projetos Symfony, o bundle oficial simplifica a integração:
composer require doctrine/doctrine-migrations-bundle
Configuração no doctrine_migrations.yaml:
doctrine_migrations:
migrations_paths:
'App\Migrations': '%kernel.project_dir%/migrations'
storage:
table_storage:
table_name: 'doctrine_migration_versions'
organize_migrations: by_year_and_month
Referências
- Documentação Oficial do Doctrine Migrations — Guia completo de instalação, configuração e uso da biblioteca
- Doctrine Migrations Bundle para Symfony — Documentação oficial de integração com Symfony
- Doctrine DBAL Schema Manager — Referência sobre manipulação de esquemas com DBAL
- PHP The Right Way - Migrations — Artigo sobre boas práticas de migrations em PHP
- Migrations Avançadas com Doctrine — Tutorial avançado sobre operações complexas com Schema API
- Integração Contínua com Doctrine Migrations — Action do GitHub para automatizar migrations em CI/CD
- Doctrine Migrations no Stack Overflow — Respostas para problemas comuns da comunidade