Referência v3
Voltar à plataforma

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.

Migrations são append-only

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
<?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
<?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

bash
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
Como o sistema rastreia migrations

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.

Seeders devem ser idempotentes

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
<?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

bash
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
<?php
// src/Modules/MeuModulo/Database/connection.php
// Opções: 'core' (DB_*) | 'modules' (DB2_*) | 'auto'
return 'core';
ValorUsaQuando usar
'core'DB_*Módulos nativos em src/Modules/ (padrão)
'modules'DB2_*Módulos externos / plugins que precisam de banco separado
'auto'Detecta automaticamenteMódulos em vendor/ usam modules; os demais usam core

Sempre use Prepared Statements

Nunca concatene SQL

Concatenar variáveis em SQL abre brecha para SQL Injection. Sempre use prepare() + bindValue().

php
// 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);