Migrations e Schema
Criar e alterar schema com migrations PHP: Blueprint, Column, índices, chaves estrangeiras e relações entre tabelas.
Formato do arquivo
Arquivos database/migrations/*.php são ordenados pelo nome e retornam uma instância anônima de Framework\Database\Migration. Implemente up() e down().
use Framework\Database\Migration;
use Framework\Database\Schema;
use Framework\Database\Blueprint;
return new class extends Migration {
public function up(): void
{
Schema::create('posts', function (Blueprint $table): void {
$table->id();
$table->string('title', 150);
$table->text('body')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
Schema e Blueprint
Use Schema e Blueprint para definir DDL em migrations. O guia completo de Blueprint abaixo cobre criação e alteração de tabelas, todos os tipos/modificadores, índices, FKs, relações 1:1/1:N/N:1/N:N, SQL gerado, execução e limitações da implementação.
Guia completo de Blueprint e definição de schema
Esta seção explica a API real de Framework\Database\Blueprint, Column e Schema, do primeiro arquivo de migration até relacionamentos e SQL gerado. O código entregue configura MySQL/InnoDB; não há suporte SQLite nesta conexão. Exemplos de tabelas como posts e profiles são modelos didáticos que você pode criar, não classes ou tabelas preexistentes.
1. Como Blueprint funciona dentro do framework
Há dois fluxos distintos. No fluxo de schema, a migration chama Schema; a facade cria um Blueprint, o callback registra objetos Column e índices, e o framework monta SQL DDL e o entrega a Database::execMigration(). No uso cotidiano, Models consultam e alteram as tabelas já criadas.
Migrations/arquivo.php → Migration::up()
→ Schema::create() ou Schema::table()
→ Blueprint + Column / índices / FKs
→ SQL DDL → Database::execMigration() → MySQL/InnoDB
Aplicação em execução: Model ou Database → SQL de leitura/escrita → tabelas
Blueprint não executa queries de domínio nem insere registros. Use Models ou Database/PDO para dados; use este builder para a estrutura do banco. O Migrator da CLI carrega apenas arquivos .php em database/migrations/, ordenados pelo nome.
2. Criar uma tabela do zero: exemplo iniciante
Cenário e objetivo. Você está começando uma pequena aplicação de catálogo e precisa guardar produtos com nome, preço e datas de auditoria. Vamos criar uma migration reversível, entender cada arquivo e verificar o resultado. O nome e as colunas abaixo são exemplos; os métodos usados existem em Blueprint.php.
- Configure as variáveis do banco em
.env(por exemploDB_HOST,DB_PORT,DB_DATABASE,DB_USERNAMEeDB_PASSWORD). A conexão ativa está emconfig/database.php; no código atual é MySQL. - Na raiz do projeto, gere o esqueleto:
php mvc make:migration CreateProductsTable. O comando cria um arquivo com prefixo de data/hora emdatabase/migrations/. O stub inicial usaSchema::create(); revise e complete o conteúdo antes de executar. - Edite o arquivo gerado para que tenha esta forma:
<?php
use Framework\Database\Blueprint;
use Framework\Database\Migration;
use Framework\Database\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('products', function (Blueprint $table): void {
$table->id();
$table->string('name', 150);
$table->decimal('price', 10, 2);
$table->boolean('active')->default(1);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
};
O que foi feito. O arquivo retorna uma instância anônima de Migration, com up() para aplicar e down() para desfazer. Schema::create() recebe o nome literal da tabela e um callback com Blueprint. id() cria um INT UNSIGNED AUTO_INCREMENT PRIMARY KEY; string(), decimal() e boolean() registram colunas; timestamps() cria created_at e updated_at.
Resultado esperado. Ao chamar php mvc migrate, o Migrator carrega o arquivo, chama up() e registra a migration e seu batch na tabela migrations. Para esta tabela, Blueprint produz um CREATE TABLE IF NOT EXISTS InnoDB com charset utf8mb4 e collation utf8mb4_unicode_ci. O SQL exato depende das colunas declaradas; mais adiante há uma saída real de exemplo.
migrate conecta e executa DDL de verdade. Faça o primeiro teste em um banco local/de desenvolvimento com backup adequado. CREATE TABLE IF NOT EXISTS não atualiza uma tabela que já existe: se a tabela estiver presente, o comando não converte a estrutura antiga para a nova.3. Alterar uma tabela existente
Cenário e objetivo. A tabela products já existe e agora precisa de um SKU opcional. Para alteração usa-se Schema::table(); o callback ainda recebe um Blueprint, mas o framework gera uma sequência de ALTER TABLE, não um CREATE TABLE.
<?php
use Framework\Database\Blueprint;
use Framework\Database\Migration;
use Framework\Database\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('products', function (Blueprint $table): void {
$table->string('sku', 64)->nullable();
});
}
public function down(): void
{
Schema::table('products', function (Blueprint $table): void {
$table->dropColumn('sku');
});
}
};
Como funciona. Schema::table() compila as operações e executa cada statement em sequência, imediatamente. No exemplo, up() adiciona sku VARCHAR(64) NULL; o rollback remove a coluna. A migration só é registrada pelo Migrator depois que up() termina. Em MySQL, DDL pode fazer commit implícito, então uma falha no meio pode deixar parte das alterações aplicada.
dropColumn(string|array $columns) aceita um nome ou uma lista e gera um ALTER TABLE ... DROP COLUMN por coluna. Para desfazer uma remoção, down() pode recriar a coluna com tipo e nulabilidade equivalentes, mas não recupera os valores apagados. Faça backup antes de migrations destrutivas.
change(), renameColumn() ou dropForeign(); essas APIs não existem neste Blueprint.4. Catálogo completo de tipos de coluna
Todos os métodos abaixo pertencem a Blueprint. Salvo os métodos de conveniência indicados depois, cada método de coluna retorna Framework\Database\Column, permitindo encadear os modificadores descritos na próxima seção. Os tamanhos/valores são colocados no SQL; Blueprint não verifica se cabem nos limites do tipo nem se precisão e escala são coerentes.
Números
| Método e assinatura | SQL gerado | Quando usar / observações |
|---|---|---|
tinyInteger(string $name): Column | TINYINT | Inteiro pequeno com sinal. |
unsignedTinyInteger(string $name): Column | TINYINT UNSIGNED | Faixa não negativa de TINYINT no MySQL. |
smallInteger(string $name): Column | SMALLINT | Inteiro pequeno/médio com sinal. |
unsignedSmallInteger(string $name): Column | SMALLINT UNSIGNED | Contagens não negativas que cabem no tipo. |
mediumInteger(string $name): Column | MEDIUMINT | Inteiro MySQL de faixa intermediária. |
unsignedMediumInteger(string $name): Column | MEDIUMINT UNSIGNED | Versão sem sinal do MEDIUMINT. |
integer(string $name): Column | INT | Inteiro de uso geral com sinal. |
unsignedInteger(string $name): Column | INT UNSIGNED | Inteiro não negativo; id() e foreignId() usam esta largura. |
bigInteger(string $name): Column | BIGINT | Inteiro de faixa maior com sinal. |
unsignedBigInteger(string $name): Column | BIGINT UNSIGNED | Inteiro grande não negativo. Não é o tipo produzido por foreignId(). |
decimal(string $name, int $precision = 10, int $scale = 2): Column | DECIMAL(p,s) | Precisão fixa, apropriada para valores financeiros com escala conhecida; defina p/s conscientemente. |
float(string $name, int $precision = 8, int $scale = 2): Column | FLOAT(p,s) | Medidas aproximadas; evite para dinheiro. O formato de precisão/escala depende da versão/configuração MySQL. |
double(string $name, int $precision = 8, int $scale = 2): Column | DOUBLE(p,s) | Ponto flutuante aproximado de precisão maior que FLOAT; também não é decimal exato. |
Texto
| Método e assinatura | SQL gerado | Quando usar / observações |
|---|---|---|
char(string $name, int $length = 1): Column | CHAR(n) | Texto de tamanho fixo, como uma sigla curta. |
string(string $name, int $length = 255): Column | VARCHAR(n) | Texto curto/variável, como nome, título ou e-mail. |
text(string $name): Column | TEXT | Texto longo; capacidade é regida pelo tipo MySQL e charset. |
mediumText(string $name): Column | MEDIUMTEXT | Conteúdo mais extenso, conforme limites do MySQL. |
longText(string $name): Column | LONGTEXT | Conteúdo muito grande; não escolha sem necessidade. |
Data e hora
| Método e assinatura | SQL gerado | Quando usar / observações |
|---|---|---|
date(string $name): Column | DATE | Data sem hora. |
time(string $name): Column | TIME | Horário/duração conforme semântica do MySQL. |
year(string $name): Column | YEAR | Tipo YEAR específico do MySQL. |
dateTime(string $name): Column | DATETIME | Data/hora; não adiciona fuso horário. |
timestamp(string $name): Column | TIMESTAMP | Timestamp do MySQL, sujeito às regras e faixa da versão do servidor. |
Booleanos e tipos especiais
| Método e assinatura | SQL gerado | Quando usar / observações |
|---|---|---|
boolean(string $name): Column | TINYINT(1) | Convenção booleana MySQL; o builder não cria CHECK constraint nem converte/valida automaticamente o valor. |
json(string $name): Column | JSON | Requer versão do MySQL com suporte ao tipo JSON. |
enum(string $name, array $values): Column | ENUM('a','b',...) | Lista fechada codificada no schema. Os itens são interpolados sem escaping; use apenas valores literais controlados. |
uuid(string $name): Column | CHAR(36) | Armazena uma string UUID; não gera UUID, não a torna chave primária e não adiciona índice/unique. |
id(string $name = 'id'): Column | INT UNSIGNED AUTO_INCREMENT PRIMARY KEY | Convenção de chave primária auto-incrementável; não é BIGINT nem UUID. |
foreignId(string $name): Column | INT UNSIGNED + constraint se aplicado como FK | Cria coluna unsigned e a marca para compilação de FK; encadeie constrained() antes de executar a migration. |
Convenções adicionais: timestamps(): void cria created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP e updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP. Não retorna Column e não pode ser encadeado. softDeletes(): Column cria deleted_at DATETIME NULL e retorna essa Column. A coluna só participa da exclusão lógica do Model se o Model correspondente usar $softDelete = true; criar a coluna, por si só, não ativa esse comportamento. O Model base tem $timestamps = true por padrão; verifique também as colunas na tabela.
5. Column: nulabilidade, padrões e modificadores encadeáveis
Uma coluna normal retorna Column. Estes métodos alteram a definição guardada e retornam a própria instância (static), exceto os métodos internos de compilação. O SQL final é montado na ordem fixa: tipo, NULL/NOT NULL, DEFAULT, depois texto de rawExtra, independentemente da ordem em que os modificadores foram chamados.
| Método | Assinatura / retorno | Comportamento e uso |
|---|---|---|
nullable() | nullable(bool $value = true): static | Sem chamada, coluna é NOT NULL. nullable() gera NULL; nullable(false) volta para NOT NULL. Não define valor padrão. |
default() | default(mixed $value, bool $raw = false): static | Define o default. Com raw=false (padrão), strings são cercadas por aspas simples, mas não escapadas; números são anexados ao SQL. raw=true insere o texto como expressão SQL, usado internamente para CURRENT_TIMESTAMP. Use apenas valores estáticos e confiáveis. |
rawExtra() | rawExtra(string $sql): static | Anexa SQL literal ao fim da definição, por exemplo ON UPDATE CURRENT_TIMESTAMP. Não escapa nem valida. Uma chamada substitui o texto extra anterior. |
constrained() | constrained(?string $table = null, string $column = 'id'): static | Guarda a tabela/coluna referenciada. Sem tabela, tenta inferir removendo o sufixo _id do nome e adicionando s; a regra é simplista, portanto prefira indicar explicitamente a tabela e, se preciso, a coluna. |
onDelete() | onDelete(string $action): static | Configura a ação de FK no DELETE do registro pai; converte a string para maiúsculas. Padrão interno: RESTRICT. |
onUpdate() | onUpdate(string $action): static | Configura a ação de FK no UPDATE da chave referenciada; converte a string para maiúsculas. Padrão interno: RESTRICT. |
Exemplo: $table->string('nickname', 80)->nullable()->default('guest'); gera uma coluna VARCHAR(80) NULL DEFAULT 'guest'. Um default e a nulabilidade são conceitos distintos: permitir NULL não cria um default, e definir um default não torna a coluna nullable.
default() são colocadas entre aspas sem escape; enum(), rawExtra() e default(..., raw: true) também entram no SQL literalmente. Use somente nomes e valores constantes escritos no código da migration. Nunca passe input HTTP, conteúdo de banco ou dados externos a esses métodos.6. Índices, unique e remoção de coluna
index(string|array $columns, ?string $name = null): void cria índice comum. unique(string|array $columns, ?string $name = null): void cria índice/constraint único. Com uma string, é um índice de uma coluna; com array, a ordem define um índice composto. Se não informar o nome, os nomes são idx_{tabela}_{colunas} e uniq_{tabela}_{colunas}. Os dois métodos retornam void: não encadeie modificadores após eles.
$table->string('email', 180);
$table->unique('email', 'uniq_customers_email');
$table->index(['status', 'created_at'], 'idx_orders_status_created');
unique() impede duplicidade conforme a regra do banco; index() não garante unicidade. Um índice composto em (status, created_at) não equivale automaticamente a índices independentes para qualquer ordem de busca. O builder não inspeciona dados existentes: adicionar unique a uma coluna com duplicatas pode fazer a migration falhar. Identificadores e nomes fornecidos são interpolados sem escape.
dropColumn(string|array $columns): void registra colunas a remover; é aplicado por Schema::table() via um ALTER TABLE ... DROP COLUMN para cada nome. toCreateSql() não usa essa lista, então chamar dropColumn() dentro de Schema::create() não remove nada.
7. Chaves estrangeiras: exatamente como configurar
foreignId($name) produz INT UNSIGNED e marca a coluna como foreign ID. O FK é compilado para essa marca, e constrained() informa o destino. Exemplo recomendado com a tabela explícita:
$table->foreignId('user_id')
->constrained('users')
->onDelete('cascade');
O SQL gerado contém CONSTRAINT fk_{tabela}_{coluna} FOREIGN KEY (...) REFERENCES users(id) ON DELETE CASCADE ON UPDATE RESTRICT. O nome da constraint é criado automaticamente; não há parâmetro para personalizá-lo. Sem chamadas a onDelete()/onUpdate(), ambas as ações ficam em RESTRICT. A implementação converte o texto recebido para maiúsculas, mas não valida nem limita as ações: use apenas ações aceitas pelo MySQL/InnoDB e deixe o banco rejeitar opções inválidas.
Obrigatório na prática: chame constrained() para cada coluna criada por foreignId(). A implementação emite a constraint sempre que encontra esse tipo, mesmo que uma tabela referenciada não tenha sido configurada; nesse caso pode produzir SQL inválido. Informe explicitamente a tabela, pois a inferência automática apenas remove _id e acrescenta s (user_id → users, mas nomes irregulares não são singularizados corretamente).
Tipos/chaves precisam ser compatíveis e o alvo deve existir quando o DDL for executado. Como id() é INT UNSIGNED, foreignId() combina com essa convenção. Apesar de haver unsignedBigInteger(), não há método separado para marcar um BIGINT como foreign ID; chamar constrained() sobre outra Column não basta para gerar a FK. Chaves estrangeiras compostas também não têm método próprio. Coloque migrations das tabelas-pai antes das tabelas-filhas (a execução é ordenada pelo nome do arquivo).
onDelete('cascade') apaga fisicamente os filhos quando o registro pai é apagado. SET NULL exige que a coluna FK seja nullable; caso contrário a ação não consegue gravar o resultado. Uma exclusão lógica do Model apenas atualiza deleted_at, portanto não aciona ON DELETE CASCADE. A tabela real api_tokens do projeto usa foreignId('user_id')->constrained('users')->onDelete('CASCADE') como exemplo implementado.
SQL efetivamente gerado por Blueprint
Para deixar o resultado concreto, este CREATE TABLE foi conferido executando em PHP os mesmos métodos de Blueprint (ID, FK para users, string, decimal, timestamps, soft delete, unique e índice composto). Os identificadores e valores são inseridos diretamente como código; a saída mostra também que o builder não envolve os nomes em backticks.
CREATE TABLE IF NOT EXISTS posts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY NOT NULL,
user_id INT UNSIGNED NOT NULL,
title VARCHAR(150) NOT NULL,
price DECIMAL(10,2) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
UNIQUE KEY uniq_posts_title (title),
INDEX idx_posts_user_title (user_id, title),
CONSTRAINT fk_posts_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
toAlterSql() também retorna statements em vez de uma única alteração; Schema::table() chama execMigration() para cada item, na ordem abaixo. A lista foi gerada com as operações indicadas:
ALTER TABLE posts ADD COLUMN published_at DATETIME NULL;
ALTER TABLE posts ADD COLUMN team_id INT UNSIGNED NOT NULL;
ALTER TABLE posts ADD CONSTRAINT fk_posts_team_id FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE posts DROP COLUMN old_code;
ALTER TABLE posts DROP COLUMN old_note;
ALTER TABLE posts ADD INDEX idx_posts_published_at (published_at);
Essa ordenação importa: colunas novas são adicionadas primeiro, uma FK de foreignId() vem logo após sua coluna, depois as colunas registradas para remoção e, por fim, índices. Cada statement é executado separadamente; uma falha pode deixar as operações anteriores aplicadas.
8. Modelagem das relações comuns
As relações abaixo são constraints no banco, não métodos de associação no Model. O código do Model confirma que não há hasOne(), hasMany(), belongsTo() ou belongsToMany(). Models podem fazer joins/queries explícitas; para relações automáticas ou carregamento eager, essa implementação não fornece essa API.
1:1 — usuário e perfil
Cenário e objetivo. Cada perfil pertence a um usuário e, no máximo, um perfil pode apontar para o mesmo usuário. A FK sozinha permitiria vários perfis; a restrição unique em user_id é o que limita a um.
<?php
use Framework\Database\Blueprint;
use Framework\Database\Migration;
use Framework\Database\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('profiles', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->string('display_name', 120)->nullable();
$table->unique('user_id', 'uniq_profiles_user');
});
}
public function down(): void
{
Schema::dropIfExists('profiles');
}
};
O resultado garante que um perfil tenha um usuário existente e que um usuário tenha no máximo um perfil. Não obriga cada usuário a ter perfil. Para tabela existente, mova esse callback para up() de uma migration; não cole-o diretamente fora da migration.
1:N e N:1 — usuário e publicações
Cenário e objetivo. Um usuário pode criar várias publicações; cada publicação pertence a um usuário. A FK fica no lado “muitos”, em posts.user_id. Do ponto de vista do post, a mesma estrutura é N:1 (muitos posts para um user), sem uma segunda constraint.
<?php
use Framework\Database\Blueprint;
use Framework\Database\Migration;
use Framework\Database\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('posts', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->string('title', 150);
$table->text('body')->nullable();
$table->timestamps();
$table->index('user_id', 'idx_posts_user');
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
Ao terminar up(), cada linha de posts exige um user_id existente. Não existe lista automática $user->posts; faça a consulta de forma explícita, por exemplo com o wrapper Database:
$posts = \Framework\Database\Database::getInstance()
->query('SELECT posts.id, posts.title, users.name AS author_name
FROM posts
INNER JOIN users ON users.id = posts.user_id
WHERE posts.user_id = :user_id')
->bind(':user_id', $userId)
->fetchAll();
O valor variável é vinculado como parâmetro; não concatene $userId na SQL. Para a API fluente do Model e seus joins explícitos, consulte Query Builder e Database/PDO.
N:N — usuários e papéis por tabela pivô
Cenário e objetivo. Um usuário pode ter vários papéis e cada papel pode pertencer a vários usuários. O framework não cria pivô nem métodos N:N automaticamente: declare uma tabela intermediária com duas FKs. A chave unique composta evita repetir o mesmo par.
<?php
use Framework\Database\Blueprint;
use Framework\Database\Migration;
use Framework\Database\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('roles', function (Blueprint $table): void {
$table->id();
$table->string('name', 60);
$table->unique('name', 'uniq_roles_name');
});
Schema::create('role_user', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->foreignId('role_id')->constrained('roles')->onDelete('cascade');
$table->unique(['user_id', 'role_id'], 'uniq_role_user_pair');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('role_user');
Schema::dropIfExists('roles');
}
};
Crie as tabelas-pai antes da pivô. No down(), remova primeiro role_user, depois roles; a tabela users pertence a outra migration e não deve ser apagada por esta. Se o par precisar ter atributos adicionais (por exemplo assigned_at), coloque-os na pivô.
9. Exemplo integrado: catálogo, pedidos e itens
Cenário e objetivo. Um pequeno sistema de vendas terá produtos, pedidos feitos por usuários já existentes e itens associados a cada pedido. Isso cobre 1:N (users → orders e orders → order_items) e, conceitualmente, N:N entre pedidos e produtos, representada pela tabela order_items. Uma única migration mostra ordem de criação e reversão.
<?php
use Framework\Database\Blueprint;
use Framework\Database\Migration;
use Framework\Database\Schema;
return new class extends Migration {
public function up(): void
{
// A tabela products não depende de outra tabela deste exemplo.
Schema::create('products', function (Blueprint $table): void {
$table->id();
$table->string('sku', 64);
$table->string('name', 150);
$table->decimal('price', 10, 2);
$table->boolean('active')->default(1);
$table->timestamps();
$table->unique('sku', 'uniq_products_sku');
});
// users é criada pela migration existente do projeto.
Schema::create('orders', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('restrict');
$table->string('status', 30)->default('pending');
$table->timestamps();
$table->index('user_id', 'idx_orders_user');
});
// Tabela de associação: cada linha liga um pedido a um produto.
Schema::create('order_items', function (Blueprint $table): void {
$table->id();
$table->foreignId('order_id')->constrained('orders')->onDelete('cascade');
$table->foreignId('product_id')->constrained('products')->onDelete('restrict');
$table->unsignedInteger('quantity')->default(1);
$table->decimal('unit_price', 10, 2);
$table->unique(['order_id', 'product_id'], 'uniq_order_product');
});
}
public function down(): void
{
// Desfaz primeiro as dependências e só então a tabela-pai do exemplo.
Schema::dropIfExists('order_items');
Schema::dropIfExists('orders');
Schema::dropIfExists('products');
}
};
Como ler o resultado. Um usuário pode ter vários pedidos por orders.user_id. Cada pedido pode ter várias linhas em order_items; cada linha aponta para um produto. O unique composto permite apenas uma linha por par pedido/produto. Se seu domínio permite linhas separadas para o mesmo produto (variações, lote, preço ou desconto), altere a chave unique para incluir a variação ou retire a unique conforme as regras do domínio. unit_price preserva o preço usado naquele pedido; consultar apenas o preço atual de products não substituiria esse dado histórico.
Resultado esperado. Se todas as operações forem aceitas pelo MySQL, serão criadas três tabelas InnoDB com as colunas, defaults, índices e constraints declaradas. A migration assume que users já existe. O rollback remove as tabelas na ordem inversa das dependências. Nenhum Model Order, Product ou relação ORM é criado automaticamente; esses arquivos e consultas são responsabilidade da aplicação.
10. Executar, verificar e testar uma migration
- Gere/edite o arquivo em
database/migrations/; confira namespace/imports,up(),down(), nomes de tabela e dependências. - Confirme que o banco de desenvolvimento está configurado em
.enve acessível. - Execute
php mvc migrate. O Migrator processa arquivos.phpem ordem alfabética do prefixo timestamp, pula nomes já registrados e registra o batch somente depois deup()completar. - Verifique a estrutura no cliente MySQL, por exemplo
SHOW CREATE TABLE order_items;, e confira índices/FKs no schema do banco. Esse comando é SQL para o cliente do banco, não um subcomando MVC. - Para validar o rollback em ambiente descartável, use
php mvc migrate:rollbacke depois reaplique comphp mvc migrate.--step=Nindica quantos batches desfazer, não quantos arquivos. Faça isso apenas quando o impacto nos dados for aceitável.
O framework não fornece nesta classe um validador estático/preview de DDL, dry-run do Blueprint ou método de teste de migration isolada. Pode inspecionar a string internamente com Blueprint::toCreateSql() / toAlterSql(), mas esses métodos são auxiliares públicos voltados ao Schema e não substituem execução em MySQL compatível. Erros de PDO são lançados; identifique a primeira instrução que falhou e confirme o schema real antes de repetir.
migrations. Corrija o estado parcial com cuidado antes de executar novamente. migrate --fresh apaga e recria o banco inteiro; não use como atalho em dados que precisam ser preservados.11. O que evitar: erros comuns e correção
| Evite | Por que falha / risco | Faça assim |
|---|---|---|
foreignId('user_id') sem constrained() | O compilador ainda tenta montar FK, mas a tabela de destino não foi definida; pode gerar SQL inválido. | Use foreignId('user_id')->constrained('users') explicitamente. |
integer('user_id')->constrained('users') | constrained() existe em Column, mas o compilador só emite FK para coluna marcada por foreignId(). | Use foreignId() (INT UNSIGNED) e mantenha o tipo compatível com a chave referenciada. |
Confiar sempre na inferência de tabela de constrained() | A regra remove _id e acrescenta s; é uma pluralização simples, não trata nomes irregulares. | Escreva constrained('users') ou constrained('categories'). |
$table->string('email')->unique() | Column não possui método unique(). | Declare a coluna e depois use $table->unique('email') no Blueprint. |
$table->index('x')->nullable() ou $table->timestamps()->nullable() | index() e timestamps() retornam void. | Configure a coluna antes; declare o índice à parte. timestamps() é chamada isolada. |
Usar Schema::create() esperando alterar tabela existente | Gera CREATE TABLE IF NOT EXISTS; não altera uma tabela que já existe. | Crie uma nova migration e use Schema::table(). |
| Concatenar entrada do usuário em nome/default/enum/raw SQL | Blueprint interpolará identificadores, valores ou trechos crus no DDL; não há prepared bindings nem escaping geral. | Use apenas identificadores e valores constantes no código de migration. Para dados de runtime, use Database::query()->bind(). |
Usar float()/double() para dinheiro | Ponto flutuante é aproximado. | Use decimal('price', 10, 2) e defina a escala de negócio. |
Usar SET NULL em FK NOT NULL | O banco não consegue aplicar a ação referencial se a coluna não aceita NULL. | Encadeie nullable() na coluna FK e valide que a regra do domínio permite ausência. |
| Excluir migration já aplicada sem plano | O banco guarda seu nome/batch; apagar arquivo pode deixar o histórico inconsistente e impede executar o down() da cópia local. | Adicione nova migration corretiva e mantenha os arquivos versionados. |
Esperar hasMany(), auto-pivot, cascade em soft delete ou alteração de coluna | Essas facilidades não são implementadas por este Blueprint/Model. | Declare FKs/índices manualmente, faça queries/join explicitamente e planeje as migrations compatíveis com as operações disponíveis. |
12. Referência técnica resumida e limites reais
Schema
| Assinatura | O que faz | Retorno / limite |
|---|---|---|
Schema::create(string $table, callable $callback): void | Cria Blueprint, chama callback e executa toCreateSql(). | Executa já; SQL contém CREATE TABLE IF NOT EXISTS. Identificador é interpolado. |
Schema::table(string $table, callable $callback): void | Cria Blueprint de alteração e executa cada statement retornado por toAlterSql(). | Execução sequencial e imediata; não há transação criada por Schema. |
Schema::dropIfExists(string $table): void | Executa DROP TABLE IF EXISTS. | Use para down idempotente de tabela, com atenção aos dados. |
Schema::drop(string $table): void | Executa DROP TABLE. | Falha se a tabela não existir; identificador interpolado. |
Métodos auxiliares públicos de Blueprint
| Método | Retorno | Uso / observação |
|---|---|---|
__construct(string $table, bool $isAlter = false) | — | Normalmente criado por Schema. O parâmetro $isAlter não é armazenado nem consultado no código atual; o tipo de operação é determinado por toCreateSql() ou toAlterSql(). Não dependa desse booleano para escolher comportamento. |
toCreateSql(): string | String SQL | Compilador chamado por Schema::create(). É público, mas comentado no código como interno do Schema. Não executa por si só. |
toAlterSql(): array (docblock: string[]) | Lista de SQLs | Compilador chamado por Schema::table(). Gera ADD COLUMN/FK, DROP COLUMN e ADD INDEX em ordem de categoria; não executa por si só. |
Métodos públicos de Column usados internamente
| Método | Retorno | Observação |
|---|---|---|
__construct(string $name, string $type) | — | Chamado pelo Blueprint; a própria classe orienta não instanciar diretamente. |
toSql(): string | Definição SQL da coluna | Auxiliar interno, não executa e não escapa identificadores/valores. |
toForeignKeySql(string $table): string | Definição SQL da constraint | Auxiliar interno chamado quando o Blueprint encontra a marca de foreignId. |
Para a API de Models e a ausência de relações ORM, consulte Models; para transações e statements parametrizados, Database e PDO; para queries/join, Query Builder; para criar/executar migrations, CLI. O ponto de partida das implementações é framework/Database/Schema.php, Blueprint.php, Column.php, Migration.php, Database.php e cli/Migrator.php.
Executar e reverter
php mvc make:migration create_posts_table
php mvc migrate
php mvc migrate:rollback
php mvc migrate:rollback --step=2O Migrator mantém a tabela migrations, registra nome e batch, ignora migrations já executadas e desfaz batches pela ordem inversa usando down().
--fresh destrói o banco configurado. Ele executa DROP DATABASE e cria novamente; o comando pede confirmação e tem proteção de ambiente, mas use somente sabendo que todos os dados daquele banco serão perdidos. DDL MySQL pode fazer commit implícito: falhas parciais podem exigir reparo manual.Progressão · criar e alterar schema
Uma migration é uma unidade versionada de DDL. Arquivos PHP retornam um objeto Migration anônimo; o Migrator ordena pelo nome do arquivo e registra o batch. O exemplo abaixo segue a estrutura real de users, sem depender de APIs externas ao Schema/Blueprint.
return new class extends Migration {
public function up(): void
{
Schema::create('posts', function (Blueprint $table): void {
$table->id();
$table->string('title', 150);
$table->text('body')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};`Schema::create()` recebe nome e callback; cada método de coluna devolve Column para encadear nullable/default/constrained/onDelete/onUpdate. Para uma alteração, use `Schema::table()` e faça o `down()` remover ou restaurar o que `up()` mudou. O rollback executa migrations do batch em ordem inversa.
Executar e escolher o nível de reversão
| Comando | Ação | Risco/uso |
|---|---|---|
| php mvc migrate | Aplica migrations pendentes | Fluxo normal após criar/ajustar a migration. |
| php mvc migrate:rollback | Desfaz o último batch | Executa down(); confirme impacto nos dados. |
| php mvc migrate:rollback --step=2 | Desfaz os dois batches mais recentes | A opção step conta batches, não arquivos individuais. |
| php mvc migrate --fresh | Drop/recria o banco e reaplica migrations | Destrói dados; confirmação não substitui backup. |
Veja também a relação com Models e comandos CLI.
Chaves estrangeiras e SQL gerado
foreignId($name) cria coluna e a marca para geração de FK; encadeie constrained($table) para informar a tabela referenciada antes de executar a migration. Sem constrained(), o gerador chega a SQL com tabela referenciada ausente, portanto não use foreignId isoladamente.
$table->foreignId('user_id')
->constrained('users')
->onDelete('cascade');
Schema executa DDL imediatamente no MySQL. Os nomes de tabela/coluna e expressões raw são interpolados no SQL gerado; mantenha-os como literais do código. default($value, raw: true) e rawExtra() aceitam SQL literal e não devem receber input externo. Os valores de enum também são interpolados sem escaping automático.
A referência inclui Database/PDO e a classe Migration base. O Migrator executa migrations PHP em ordem de nome; migrations SQL não são carregadas pelo comando atual.
migrate --fresh recria o database inteiro
php mvc migrate --fresh não limpa apenas as tabelas de migrations: o Migrator executa DROP e CREATE do database configurado. O comando pede que você digite o nome do database para confirmar; --yes suprime essa pergunta. Em produção, também é necessário --force. Faça e verifique um backup antes de qualquer execução; --force apenas autoriza o comando e não protege os dados.
# Interativo; leia a confirmação e digite o nome do database
php mvc migrate --fresh
# Não interativo (continua destrutivo)
php mvc migrate --fresh --yes