Docs›Banco de dados›Models Eloquent
Banco de dados

Models Eloquent

Traits MAD (id-policy, auditoria, soft delete, tenant), CRUD, accessors.

Modelos no MAD são Eloquent puro — Illuminate\Database\Eloquent\Model, sem nenhuma camada de Active Record por cima. Não existe mais uma classe base obrigatória com constantes (TABLENAME, PRIMARYKEY, IDPOLICY...): o que o MAD acrescenta são traits opcionais que você mistura no model conforme precisa — política de geração de PK, auditoria automática, soft delete e isolamento por tenant.

Um teste de arquitetura (tests/Feature/ModelArchitectureTest.php) garante que nenhum model reintroduza um contrato tipo Active Record — todo model deste projeto é Illuminate\Database\Eloquent\Model (ou Authenticatable, que estende Model) mais traits explícitos.

Definindo um model

Exemplo real do projeto (App\Models\Comm\Notification):

namespace App\Models\Comm;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Mad\Database\Concerns\HasIdPolicy;
use Mad\Database\Concerns\HasMadAudit;
use Mad\Database\Concerns\HasMadSoftDeletes;
use Mad\Database\Concerns\BelongsToTenant;

class Notification extends Model
{
    use HasIdPolicy, HasMadAudit, HasMadSoftDeletes, BelongsToTenant;

    protected $connection = 'comm';
    protected $table      = 'mad_comm_notification';

    protected $fillable = [
        'user_to_id',
        'user_from_id',
        'type',
        'data',
        'action',
        'read_at',
    ];

    protected $casts = [
        'data'       => 'array',
        'action'     => 'array',
        'read_at'    => 'datetime',
        'created_at' => 'datetime',
    ];
}

$connection e $table são propriedades nativas do Eloquent — cada domínio do app aponta para uma das 6 conexões lógicas (iam, log, business, comm, ged, ai). $fillable e $casts também são 100% Eloquent puro.

Traits MAD disponíveis

Em Mad\Database\Concerns — todas opcionais, combine conforme o model precisa:

TraitO que faz
HasIdPolicyGera a chave primária no evento creating conforme a política configurada (serial, UUID, ULID, etc).
HasMadAuditSubstitui os timestamps nativos por auditoria completa: created_at/by/by_user_id/by_unit_id, updated_at/by/by_user_id.
HasMadSoftDeletesSoft delete condicional — ativo só quando o model define a coluna. Ver Soft delete.
BelongsToTenantIsolamento por linha (tenant_id) em modo pool multi-tenant, atrás da flag mad.tenant.row_scope_enabled. Também stampa tenant_id no creating quando vier vazio, e expõe a relação tenant().
BelongsToUnitIsolamento por linha por unidade/filial (unit_id), atrás da flag mad.general.multiunit ('1' = ON, default '0'). Stampa unit_id no creating e expõe a relação unit(). Um model pode ter flag própria sobrescrevendo unitScopeFlagKey() (ex.: GED → 'mad.ged.multiunit').

BelongsToTenant/BelongsToUnit valem só para models data-plane (business, comm, ged, ai). Control-plane (iam, log) nunca usa — identidade e auditoria são globais ao install.

Namespace por domínio e ModelRegistry

Models vivem em sub-namespaces por domínio: App\Models\<Dominio>\<Entidade> (App\Models\Iam\User, App\Models\Comm\Message, App\Models\Ged\Document). Quem traduz um identificador em string para o FQCN real é Mad\Database\ModelRegistry — a fonte única de resolução usada pelas props model="..." dos componentes:

Forma aceitaExemploObservação
FQCN explícitoApp\Models\Iam\UserSempre resolve
Token DominioEntidadeIamUser, CommMessageHandle público canônico — sempre único
Basename da entidadeUser, DocumentSó quando único; em colisão o model flat/app-space vence

O índice é montado varrendo app/Models recursivamente e memoizado por processo; só subclasses de Illuminate\Database\Eloquent\Model entram (traits em Concerns\*, enums e interfaces são ignorados).

Política de geração de PK

