PHP

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

8 min de leitura

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

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

<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">&lt;?php

// migrations/Migration.php

abstract class Migration {

protected $pdo;

public function __construct(PDO $pdo) {

$this-&gt;pdo = $pdo;

}

abstract public function up();

abstract public function down();

protected function execute($sql) {

try {

return $this-&gt;pdo-&gt;exec($sql);

} catch (PDOException $e) {

throw new Exception(&quot;Erro ao executar migration: &quot; . $e-&gt;getMessage());

}

}

}

?&gt;</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">&lt;?php

// migrations/2024010101_criar_tabela_usuarios.php

class CriarTabelaUsuarios extends Migration {

public function up() {

$sql = &quot;

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;

&quot;;

$this-&gt;execute($sql);

echo &quot;✓ Tabela &#039;usuarios&#039; criada\n&quot;;

}

public function down() {

$this-&gt;execute(&quot;DROP TABLE IF EXISTS usuarios;&quot;);

echo &quot;✓ Tabela &#039;usuarios&#039; removida\n&quot;;

}

}

?&gt;</code></pre>

<h3>Exemplo 2: Modificando Estrutura</h3>

<pre><code class="language-php">&lt;?php

// migrations/2024010102_adicionar_campo_telefone.php

class AdicionarCampoTelefone extends Migration {

public function up() {

$sql = &quot;ALTER TABLE usuarios ADD COLUMN telefone VARCHAR(20) AFTER email;&quot;;

$this-&gt;execute($sql);

echo &quot;✓ Campo &#039;telefone&#039; adicionado\n&quot;;

}

public function down() {

$sql = &quot;ALTER TABLE usuarios DROP COLUMN telefone;&quot;;

$this-&gt;execute($sql);

echo &quot;✓ Campo &#039;telefone&#039; removido\n&quot;;

}

}

?&gt;</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">&lt;?php

// src/MigrationRunner.php

class MigrationRunner {

private $pdo;

private $migrationDir;

public function __construct(PDO $pdo, $migrationDir = &#039;migrations&#039;) {

$this-&gt;pdo = $pdo;

$this-&gt;migrationDir = $migrationDir;

$this-&gt;criarTabelaControle();

}

private function criarTabelaControle() {

$sql = &quot;

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;

&quot;;

$this-&gt;pdo-&gt;exec($sql);

}

public function executar() {

$arquivos = $this-&gt;obterMigrationsNaoExecutadas();

if (empty($arquivos)) {

echo &quot;Nenhuma migration para executar.\n&quot;;

return;

}

foreach ($arquivos as $arquivo) {

require_once $this-&gt;migrationDir . &#039;/&#039; . $arquivo;

$className = $this-&gt;extrairNomeClasse($arquivo);

$migration = new $className($this-&gt;pdo);

try {

$migration-&gt;up();

$this-&gt;registrarExecucao(basename($arquivo, &#039;.php&#039;));

echo &quot;✓ {$arquivo} executada com sucesso\n&quot;;

} catch (Exception $e) {

echo &quot;✗ Erro ao executar {$arquivo}: {$e-&gt;getMessage()}\n&quot;;

throw $e;

}

}

}

public function reverter($passos = 1) {

$stmt = $this-&gt;pdo-&gt;prepare(&quot;

SELECT migration FROM migrations

ORDER BY id DESC LIMIT :passos

&quot;);

$stmt-&gt;bindValue(&#039;:passos&#039;, $passos, PDO::PARAM_INT);

$stmt-&gt;execute();

$migrationsRevertidas = $stmt-&gt;fetchAll(PDO::FETCH_COLUMN);

foreach (array_reverse($migrationsRevertidas) as $migration) {

require_once $this-&gt;migrationDir . &#039;/&#039; . $migration . &#039;.php&#039;;

$className = $this-&gt;extrairNomeClasse($migration . &#039;.php&#039;);

$instance = new $className($this-&gt;pdo);

try {

$instance-&gt;down();

$this-&gt;removerRegistro($migration);

echo &quot;✓ {$migration} revertida\n&quot;;

} catch (Exception $e) {

echo &quot;✗ Erro ao reverter: {$e-&gt;getMessage()}\n&quot;;

}

}

}

private function obterMigrationsNaoExecutadas() {

$executadas = $this-&gt;pdo-&gt;query(

&quot;SELECT migration FROM migrations&quot;

)-&gt;fetchAll(PDO::FETCH_COLUMN);

$arquivos = array_filter(

scandir($this-&gt;migrationDir),

fn($f) =&gt; str_ends_with($f, &#039;.php&#039;)

);

return array_diff($arquivos, array_map(

fn($m) =&gt; $m . &#039;.php&#039;,

$executadas

));

}

private function registrarExecucao($migration) {

$stmt = $this-&gt;pdo-&gt;prepare(&quot;INSERT INTO migrations (migration) VALUES (?)&quot;);

$stmt-&gt;execute([$migration]);

}

private function removerRegistro($migration) {

$stmt = $this-&gt;pdo-&gt;prepare(&quot;DELETE FROM migrations WHERE migration = ?&quot;);

$stmt-&gt;execute([$migration]);

}

private function extrairNomeClasse($arquivo) {

$nome = str_replace(&#039;.php&#039;, &#039;&#039;, $arquivo);

return implode(&#039;&#039;, array_map(

&#039;ucfirst&#039;,

preg_split(&#039;/_/&#039;, preg_replace(&#039;/^\d+_/&#039;, &#039;&#039;, $nome))

));

}

}

?&gt;</code></pre>

<h3>Script de Execução</h3>

<pre><code class="language-php">&lt;?php

// bin/migrate.php

require &#039;src/MigrationRunner.php&#039;;

$pdo = new PDO(

&#039;mysql:host=localhost;dbname=seu_banco&#039;,

&#039;usuario&#039;,

&#039;senha&#039;

);

$runner = new MigrationRunner($pdo);

$comando = $argv[1] ?? &#039;up&#039;;

if ($comando === &#039;up&#039;) {

$runner-&gt;executar();

} elseif ($comando === &#039;down&#039;) {

$passos = intval($argv[2] ?? 1);

$runner-&gt;reverter($passos);

}

?&gt;</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>

Comentários

Mais em PHP

Middlewares em PHP: Interceptando o Ciclo de Requisição na Prática
Middlewares em PHP: Interceptando o Ciclo de Requisição na Prática

O Que São Middlewares e Por Que Importam Middlewares são funções ou classes q...

Guia Completo de Roteamento HTTP em PHP: Criando um Router Simples
Guia Completo de Roteamento HTTP em PHP: Criando um Router Simples

O que é Roteamento HTTP e Por Que Importa Roteamento HTTP é o mecanismo funda...

Dominando Cache em PHP: Redis, Memcached e Cache de Opcode com OPcache em Projetos Reais
Dominando Cache em PHP: Redis, Memcached e Cache de Opcode com OPcache em Projetos Reais

Cache em PHP: Redis, Memcached e OPcache O cache é um dos pilares da otimizaç...