In Laravel, how do withTrashed(), missing() and a backed-enum route parameter change what a bound route does when the lookup fails or finds a soft-deleted row?
answer
- SoftDeletes rows 404 by default
- withTrashed() on the route definition
- missing(fn ($request, $e) => ...)
- missing() only catches ModelNotFoundException
- enum: tryFrom, else 404
basics
~20 sSoft-deleted models 404 unless the route calls withTrashed(). missing() swaps the not-found 404 for your own response, such as a redirect. A backed-enum parameter binds only valid cases; anything else is a 404 that missing() does not intercept.
solid answer
~40 sA model using `SoftDeletes` is bound through its global scope, so a trashed task is "not found" and the route answers 404. `->withTrashed()` on the route switches binding to a query that includes trashed rows, for every soft-deletable model bound on that route — handy for a restore endpoint. `->missing(function (Request $request, ModelNotFoundException $e) { ... })` catches the `ModelNotFoundException` thrown by implicit or explicit binding and returns your response instead, such as a redirect back to the project's task list. A parameter type-hinted with a backed enum is bound via `tryFrom()`; an invalid value throws `BackedEnumCaseNotFoundException`, which the exception handler renders as a 404 — and `missing()` does not catch it, because it only handles `ModelNotFoundException`.
code
php · 13 lines<?php
use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;
Route::patch('/tasks/{task}/restore', [TaskController::class, 'restore'])
->withTrashed();
// TaskController::restore(Task $task)
// {
// $task->restore();
// return back();
// }go deeper
Recall that a soft-deleted model 404s unless the route calls withTrashed(), and that an invalid enum segment is also a 404.
Explain how withTrashed() swaps the binding query, what the missing() closure receives, and why missing() ignores enum failures.
Scope withTrashed() to the routes that need it, keep APIs on honest 404s, and authorize access to archived records.
Define how deleted and archived records surface in URLs — 404, redirect or archive page — consistently across web and API clients.
## Three ways a bound route can "not find" something In a project tracker, the task routes meet three kinds of failed or unusual lookups: 1. The task was **soft-deleted** (archived) and someone opens its old URL or wants to restore it. 2. The task **never existed** or was hard-deleted. 3. A filter segment such as `/tasks/status/{status}` holds a value that is **not a valid status**. Laravel's route model binding handles each one differently. ## Soft-deleted models and `withTrashed()` A model that uses the `SoftDeletes` trait has a global scope adding `deleted_at is null` to its queries. Binding goes through that scope, so a trashed task behaves exactly like a missing one: **404**. Calling `->withTrashed()` on the route changes that: - Binding switches to `resolveSoftDeletableRouteBinding()`, which runs the same query with `withTrashed()`. - It applies to **every soft-deletable model bound on that route**, including a scoped child and models bound with `Route::model()`. - Models without `SoftDeletes` are unaffected. Typical uses are a restore endpoint (`PATCH /tasks/{task}/restore`) or an "archived task" page. Because the route now exposes deleted records, check authorization and render trashed items clearly. ## Custom not-found behaviour with `missing()` By default, a failed model binding throws `ModelNotFoundException`, rendered as a 404. The `missing()` method on a route definition lets you respond differently: ```php Route::get('/projects/{project}/tasks/{task}', [TaskController::class, 'show']) ->scopeBindings() ->missing(function (Request $request) { return redirect()->route('projects.index') ->with('status', 'That task no longer exists.'); }); ``` - The closure receives the **request** and the **`ModelNotFoundException`**. - It runs for failures from both explicit binders and implicit binding on that route. - Whatever it returns is sent instead of the 404; the action never runs. - Use it for user-friendly flows — a redirect with a flash message after a deleted task link — not to hide missing records from APIs that should report a 404. ## Backed-enum parameters A route action can type-hint a **backed enum**: ```php enum TaskStatus: string { case Open = 'open'; case Done = 'done'; } Route::get('/tasks/status/{status}', fn (TaskStatus $status) => $status->value); ``` - Laravel converts the segment with `TaskStatus::tryFrom($value)` and injects the case. - An invalid value throws `BackedEnumCaseNotFoundException`, which the exception handler maps to a **404**. - The docs describe string-backed enums; they are the natural fit for URL segments. - `missing()` does **not** run for an enum failure, because it catches only `ModelNotFoundException`. ## Summary | Situation | Default outcome | How to change it | |---|---|---| | Soft-deleted model | 404 | `->withTrashed()` on the route | | Missing model | 404 via `ModelNotFoundException` | `->missing(fn ($request, $e) => ...)` | | Invalid enum value | 404 via `BackedEnumCaseNotFoundException` | Customise exception rendering; `missing()` is not involved | ## Order of evaluation Inside the binding middleware the steps run in a fixed order, which explains most surprises: 1. **Explicit binders** registered with `Route::model()` or `Route::bind()` run first, parameter by parameter. 2. **Backed-enum parameters** are converted next; an invalid value throws immediately. 3. **Implicit model bindings** run last, left to right, each child scoped by its parent when scoping is on. Only a `ModelNotFoundException` thrown in steps 1 or 3 is routed to the `missing()` closure; everything else goes to the exception handler. Because the steps run before the action, a feature test that requests a trashed task, a nonexistent task and an invalid status should see three predictable outcomes without any controller code executing. ## Pitfalls - Adding `withTrashed()` to a whole group because one route needs it, and exposing archived records on every route in it. - Using `missing()` to redirect API clients, who then receive a 302 and HTML instead of a 404. - Expecting `missing()` to handle a bad enum value and shipping a raw 404 page for it.
- A Laravel route type-hints TaskStatus $status and has ->missing(...). Why does /tasks/status/bogus still show the plain 404?Enum binding throws `BackedEnumCaseNotFoundException`, while the binding middleware only hands `ModelNotFoundException` to the `missing()` closure. The enum exception propagates to the exception handler, which renders it as a 404. Customise that exception's rendering if you need a different response.
- Does withTrashed() on a scoped route also let a soft-deleted child task bind?Yes. When the route allows trashed bindings, Laravel uses the soft-deletable variants for both top-level and scoped child lookups, so a trashed task under its project binds instead of returning a 404, as long as the task model uses `SoftDeletes`.
saying these in an interview costs you the question
- Soft-deleted models are bound like any other row
- missing() also handles invalid enum values
- withTrashed() makes the model include trashed rows in every query
- A failed binding returns 410 Gone for soft-deleted rows
- missing() receives only the missing segment value