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(ouAuthenticatable, 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:
| Trait | O que faz |
|---|---|
HasIdPolicy | Gera a chave primária no evento creating conforme a política configurada (serial, UUID, ULID, etc). |
HasMadAudit | Substitui os timestamps nativos por auditoria completa: created_at/by/by_user_id/by_unit_id, updated_at/by/by_user_id. |
HasMadSoftDeletes | Soft delete condicional — ativo só quando o model define a coluna. Ver Soft delete. |
BelongsToTenant | Isolamento 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(). |
BelongsToUnit | Isolamento 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/BelongsToUnitvalem 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 aceita | Exemplo | Observação |
|---|---|---|
| FQCN explícito | App\Models\Iam\User | Sempre resolve |
Token DominioEntidade | IamUser, CommMessage | Handle público canônico — sempre único |
| Basename da entidade | User, Document | Só 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ítica | Comportamento |
|---|---|
serial (default) | Banco gera (identity / auto-increment) — nada é gerado na aplicação. |
uuid, uuid4, uuid7, ulid, tsid, cuid2, nanoid, snowflake | Gerado via Mad\Util\IdGenerator; coluna vira keyType = 'string' automaticamente. |
none | O 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 mesmoMAX+1). Declarar'max'hoje lançaRuntimeExceptionnosave(); migre a tabela para identity/auto-increment e useserial.
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
- Query Builder fluente — encadeamento where/orderBy/take.
- Filtros de query — WHERE seguro, IN, BETWEEN, subqueries.
- Relacionamentos — hasMany, belongsToMany, eager loading.
- Soft delete — exclusão lógica via
HasMadSoftDeletes. - Operações em massa — update/delete em lote, agregações.