Migrations Manuais em PHP: Versionando o Banco de Dados na Prática Já leu

O que são Migrations e por que você precisa delas Migrations são controle de versão para seu banco de dados. Assim como você usa Git para rastrear mudanças no código, migrations rastreiam alterações na estrutura de dados — criação de tabelas, adição de colunas, índices, mudanças de tipos. Em PHP, especialmente em projetos sem frameworks robustos, implementar migrations manuais te dá controle total sobre quando e como suas mudanças de schema são aplicadas. O principal benefício é garantir que todos os ambientes (desenvolvimento, staging, produção) tenham exatamente a mesma estrutura de banco. Sem migrations, mudanças no schema viram emails ou documentos perdidos, causando inconsistências e bugs difíceis de rastrear. Estrutura de um Sistema de Migrations Manual Organização de Arquivos Comece criando um diretório na raiz do seu projeto. Cada arquivo de migration deve seguir um padrão de versionamento: timestamp ou número sequencial. O timestamp é superior pois evita conflitos em times distribuídos. Criando a Classe Base de Migration Toda migration

O que são Migrations e por que você precisa delas

Migrations são controle de versão para seu banco de dados. Assim como você usa Git para rastrear mudanças no código, migrations rastreiam alterações na estrutura de dados — criação de tabelas, adição de colunas, índices, mudanças de tipos. Em PHP, especialmente em projetos sem frameworks robustos, implementar migrations manuais te dá controle total sobre quando e como suas mudanças de schema são aplicadas.

O principal benefício é garantir que todos os ambientes (desenvolvimento, staging, produção) tenham exatamente a mesma estrutura de banco. Sem migrations, mudanças no schema viram emails ou documentos perdidos, causando inconsistências e bugs difíceis de rastrear.

Estrutura de um Sistema de Migrations Manual

Organização de Arquivos

Comece criando um diretório migrations/ na raiz do seu projeto. Cada arquivo de migration deve seguir um padrão de versionamento: timestamp ou número sequencial. O timestamp é superior pois evita conflitos em times distribuídos.

projeto/
├── migrations/
│   ├── 2024010101_criar_tabela_usuarios.php
│   ├── 2024010102_adicionar_email_usuarios.php
│   └── 2024010103_criar_tabela_posts.php
├── src/
└── config/

Criando a Classe Base de Migration

Toda migration deve herdar de uma classe base que define o contrato entre seu sistema e o banco:

<?php
// migrations/Migration.php

abstract class Migration {
    protected $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    abstract public function up();
    abstract public function down();

    protected function execute($sql) {
        try {
            return $this->pdo->exec($sql);
        } catch (PDOException $e) {
            throw new Exception("Erro ao executar migration: " . $e->getMessage());
        }
    }
}
?>

Os métodos up() e down() são cruciais: up() aplica a mudança, down() a reverte. Isso permite rollback seguro.

Implementando Migrations Práticas

Exemplo 1: Criando uma Tabela

<?php
// migrations/2024010101_criar_tabela_usuarios.php

class CriarTabelaUsuarios extends Migration {
    public function up() {
        $sql = "
            CREATE TABLE usuarios (
                id INT PRIMARY KEY AUTO_INCREMENT,
                nome VARCHAR(255) NOT NULL,
                email VARCHAR(255) UNIQUE NOT NULL,
                senha VARCHAR(255) NOT NULL,
                criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                atualizado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
        ";
        $this->execute($sql);
        echo "✓ Tabela 'usuarios' criada\n";
    }

    public function down() {
        $this->execute("DROP TABLE IF EXISTS usuarios;");
        echo "✓ Tabela 'usuarios' removida\n";
    }
}
?>

Exemplo 2: Modificando Estrutura

<?php
// migrations/2024010102_adicionar_campo_telefone.php

class AdicionarCampoTelefone extends Migration {
    public function up() {
        $sql = "ALTER TABLE usuarios ADD COLUMN telefone VARCHAR(20) AFTER email;";
        $this->execute($sql);
        echo "✓ Campo 'telefone' adicionado\n";
    }

    public function down() {
        $sql = "ALTER TABLE usuarios DROP COLUMN telefone;";
        $this->execute($sql);
        echo "✓ Campo 'telefone' removido\n";
    }
}
?>

Sistema de Execução e Rastreamento

Tabela de Controle

Você precisa de uma tabela para registrar quais migrations foram aplicadas:

<?php
// src/MigrationRunner.php

class MigrationRunner {
    private $pdo;
    private $migrationDir;

