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:

  1. Ordenação por data/hora: Use prefixos com timestamp (ex: Version20240101120000)
  2. Nomes descritivos: Adicione o método getDescription() com uma descrição clara
  3. Atomicidade: Cada migration deve representar uma única mudança lógica
  4. 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 TABLE sem verificar a existência
  • Prefira ALTER TABLE com verificações de segurança
  • Use --dry-run para 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