Lazy eager loading — load() after collection retrieval
Concept
Soft deletes allow "deleting" records without removing them from the database. A deleted_at timestamp column marks the deletion time. All standard queries automatically filter out soft-deleted records via a global scope.
Setup:
- Add
deleted_atcolumn in migration:$table->softDeletes(). - Add
use SoftDeletestrait to the model.
What changes with SoftDeletes:
$model->delete()→ setsdeleted_at = NOW()instead of DELETE.Model::all()/where()/find()→ automatically addsWHERE deleted_at IS NULL.Model::findOrFail($id)→ returns 404 if the record is soft-deleted.
Accessing soft-deleted records:
Model::withTrashed(): Include soft-deleted in results.Model::onlyTrashed(): Only soft-deleted records.Model::withTrashed()->find($id): Find including soft-deleted.
Restore: $model->restore() → sets deleted_at = null.
Force delete: $model->forceDelete() → actual DELETE SQL, bypasses soft delete.
trashed() / isNotTrashed(): Check if a specific model instance is soft-deleted.
Relationships with soft deletes: By default, eager loaded relationships don't filter soft-deleted related records. Add ->withoutTrashed() or ->onlyTrashed() to relationship definitions if needed.
Cascading soft deletes: Not automatic. Implement in the deleting observer: $post->comments()->each->delete().
Code Example
<?php
// Migration
Schema::table('posts', function (Blueprint $table) {
$table->softDeletes(); // adds nullable deleted_at timestamp column
});
// Model
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes;
}
// CRUD with soft deletes
$post = Post::find(1);
$post->delete(); // soft delete — sets deleted_at
// The post remains in the DB with deleted_at = NOW()
Post::find(1); // null — hidden by global scope
Post::withTrashed()->find(1); // found — includes soft-deleted
$post->trashed(); // true — is soft-deleted
$post->restore(); // un-delete — deleted_at = null
$post->forceDelete(); // permanent DELETE SQL
// Querying
$activePosts = Post::all(); // WHERE deleted_at IS NULL
$allPosts = Post::withTrashed()->get(); // includes deleted
$deletedPosts = Post::onlyTrashed()->get(); // only deleted
// Restoring multiple
Post::onlyTrashed()->where('user_id', 5)->restore();
// Cascade soft-delete to children
class User extends Model
{
use SoftDeletes;
protected static function boot(): void
{
parent::boot();
static::deleting(function(User $user) {
// Soft-delete all related posts too
$user->posts()->each->delete();
});
static::restoring(function(User $user) {
// When restoring user, restore soft-deleted posts within 5 min
$user->posts()->withTrashed()
->where('deleted_at', '>=', now()->subMinutes(5))
->restore();
});
}
}
// Unique constraint on soft-deletable column — handle deleted duplicates
// Use Rule::unique()->whereNull('deleted_at') in validation
'email' => \Illuminate\Validation\Rule::unique('users')->whereNull('deleted_at'),