Configurada via propriedade $idPolicy (ou constante legada IDPOLICY, que tem precedência) e aplicada por HasIdPolicy no evento creating:

class Pedido extends Model
{
    use HasIdPolicy;

    protected string $idPolicy = 'uuid7'; // default: 'serial'
}
PolíticaComportamento
serial (default)Banco gera (identity / auto-increment) — nada é gerado na aplicação.
uuid, uuid4, uuid7, ulid, tsid, cuid2, nanoid, snowflakeGerado via Mad\Util\IdGenerator; coluna vira keyType = 'string' automaticamente.
noneO chamador define a PK (ex.: chave semântica string) — nada é gerado.

A política max (MAX(pk)+1) foi removida — tinha condição de corrida (dois inserts concorrentes calculam o mesmo MAX+1). Declarar 'max' hoje lança RuntimeException no save(); migre a tabela para identity/auto-increment e use serial.

Auditoria automática

HasMadAudit substitui os timestamps nativos do Eloquent (desliga $timestamps sozinho) e audita criação/atualização via eventos creating/updating, lendo usuário/unidade da session (session('login'), session('userid'), session('userunitid')):

class Produto extends Model
{
    use HasMadAudit;

    // Remapeia colunas (chave lógica => coluna real; false = desliga a coluna)
    protected array $madAudit = [
        'created_at'         => 'criado_em',
        'created_by'         => 'criado_por',
        'created_by_user_id' => 'criado_por_user_id',
        'created_by_unit_id' => false, // não audita unidade
        'updated_at'         => 'alterado_em',
        'updated_by'         => 'alterado_por',
        'updated_by_user_id' => 'alterado_por_user_id',
    ];
}

Sem $madAudit, usa os nomes padrão (created_at, created_by...). CLI/jobs sem session ativa não quebram — o valor de usuário simplesmente fica null.

Buscar registros

// find() — Eloquent nativo, retorna null se não encontrar
$produto = Produto::find($id);
if ($produto) {
    echo $produto->nome;
}

// findOrFail() — lança ModelNotFoundException
$produto = Produto::findOrFail($id);

// Por outro campo
$produto = Produto::where('cod_barras', $ean)->first();

// Todos
$todos = Produto::all();

CRUD básico

// Criar
$produto = new Produto();
$produto->nome  = 'Camiseta';
$produto->valor = 59.90;
$produto->save();

// Ou via create() (respeita $fillable)
$produto = Produto::create([
    'nome'  => 'Camiseta',
    'valor' => 59.90,
]);

// updateOrCreate — busca ou cria/atualiza
$produto = Produto::updateOrCreate(
    ['cod_barras' => '123'],
    ['nome' => 'Camiseta', 'valor' => 59.90]
);

// Atualizar
$produto = Produto::find($id);
$produto->nome = 'Camiseta P';
$produto->save();

// Deletar
$produto = Produto::find($id);
$produto?->delete();

Accessors (propriedades calculadas)

Eloquent usa Illuminate\Database\Eloquent\Casts\Attribute para accessors modernos — exemplo real de App\Models\Iam\User:

use Illuminate\Database\Eloquent\Casts\Attribute;

class User extends Authenticatable
{
    // Acessado como $user->frontpage_name
    protected function frontpageName(): Attribute
    {
        return Attribute::make(get: fn () => $this->frontpage?->name);
    }
}

Relacionamentos

belongsTo, hasMany, belongsToMany — relations Eloquent nativas, com eager loading via with() para evitar N+1. Ver Relacionamentos em detalhe.

NUNCA fazer

// ❌ ERRADO — SQL raw bypassa o ORM e a auditoria/soft-delete dos traits
$rows = DB::connection('business')->select("SELECT * FROM produto WHERE ativo = 1");

// ✅ CERTO — Eloquent
$items = Produto::where('ativo', true)->get();
// ❌ ERRADO — operação de escrita fora de transação em fluxo crítico
Produto::create($dados);

// ✅ CERTO — escrita encapsulada (ver Transações)
DB::connection('business')->transaction(function () use ($dados) {
    Produto::create($dados);
});

Próximos passos