Banco de Dados
Migrations, seeders e conexões — com exemplos reais do módulo Usuario.
Migrations
Uma migration é um arquivo PHP em Database/Migrations/ que retorna um array com duas chaves: up (aplica a mudança) e down (reverte). O nome do arquivo define a ordem de execução — use prefixo numérico.
Nunca edite uma migration já executada. O sistema guarda um hash de cada arquivo e lança erro se detectar alteração. Para mudar o schema, crie uma nova migration.
Criando uma tabela (exemplo: usuarios)
Arquivo: src/Modules/Usuario/Database/Migrations/001_create_usuarios.php
<?php
return [
'up' => function (PDO $pdo): void {
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if ($driver === 'pgsql') {
$pdo->exec("
CREATE TABLE IF NOT EXISTS usuarios (
uuid UUID NOT NULL PRIMARY KEY,
nome_completo VARCHAR(255) NOT NULL,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
senha_hash VARCHAR(255) NOT NULL,
nivel_acesso VARCHAR(20) DEFAULT 'usuario'
CHECK (nivel_acesso IN ('usuario','admin','moderador','admin_system')),
ativo BOOLEAN NOT NULL DEFAULT TRUE,
verificado_email BOOLEAN NOT NULL DEFAULT FALSE,
criado_em TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
atualizado_em TIMESTAMP
)
");
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_usuarios_email ON usuarios (email)");
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_usuarios_username ON usuarios (username)");
} else {
$pdo->exec("
CREATE TABLE IF NOT EXISTS usuarios (
uuid CHAR(36) NOT NULL PRIMARY KEY,
nome_completo VARCHAR(255) NOT NULL,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
senha_hash VARCHAR(255) NOT NULL,
nivel_acesso VARCHAR(20) DEFAULT 'usuario',
ativo TINYINT(1) NOT NULL DEFAULT 1,
verificado_email TINYINT(1) NOT NULL DEFAULT 0,
criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
atualizado_em DATETIME,
INDEX idx_email (email),
INDEX idx_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");
}
},
'down' => function (PDO $pdo): void {
$pdo->exec("DROP TABLE IF EXISTS usuarios");
},
];
Alterando uma tabela (nova migration)
Para adicionar ou modificar colunas, crie um novo arquivo — nunca edite o anterior.
Arquivo: src/Modules/Usuario/Database/Migrations/002_add_biografia_to_usuarios.php
<?php
return [
'up' => function (PDO $pdo): void {
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if ($driver === 'pgsql') {
$pdo->exec("ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS biografia TEXT");
$pdo->exec("ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS url_avatar VARCHAR(255)");
} else {
// MySQL não tem ADD COLUMN IF NOT EXISTS — verifica antes
$cols = $pdo->query("SHOW COLUMNS FROM usuarios LIKE 'biografia'")->fetchAll();
if (empty($cols)) {
$pdo->exec("ALTER TABLE usuarios ADD COLUMN biografia TEXT");
$pdo->exec("ALTER TABLE usuarios ADD COLUMN url_avatar VARCHAR(255)");
}
}
},
'down' => function (PDO $pdo): void {
$pdo->exec("ALTER TABLE usuarios DROP COLUMN IF EXISTS biografia");
$pdo->exec("ALTER TABLE usuarios DROP COLUMN IF EXISTS url_avatar");
},
];
Comandos de migration
php vupi migrate # executa todas as migrations pendentes
php vupi migrate --seed # migrations + seeders
php vupi migrate --rollback # desfaz a última migration
php vupi migrate --status # lista status de cada migration (done/pending)
php vupi migrate --core # apenas conexão core (DB_*)
php vupi migrate --modules # apenas conexão modules (DB2_*)
# Alias equivalente
php db migrate
php db seed
php db rollback
O Migrator cria automaticamente uma tabela migrations no banco e guarda o nome e hash de cada arquivo executado. Se você alterar um arquivo já executado, o sistema detecta a mudança e lança erro antes de continuar.
Seeders
Um seeder é um arquivo PHP em Database/Seeders/ que retorna uma callable (função anônima). Ele recebe o PDO e insere dados iniciais. Seeders também são rastreados — cada arquivo roda apenas uma vez.
Use ON CONFLICT DO NOTHING (PostgreSQL) ou ON DUPLICATE KEY UPDATE / verificação prévia (MySQL) para que o seeder não falhe se rodar mais de uma vez.
Criando um seeder (exemplo: admin inicial)
Arquivo: src/Modules/Usuario/Database/Seeders/001_admin_user.php
<?php
/**
* Seeder: cria o usuário admin_system inicial.
* Credenciais configuráveis via .env:
* ADMIN_EMAIL, ADMIN_PASSWORD, ADMIN_NAME, ADMIN_USERNAME
*/
return function (PDO $pdo): void {
$email = $_ENV['ADMIN_EMAIL'] ?? '[email protected]';
$senha = $_ENV['ADMIN_PASSWORD'] ?? 'admin123';
$nome = $_ENV['ADMIN_NAME'] ?? 'Administrador';
$username = $_ENV['ADMIN_USERNAME'] ?? 'admin';
// Verifica se já existe — idempotência
$stmt = $pdo->prepare("SELECT 1 FROM usuarios WHERE email = :email LIMIT 1");
$stmt->execute([':email' => $email]);
if ($stmt->fetchColumn()) {
echo " ⊘ Admin já existe: {$email}\n";
return;
}
// Verifica conflito de username
$stmt = $pdo->prepare("SELECT 1 FROM usuarios WHERE username = :username LIMIT 1");
$stmt->execute([':username' => $username]);
if ($stmt->fetchColumn()) {
$username = 'admin_' . bin2hex(random_bytes(3));
}
$uuid = \Ramsey\Uuid\Uuid::uuid4()->toString();
$hash = password_hash($senha, PASSWORD_BCRYPT, ['cost' => 12]);
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if ($driver === 'pgsql') {
$stmt = $pdo->prepare("
INSERT INTO usuarios (uuid, nome_completo, username, email, senha_hash,
nivel_acesso, ativo, verificado_email, criado_em)
VALUES (:uuid, :nome, :username, :email, :hash,
'admin_system', TRUE, TRUE, NOW())
");
} else {
$stmt = $pdo->prepare("
INSERT INTO usuarios (uuid, nome_completo, username, email, senha_hash,
nivel_acesso, ativo, verificado_email, criado_em)
VALUES (:uuid, :nome, :username, :email, :hash,
'admin_system', 1, 1, NOW())
");
}
$stmt->execute([
':uuid' => $uuid,
':nome' => $nome,
':username' => $username,
':email' => $email,
':hash' => $hash,
]);
echo " ✔ Admin criado: {$email} / {$username}\n";
echo " ⚠ TROQUE A SENHA EM PRODUÇÃO!\n";
};
Executando seeders
php vupi migrate --seed # migrations + seeders juntos (recomendado)
php db seed # apenas seeders
Dois bancos simultâneos
O sistema suporta dois bancos: core (DB_*) para módulos nativos e modules (DB2_*) para módulos externos. Declare qual usar no connection.php do módulo:
<?php
// src/Modules/MeuModulo/Database/connection.php
// Opções: 'core' (DB_*) | 'modules' (DB2_*) | 'auto'
return 'core';
| Valor | Usa | Quando usar |
|---|---|---|
'core' | DB_* | Módulos nativos em src/Modules/ (padrão) |
'modules' | DB2_* | Módulos externos / plugins que precisam de banco separado |
'auto' | Detecta automaticamente | Módulos em vendor/ usam modules; os demais usam core |
Sempre use Prepared Statements
Concatenar variáveis em SQL abre brecha para SQL Injection. Sempre use prepare() + bindValue().
// ERRADO — vulnerável a SQL Injection
$pdo->query("SELECT * FROM usuarios WHERE email = '$email'");
// CORRETO — sempre assim
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE email = :email");
$stmt->bindValue(':email', $email);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);