AlgoInsight

Q&A

How to perform soft delete in Laravel

By AlgoInsight Team2026-06-07databaseslaravelphp

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

References

More answers