Once a model uses the SoftDeletes trait, a "deleted" row isn't actually removed from the database — it's marked with a deleted_at timestamp and automatically excluded from normal query results. Working with those hidden rows again needs a few specific methods.
Setting up soft deletes
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes;
}
Schema::table('posts', function (Blueprint $table) {
$table->softDeletes(); // adds a nullable deleted_at column
});
Deleting a record (soft delete, once the trait is applied)
$post->delete(); // sets deleted_at instead of removing the row
Normal queries automatically exclude soft-deleted rows
Post::all(); // does NOT include soft-deleted posts
Including soft-deleted records alongside normal ones
Post::withTrashed()->get(); // includes both active and soft-deleted posts
Getting only the soft-deleted records
Post::onlyTrashed()->get(); // only posts with a non-null deleted_at
Restoring a soft-deleted record
$post = Post::onlyTrashed()->find($id);
$post->restore(); // clears deleted_at, making it a normal active record again
Post::onlyTrashed()->where('id', $id)->restore(); // restore directly via the query builder
Permanently deleting a soft-deleted record
$post = Post::onlyTrashed()->find($id);
$post->forceDelete(); // actually removes the row from the database this time
Checking whether a specific instance is soft-deleted
if ($post->trashed()) {
// this instance currently has a non-null deleted_at
}
Why soft deletes are worth using for certain data
Soft deletes are particularly valuable for data with real business or audit value — orders, user accounts, financial records — where "undo" needs to actually be possible and a permanent, accidental deletion would be a genuine problem, as opposed to something more disposable like a cache entry or a temporary session record, where a hard delete is perfectly reasonable.
A gotcha: unique validation against soft-deleted rows
Rule::unique('users', 'email')->ignore($user->id)->whereNull('deleted_at')
Without whereNull('deleted_at'), a unique validation rule can incorrectly reject a new record's email as "taken" when the only existing match is actually a soft-deleted row — worth remembering specifically because Laravel's default unique validation doesn't account for soft deletes automatically.