Docs›Banco de dados›Soft delete
Banco de dados

Soft delete

HasMadSoftDeletes, withTrashed, restore.

Soft delete marca registros como deletados sem removê-los fisicamente do banco. No MAD isso é implementado pelo trait Mad\Database\Concerns\HasMadSoftDeletes — soft delete condicional: só fica ativo quando o model declara a coluna.

Não é o trait Illuminate\Database\Eloquent\SoftDeletes nativo do Laravel — é uma implementação própria do MAD com a mesma ideia, mas remapeamento de nome de coluna e compatibilidade com constantes legadas (DELETEDAT).

Habilitar

use Mad\Database\Concerns\HasMadSoftDeletes;

class Produto extends Model
{
    use HasMadSoftDeletes;

    protected $connection = 'business';
    protected $table      = 'produto';

    // Coluna padrão é 'deleted_at' — só precisa declarar se for outro nome
    protected ?string $deletedAtColumn = 'deleted_at';
}

Forma recomendada pra remapear (junto de HasMadAudit, mesmo array $madAudit, false desliga a coluna):

protected array $madAudit = [
    'deleted_at'         => 'removido_em',
    'deleted_by'         => 'removido_por',
    'deleted_by_user_id' => 'removido_por_user_id',
];

A coluna deve ser NULL por padrão. Quando preenchida com timestamp, o registro é considerado deletado.

Comportamento padrão

// delete() agora faz UPDATE deleted_at = NOW() — não DELETE FROM
$produto = Produto::find(42);
$produto->delete();

// Queries automaticamente excluem soft-deleted (global scope 'madSoftDelete')
$ativos = Produto::all();
// → SELECT * FROM produto WHERE deleted_at IS NULL

$total = Produto::count();
// → SELECT COUNT(*) FROM produto WHERE deleted_at IS NULL

Incluir registros deletados

// withTrashed() — remove o global scope, inclui deletados
$todos = Produto::withTrashed()->where('ativo', true)->get();

// onlyTrashed() — APENAS deletados (lixeira)
$lixeira = Produto::onlyTrashed()->orderByDesc('deleted_at')->get();

Restaurar

$produto = Produto::withTrashed()->find($id);
if ($produto && $produto->trashed()) {
    $produto->restore();
}

Hard delete (físico)

O trait não expõe um método forceDelete() próprio. A maneira real de remover fisicamente é ir direto pelo Query Builder — operações em massa não passam pela instância do model, então não disparam o override de soft delete:

// DELETE FROM produto WHERE id = X — físico, irreversível
Produto::withTrashed()->where('id', $id)->delete();

Por que isso funciona: HasMadSoftDeletes intercepta performDeleteOnModel() (chamado por $model->delete() numa instância única). Um mass delete via Query Builder (Model::where(...)->delete()) nunca instancia o model — vai direto pro SQL, igual qualquer operação em massa do Eloquent. Ver Operações em massa.

Auditoria — quem deletou

Já é automático: basta mapear as colunas em $madAudit (ou declarar as propriedades $deletedByColumn/$deletedByUserIdColumn) — o trait preenche sozinho a partir da session a cada delete(), sem precisar de evento nenhum:

class Produto extends Model
{
    use HasMadSoftDeletes;

    protected array $madAudit = [
        'deleted_at'         => 'deleted_at',
        'deleted_by'         => 'deleted_by',          // ← preenchido com session('login')
        'deleted_by_user_id' => 'deleted_by_user_id',  // ← preenchido com session('userid')
    ];
}

Não tente setar $produto->deleted_by manualmente num evento deleting — performDeleteOnModel() recalcula esse valor a partir da session e sobrescreve qualquer coisa que você tenha atribuído antes.

Listagens com toggle de deletados

Em MadDataGrid, sobrescreva o hook query(): array (vazio por padrão, sinaliza pro grid usar a auto-query builder-native) pra montar a busca você mesmo quando precisa de uma condição como "mostrar excluídos":

class ProdutoListagem extends MadDataGrid
{
    protected string $model = 'Produto';
    public bool $mostrarDeletados = false;

    protected function query(): array
    {
        $q = Produto::query();

        if ($this->mostrarDeletados) {
            $q->withTrashed();
        }

        $q->orderBy('nome');

        return [
            'items' => $q->forPage($this->page, $this->perPage)->get()->all(),
            'total' => (clone $q)->count(),
        ];
    }

    public function onToggleDeletados(): void
    {
        $this->mostrarDeletados = !$this->mostrarDeletados;
        $this->page = 1;
        $this->loadData();
    }
}

API — resumo

MétodoDescrição
where(...)->get()Exclui deletados automaticamente (global scope)
withTrashed()Inclui deletados
onlyTrashed()APENAS deletados
restore()Limpa a coluna de deleção (restaura)
trashed()Bool — registro está soft-deleted?
withTrashed()->where(...)->delete()DELETE físico (mass delete, irreversível)

Próximos