With Eloquent's SoftDeletes trait, what do delete(), withTrashed(), onlyTrashed(), restore() and forceDelete() do to a matter and its queries?
answer
- deleted_at timestamp, not DELETE
- SoftDeletingScope hides trashed rows
- withTrashed / onlyTrashed / trashed()
- restore() nulls deleted_at
- builder delete() soft-deletes in bulk
basics
~20 sWith SoftDeletes, delete() sets deleted_at instead of removing the row, and a global scope hides trashed rows from queries. withTrashed() includes them, onlyTrashed() returns only them, restore() clears deleted_at, and forceDelete() really deletes the row.
solid answer
~30 sAdding `use SoftDeletes;` to `Matter` registers `SoftDeletingScope`, a global scope that adds `whereNull('matters.deleted_at')` to every query, and changes `delete()` to an `UPDATE` setting `deleted_at` (and `updated_at`) to now. The row stays, `$matter->trashed()` becomes `true`, and normal queries, relations and `whereHas()` stop seeing it. `Matter::withTrashed()` removes that filter, `onlyTrashed()` replaces it with `whereNotNull`, and `withoutTrashed()` restores the default. `$matter->restore()` sets `deleted_at` back to `null` and saves; `Matter::onlyTrashed()->where(...)->restore()` does it in bulk. `forceDelete()` on a model issues a real `DELETE`. On the builder, `Matter::where(...)->delete()` soft-deletes every match in one `UPDATE`, while `->forceDelete()` hard-deletes them.
code
php · 14 lines<?php
use App\Models\Matter;
$matter->delete(); // UPDATE matters SET deleted_at = now()
Matter::find($matter->id); // null: hidden by SoftDeletingScope
$matter->trashed(); // true
$archive = Matter::onlyTrashed()->latest('deleted_at')->paginate();
Matter::withTrashed()->findOrFail($id)->restore(); // deleted_at = null
Matter::where('status', 'draft')->delete(); // bulk soft delete, one UPDATE
$matter->forceDelete(); // real DELETEgo deeper
Recall that SoftDeletes turns delete() into a deleted_at timestamp and that withTrashed() and onlyTrashed() bring trashed rows back into queries.
Explain the SoftDeletingScope, builder-level soft deletes, restore and forceDelete, and how relations treat trashed rows.
Plan for unique indexes, missing cascades, bulk deletes without events and table growth, and decide between a status column and soft deletes.
Set data-retention policy: what is soft-deleted, what is purged when, and how legal holds and audits interact with deletion.
## What soft deleting means in Eloquent In a legal-case manager, "delete" rarely means destroy: a closed or mistaken matter must stay recoverable and auditable. **Soft deleting** marks a row as deleted with a timestamp instead of removing it. In Eloquent it takes the `SoftDeletes` trait on the model and a nullable `deleted_at` column on the table. ## What changes when the trait is added Reading the trait and its scope in the framework source: 1. **A global scope is registered.** `SoftDeletingScope::apply()` adds `whereNull(<table>.deleted_at)`, so every Eloquent query of the model hides trashed rows. 2. **`delete()` becomes an update.** On a model, it sets `deleted_at` (and `updated_at` when timestamps are on) to the current time with an `UPDATE`, fires the usual deleting and deleted events plus a `trashed` event, and keeps the row. 3. **Builder deletes become updates too.** The scope installs an `onDelete` handler, so `Matter::where('status', 'draft')->delete()` runs one `UPDATE ... SET deleted_at = now()` for all matches. 4. **Builder methods are added:** `withTrashed()`, `withoutTrashed()`, `onlyTrashed()`, `restore()`, `restoreOrCreate()` and `createOrRestore()`. ## The API at a glance | Call | Effect | |---|---| | `$matter->delete()` | sets `deleted_at`; row stays | | `$matter->trashed()` | `true` when `deleted_at` is not null | | `Matter::withTrashed()->find($id)` | finds live or trashed | | `Matter::onlyTrashed()->get()` | only trashed rows (an archive view) | | `$matter->restore()` | sets `deleted_at` to null and saves | | `$matter->forceDelete()` | real `DELETE` for this row | | `Matter::forceDestroy($ids)` | loads each by ID, including trashed, and force-deletes it | | `Matter::where(...)->forceDelete()` | real `DELETE` in one query | Quiet variants, `forceDeleteQuietly()` and `restoreQuietly()`, skip model events. ## Effects on relations and filters - `$client->matters` excludes trashed matters, because relation queries apply the related model's global scopes. - `Client::whereHas('matters')` ignores trashed matters in its subquery for the same reason. - A `belongsTo` whose parent is trashed returns `null`; add `->withTrashed()` to the relation definition when a document must still show its archived matter. - Queries through `DB::table('matters')` see every row, trashed or not: the scope is Eloquent-only. ## Common traps - **Unique columns.** A unique index on `reference_number` still counts trashed rows, so reusing the number of an archived matter fails. Include `deleted_at` in the index design, or restore instead of recreating. - **Cascades.** A foreign key's `ON DELETE CASCADE` never fires on a soft delete, because nothing is deleted. Soft-deleting a matter leaves its documents live unless you delete them too. - **Builder `forceDelete()` skips global scopes.** It runs the base query directly, so a tenant filter or any other global scope does not apply, and without `onlyTrashed()` it deletes live and trashed rows alike; direct `where` clauses and `onlyTrashed()` itself are kept. - **Growth.** Trashed rows accumulate forever unless something prunes them. ## Working with the archive - **Archive listing:** `Matter::onlyTrashed()->latest('deleted_at')->paginate()` sorts by when each matter was archived. - **Bulk restore:** `Matter::onlyTrashed()->where('client_id', $clientId)->restore()` clears `deleted_at` for every match in one `UPDATE`, without per-model events. - **Checking one record:** `Matter::withTrashed()->findOrFail($id)` followed by `$matter->trashed()` tells an archived matter from a live one. - **Creating or reviving:** `Matter::createOrRestore(['reference_number' => $ref], [...])` restores a trashed match instead of inserting a duplicate. - **Cleanup:** trashed rows are removed for good only by `forceDelete()` or by pruning. ## When to use it Use soft deletes where recovery, audit or legal hold matters. Do not use it as a substitute for an explicit status: an "archived" matter that users still browse is often clearer as `status = archived` than as a trashed row that every query must opt into.
- Does Matter::where('status', 'draft')->delete() fire model events for each soft-deleted matter?No. The soft-delete scope replaces the builder's delete with a single `UPDATE` setting `deleted_at`, so no models are loaded and no per-model events fire. To run events, load the models and call `delete()` on each, for example in a chunked loop.
- How do you show a document whose parent matter has been soft-deleted?Define the `belongsTo` relation with `->withTrashed()`, as in `return $this->belongsTo(Matter::class)->withTrashed();`. The relation query then removes the soft-delete scope, so `$document->matter` returns the archived matter instead of `null`.
A soft delete is moving a case file to the archive shelf rather than the shredder: it disappears from the day-to-day cabinet, anyone can fetch it back with the right request, and only an explicit shredding order destroys it.
saying these in an interview costs you the question
- Believing delete() removes the row once SoftDeletes is used
- Expecting ON DELETE CASCADE to run on a soft delete
- Thinking relations still return trashed children by default
- Assuming unique indexes ignore soft-deleted rows
- Saying DB::table() queries hide trashed rows