Deleting and Pruning

Deleting, Soft Deleting, and Pruning

$model->delete() fires deleting and deleted around a DELETE, Product::destroy([9, 10]) loads each key to do the same, and Product::where(...)->delete() is one statement with no events. Products got a deleted_at column ($table->softDeletes()) and the SoftDeletes trait, so delete() now stamps the row and a global scope hides stamped rows:

Soft deleting, restoring, and a failed force deleteSQL
use App\Models\Product;
Product::find(3)->delete();
[$live, $all] = [Product::count(), Product::withTrashed()->count()];
echo "$live live, $all in all\n";
$gone = Product::onlyTrashed()->first();
$gone->restore();
try {
    $gone->forceDelete();
} catch (Illuminate\Database\QueryException $e) {
    echo substr($e->getMessage(), 0, 90), "...\n";
}
Output
...
-- update `products` set `deleted_at` = '2026-09-23 08:18:56' where `id` = 3
...
10 live, 11 in all
-- select * from `products` where `products`.`deleted_at` is not null limit 1
-- update `products` set `deleted_at` = null where `id` = 3
SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row...

The foreign key from order_items refused the force delete: a product once sold cannot really go, which is why soft deletes suit it. Rows that should go are pruned. Coupon uses Prunable, and its prunable() returns static::where('expires_at', '<', now()->subDays(30)); with a coupon expired in May, model:prune --pretend reported 1 [App\Models\Coupon] records will be pruned. Schedule model:prune daily (Defining Scheduled Tasks). MassPrunable deletes in one statement, without events.