Docs›Banco de dados›Callbacks de ciclo
Banco de dados

Callbacks de ciclo

Eventos Eloquent: creating, saving, deleting, observers.

Models Eloquent disparam eventos em cada etapa do ciclo de vida — antes/depois de salvar, criar, atualizar, deletar. Use para auditoria, validações cruzadas, side effects e normalização de dados. O MAD não acrescenta um sistema de callbacks próprio aqui: é o mecanismo de eventos nativo do Eloquent.

Eventos disponíveis

EventoQuando dispara
retrievedDepois de carregar um model existente do banco
creating / createdAntes/depois de INSERT
updating / updatedAntes/depois de UPDATE
saving / savedAntes/depois de INSERT ou UPDATE (ambos os casos)
deleting / deletedAntes/depois de DELETE (ou soft delete, se o model usa HasMadSoftDeletes)

Não existem eventos restoring/restored aqui. Eles vêm do trait nativo Illuminate\Database\Eloquent\SoftDeletes, que o MAD não usa: Mad\Database\Concerns\HasMadSoftDeletes::restore() faz um UPDATE direto na coluna de deleção (setKeysForSaveQuery(...)->update([$col => null])) e não dispara evento nenhum. Se precisa reagir a uma restauração, chame o side effect explicitamente depois do restore().

Registre via static::booted() — chamado uma vez quando a classe do model é inicializada:

class Produto extends Model
{
    protected static function booted(): void
    {
        static::creating(function (self $produto) {
            // ...
        });

        static::updating(function (self $produto) {
            // ...
        });
    }
}

Exemplo real do projeto — cascata de delete

App\Models\Iam\User::booted() — limpa vínculos antes de deletar o usuário:

protected static function booted(): void
{
    static::deleting(function (self $user) {
        UserGroup::where('user_id', $user->id)->delete();
        UserUnit::where('user_id', $user->id)->delete();
        UserProgram::where('user_id', $user->id)->delete();
        UserRole::where('user_id', $user->id)->delete();
    });
}

Exemplo: auditoria automática

É exatamente assim que Mad\Database\Concerns\HasMadAudit funciona por dentro — sem reinventar nada, dois closures simples:

public static function bootHasMadAudit(): void
{
    static::creating(fn ($model) => $model->madStampAudit(true));
    static::updating(fn ($model) => $model->madStampAudit(false));
}

Exemplo: normalização

class Cliente extends Model
{
    protected static function booted(): void
    {
        static::saving(function (self $cliente) {
            // Normaliza CPF/CNPJ — remove pontos/traços
            if (!empty($cliente->documento)) {
                $cliente->documento = preg_replace('/\D/', '', $cliente->documento);
            }

            // Email lowercase
            if (!empty($cliente->email)) {
                $cliente->email = mb_strtolower(trim($cliente->email));
            }
        });
    }
}

Exemplo: validação cruzada / bloqueio de save

Lance exceção dentro do evento pra abortar a operação:

class PedidoVenda extends Model
{
    protected static function booted(): void
    {
        static::saving(function (self $pedido) {
            if ($pedido->exists) {
                $total = $pedido->itens()->sum('valor_total');
                if ($total <= 0) {
                    throw new \Exception('Pedido sem itens não pode ser salvo.');
                }
                $pedido->valor_total = $total;
            }
        });
    }
}
// No controller:
try {
    DB::connection('business')->transaction(function () use ($pedido) {
        $pedido->save(); // dispara exceção se sem itens
    });
    return MadToast::success('Salvo!');
} catch (\Throwable $e) {
    return MadMessage::error('Erro', $e->getMessage());
}

Exemplo: bloqueio de delete

class Categoria extends Model
{
    protected static function booted(): void
    {
        static::deleting(function (self $categoria) {
            $count = Produto::where('categoria_id', $categoria->id)->count();
            if ($count > 0) {
                throw new \Exception(
                    "Não é possível excluir: há {$count} produto(s) usando esta categoria."
                );
            }
        });
    }
}

Observers — quando os closures crescem demais

Pra models com muitos eventos, extraia uma classe Observer em vez de empilhar closures em booted():

class ProdutoObserver
{
    public function creating(Produto $produto): void { /* ... */ }
    public function updating(Produto $produto): void { /* ... */ }
    public function deleting(Produto $produto): void { /* ... */ }
}

// AppServiceProvider::boot()
Produto::observe(ProdutoObserver::class);

Eventos NÃO rodam em operações em massa

Model::where(...)->update() e Model::where(...)->delete() operam direto via Query Builder — nenhuma instância de model é criada, então nenhum evento dispara. Comportamento padrão do Eloquent, não uma limitação do MAD. Ver Operações em massa.

// ❌ creating/updating NÃO rodam — mass update via query builder
Produto::where('categoria_id', 5)->update(['ativo' => false]);

// ✅ Se você precisa dos eventos, itere e salve cada instância
$produtos = Produto::where('categoria_id', 5)->get();
foreach ($produtos as $p) {
    $p->ativo = false;
    $p->save(); // eventos rodam
}

Boas práticas

  • Mantenha eventos rápidos — side effects pesados (email, webhook) devem ir pra uma queue, não rodar inline no save.
  • Não chame save() recursivo — modificar e re-salvar dentro de saved causa loop infinito.
  • Validação pertence ao controller/form request — o evento é uma defesa adicional, não o lugar primário pra regras de negócio.

Próximos