Q&A
How to perform soft delete in Laravel
Overview
Sometimes you want a record "gone" from the application but still recoverable - for audit trails, undo features, or referential safety. Laravel's soft deletes keep the row in the table and flag it with a deleted_at timestamp. Eloquent then transparently filters those rows out of normal queries, so the rest of your code behaves as if they were deleted.
graph TD
A["$model->delete()"] --> B{SoftDeletes trait?}
B -->|no| C[Row physically DELETEd]
B -->|yes| D["Set deleted_at = now()"]
D --> E[Row hidden from normal queries]
E --> F["restore() clears deleted_at"]
E --> G["forceDelete() removes permanently"]
style D fill:#1e1b4b,color:#fff
Step 1: Add the column (migration)
Use the softDeletes() helper, which adds a nullable deleted_at timestamp:
Schema::table('posts', function (Blueprint $table) {
$table->softDeletes(); // adds nullable deleted_at column
});
Step 2: Add the trait to the model
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes;
}
Step 3: Use it
$post = Post::find(1);
$post->delete(); // sets deleted_at instead of removing the row
Post::all(); // soft-deleted rows are automatically excluded
$post->trashed(); // true if soft-deleted
Querying Soft-Deleted Records
// Include soft-deleted rows
Post::withTrashed()->get();
// Only soft-deleted rows
Post::onlyTrashed()->get();
Restoring
$post->restore(); // single model: clears deleted_at
Post::onlyTrashed()->restore(); // restore many at once
Permanent (hard) Deletion
When you truly want the row gone:
$post->forceDelete(); // bypasses soft delete, runs real DELETE
How It Works Under the Hood
The SoftDeletes trait registers a global scope that adds WHERE deleted_at IS NULL to every query on the model, so deleted rows vanish from normal results. delete() is overridden to UPDATE ... SET deleted_at = NOW() instead of issuing DELETE. withTrashed() removes that scope; onlyTrashed() flips it to WHERE deleted_at IS NOT NULL.
Relationships and Cascades
Soft deletes don't automatically cascade to related models. If deleting a Post should soft-delete its Comments, handle it explicitly (e.g. in a model event/observer):
protected static function booted()
{
static::deleting(function (Post $post) {
$post->comments()->delete(); // soft-delete children too
});
}
Pruning Old Soft-Deleted Records
To eventually purge stale soft-deleted rows, Laravel offers the Prunable/MassPrunable traits with a prunable() query, run via the model:prune scheduled command - useful so the table doesn't grow forever.
Why This Matters in Interviews
Laravel/PHP interviews check whether you know the three pieces (migration column + trait + behavior), how Eloquent hides deleted rows via a global scope, and the withTrashed/onlyTrashed/restore/forceDelete vocabulary. The insight that delete() becomes an UPDATE is the "do you understand the mechanism" signal.
Common Pitfalls
- Forgetting the
deleted_atcolumn migration → trait errors. - Expecting soft deletes to cascade to relations automatically (they don't).
- Unique constraints conflicting with soft-deleted rows (a "deleted" email still occupies the unique index unless you account for
deleted_at). - Letting soft-deleted data accumulate without pruning.
References
- Laravel Eloquent - Soft Deleting: https://laravel.com/docs/eloquent#soft-deleting
- Laravel Pruning Models: https://laravel.com/docs/eloquent#pruning-models