<h2>O que são Migrations e por que você precisa delas</h2>
<p>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.</p>
<p>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.</p>
<h2>Estrutura de um Sistema de Migrations Manual</h2>
<h3>Organização de Arquivos</h3>
<p>Comece criando um diretório <code>migrations/</code> 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.</p>
<pre><code>projeto/
├── migrations/
│ ├── 2024010101_criar_tabela_usuarios.php
│ ├── 2024010102_adicionar_email_usuarios.php
│ └── 2024010103_criar_tabela_posts.php
├── src/
└── config/</code></pre>
<h3>Criando a Classe Base de Migration</h3>
<p>Toda migration deve herdar de uma classe base que define o contrato entre seu sistema e o banco:</p>
<pre><code class="language-php"><?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());
}
}
}
?></code></pre>
<p>Os métodos <code>up()</code> e <code>down()</code> são cruciais: <code>up()</code> aplica a mudança, <code>down()</code> a reverte. Isso permite rollback seguro.</p>
<h2>Implementando Migrations Práticas</h2>
<h3>Exemplo 1: Criando uma Tabela</h3>
<pre><code class="language-php"><?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";
}
}
?></code></pre>
<h3>Exemplo 2: Modificando Estrutura</h3>
<pre><code class="language-php"><?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";
}
}
?></code></pre>
<h2>Sistema de Execução e Rastreamento</h2>
<h3>Tabela de Controle</h3>
<p>Você precisa de uma tabela para registrar quais migrations foram aplicadas:</p>
<pre><code class="language-php"><?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))
));
}
}
?></code></pre>
<h3>Script de Execução</h3>
<pre><code class="language-php"><?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);
}
?></code></pre>
<p>Use assim: <code>php bin/migrate.php up</code> ou <code>php bin/migrate.php down 2</code></p>
<h2>Conclusão</h2>
<p>Migrations manuais em PHP oferecem <strong>controle explícito e rastreabilidade</strong> sobre mudanças no banco de dados. Implementar um sistema robusto envolve: estrutura clara de arquivos, classe base com contrato <code>up()</code>/<code>down()</code>, 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.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://www.php.net/manual/en/book.pdo.php" target="_blank" rel="noopener noreferrer">PDO Documentation - PHP Manual</a></li>
<li><a href="https://phinx.org/" target="_blank" rel="noopener noreferrer">Phinx - Database Migrations</a></li>
<li><a href="https://www.doctrine-project.org/projects/doctrine-migrations.html" target="_blank" rel="noopener noreferrer">Doctrine Migrations</a></li>
<li><a href="https://martinfowler.com/articles/evodb.html" target="_blank" rel="noopener noreferrer">Database Versioning Best Practices - Martin Fowler</a></li>
</ul>