    public function __construct(PDO $pdo, $migrationDir = 'migrations') {
        $this->pdo = $pdo;
        $this->migrationDir = $migrationDir;
        $this->criarTabelaControle();
    }

    private function criarTabelaControle() {
        $sql = "
            CREATE TABLE IF NOT EXISTS migrations (
                id INT PRIMARY KEY AUTO_INCREMENT,
                migration VARCHAR(255) UNIQUE NOT NULL,
                executada_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
        ";
        $this->pdo->exec($sql);
    }

    public function executar() {
        $arquivos = $this->obterMigrationsNaoExecutadas();

        if (empty($arquivos)) {
            echo "Nenhuma migration para executar.\n";
            return;
        }

        foreach ($arquivos as $arquivo) {
            require_once $this->migrationDir . '/' . $arquivo;
            $className = $this->extrairNomeClasse($arquivo);
            $migration = new $className($this->pdo);

            try {
                $migration->up();
                $this->registrarExecucao(basename($arquivo, '.php'));
                echo "✓ {$arquivo} executada com sucesso\n";
            } catch (Exception $e) {
                echo "✗ Erro ao executar {$arquivo}: {$e->getMessage()}\n";
                throw $e;
            }
        }
    }

    public function reverter($passos = 1) {
        $stmt = $this->pdo->prepare("
            SELECT migration FROM migrations 
            ORDER BY id DESC LIMIT :passos
        ");
        $stmt->bindValue(':passos', $passos, PDO::PARAM_INT);
        $stmt->execute();
        $migrationsRevertidas = $stmt->fetchAll(PDO::FETCH_COLUMN);

        foreach (array_reverse($migrationsRevertidas) as $migration) {
            require_once $this->migrationDir . '/' . $migration . '.php';
            $className = $this->extrairNomeClasse($migration . '.php');
            $instance = new $className($this->pdo);

            try {
                $instance->down();
                $this->removerRegistro($migration);
                echo "✓ {$migration} revertida\n";
            } catch (Exception $e) {
                echo "✗ Erro ao reverter: {$e->getMessage()}\n";
            }
        }
    }

    private function obterMigrationsNaoExecutadas() {
        $executadas = $this->pdo->query(
            "SELECT migration FROM migrations"
        )->fetchAll(PDO::FETCH_COLUMN);

        $arquivos = array_filter(
            scandir($this->migrationDir),
            fn($f) => str_ends_with($f, '.php')
        );

        return array_diff($arquivos, array_map(
            fn($m) => $m . '.php',
            $executadas
        ));
    }

    private function registrarExecucao($migration) {
        $stmt = $this->pdo->prepare("INSERT INTO migrations (migration) VALUES (?)");
        $stmt->execute([$migration]);
    }

    private function removerRegistro($migration) {
        $stmt = $this->pdo->prepare("DELETE FROM migrations WHERE migration = ?");
        $stmt->execute([$migration]);
    }

    private function extrairNomeClasse($arquivo) {
        $nome = str_replace('.php', '', $arquivo);
        return implode('', array_map(
            'ucfirst',
            preg_split('/_/', preg_replace('/^\d+_/', '', $nome))
        ));
    }
}
?>

Script de Execução

<?php
// bin/migrate.php

require 'src/MigrationRunner.php';

$pdo = new PDO(
    'mysql:host=localhost;dbname=seu_banco',
    'usuario',
    'senha'
);

$runner = new MigrationRunner($pdo);

$comando = $argv[1] ?? 'up';

if ($comando === 'up') {
    $runner->executar();
} elseif ($comando === 'down') {
    $passos = intval($argv[2] ?? 1);
    $runner->reverter($passos);
}
?>

Use assim: php bin/migrate.php up ou php bin/migrate.php down 2

Conclusão

Migrations manuais em PHP oferecem controle explícito e rastreabilidade sobre mudanças no banco de dados. Implementar um sistema robusto envolve: estrutura clara de arquivos, classe base com contrato up()/down(), e um runner que registra execuções. Com isso, seu time trabalha sincronizado e você consegue reverter problemas rapidamente. Para projetos maiores, considere ferramentas como Doctrine ou Phinx, mas dominar o conceito manual torna você um desenvolvedor muito mais competente.

Referências


Artigos relacionados