In Laravel, when would you use Route::model(), Route::bind() or an overridden resolveRouteBinding() instead of plain implicit route model binding?
answer
- register in AppServiceProvider::boot()
- Route::model binds by segment name
- Route::bind: closure receives the raw value
- explicit binders run before implicit ones
- resolveRouteBinding($value, $field) on the model
basics
~20 sRoute::model() binds a segment name to a model class even without a type-hint; Route::bind() runs your own closure for a segment and can return anything; overriding resolveRouteBinding() changes the lookup for every top-level binding of that model.
solid answer
~40 sImplicit binding keys off the action signature. **`Route::model('task', Task::class)`**, registered in `AppServiceProvider::boot()`, binds every `{task}` segment to `Task` regardless of the action's type-hint, with an optional third closure for the not-found case. **`Route::bind('task', fn (string $value) => ...)`** runs arbitrary code — a case-insensitive lookup, an extra condition, even a non-model value — and must throw (for example with `firstOrFail()`) to produce a 404. Explicit binders run **before** implicit binding, which then skips any parameter that already holds a model. Overriding **`resolveRouteBinding($value, $field = null)`** on the model keeps custom logic with the model, so every top-level implicit binding of that class, and `Route::model()`, uses it; keep honouring `$field`, or `{task:slug}` routes silently stop working.
go deeper
Recall that Route::model and Route::bind are registered in AppServiceProvider::boot() and change how a named segment is resolved.
Explain the difference between binding a segment to a class, running a custom closure, and overriding resolveRouteBinding() on the model, and that explicit binders run first.
Keep binders global-by-name in mind, throw for not-found, preserve $field in overrides, and put shared rules in resolveRouteBindingQuery() so scoped child lookups inherit them.
Decide whether lookup rules such as tenant filters live in routing, in model binding hooks or in global scopes, balancing discoverability against accidental bypass.
## Where implicit binding stops Implicit binding is driven by the action's signature: a model type-hint plus a variable named after the segment, resolved with `where(routeKey, value)->first()`. That covers most routes. It is not enough when: - the action does not type-hint the model (an untyped closure parameter, or legacy code); - the lookup needs extra conditions — only non-archived tasks, a case-insensitive code, a tenant filter; - the value to inject is not an Eloquent model at all; - the same custom lookup must apply everywhere the model is bound. Laravel offers three explicit tools for these cases. ## `Route::model()`: bind a segment name to a class ```php public function boot(): void { Route::model('task', Task::class); } ``` From then on every `{task}` segment in every route resolves to a `Task` through the model's `resolveRouteBinding()`, whether or not the action type-hints it. A missing row throws `ModelNotFoundException` (a 404) unless you pass a third argument — a closure that receives the raw value and returns a fallback instead. ## `Route::bind()`: run your own resolver ```php Route::bind('task', function (string $value) { return Task::where('code', strtoupper($value)) ->whereNull('archived_at') ->firstOrFail(); }); ``` - The closure receives the segment's raw value (and the route) and returns whatever should be injected. - To get a 404 it must throw; `firstOrFail()` throws `ModelNotFoundException` for you. - Hyphens in the key are normalised to underscores. - Because the binder is keyed by segment name, it applies to every route using `{task}`. Name segments deliberately, or one binder changes routes you did not intend. ## Order: explicit before implicit The binding middleware runs explicit binders first, then implicit binding. Implicit binding skips any parameter that already holds a model, so an explicit binder wins for its segment and no second query runs. `missing()` on the route catches a `ModelNotFoundException` from either stage. ## Overriding `resolveRouteBinding()` on the model When the custom rule belongs to the model rather than to a URL, override the method Laravel calls for every binding of the class: ```php public function resolveRouteBinding($value, $field = null) { return $this->where($field ?? $this->getRouteKeyName(), $value) ->whereNull('archived_at') ->first(); } ``` - Top-level implicit binding and `Route::model()` both call it, so the rule applies to every unscoped lookup of the model. - Returning `null` still produces the standard 404. - **Honour `$field`**: an override that hard-codes a column ignores `{task:slug}` on routes that ask for it. - For query-only tweaks, overriding `resolveRouteBindingQuery($query, $value, $field)` is narrower and keeps the rest of the pipeline intact. - Scoped child lookups do not call the child's `resolveRouteBinding()`; they go through the parent's `resolveChildRouteBinding()`, which builds the query with the child's `resolveRouteBindingQuery()`. A rule placed in `resolveRouteBindingQuery()` therefore covers both paths. ## A worked example: case-insensitive task codes A project tracker shows tasks as `/tasks/PRJ-104`, but users type `prj-104` into the address bar. Implicit binding compares the string as-is, so the lowercase URL 404s on a case-sensitive column. A `Route::bind('task', ...)` closure that upper-cases the value before querying fixes every route using `{task}` at once. Beware one interaction: on a route with scoped bindings, the explicit binder runs first and implicit binding then skips the parameter, so the task is no longer scoped to its project. If the rule must coexist with scoping, put it in the model: override `resolveRouteBindingQuery()` on `Task`, which Laravel uses for top-level lookups and, through the parent's relationship, for scoped child lookups too. ## Choosing | Need | Tool | |---|---| | Bind a segment to a model without relying on type-hints | `Route::model()` | | One-off custom lookup or a non-model value for a segment name | `Route::bind()` | | A rule that must hold for every binding of the model | Override `resolveRouteBinding()` or `resolveRouteBindingQuery()` | | Just a different column | `{task:slug}` or a model route key — no explicit binding needed | The docs place explicit binders at the start of `AppServiceProvider::boot()`. Keeping them together there also makes the global, name-wide effect of each binder easy to audit.
- Why should a Route::bind() closure throw, rather than return null, when the record is missing?Returning `null` does not reliably produce a 404: it just sets the parameter to null, so an untyped closure parameter receives null and the action runs anyway, while a typed one depends on a second, implicit lookup. Throwing `ModelNotFoundException`, typically via `firstOrFail()`, gives the uniform 404 or `missing()` response.
- What goes wrong when a resolveRouteBinding() override ignores its $field argument?Routes that request a specific binding field, such as `{task:slug}`, pass that field in `$field`. An override that always queries `code` or `id` discards it, so those routes compare the slug against the wrong column and usually answer 404. Use `$field ?? $this->getRouteKeyName()`.
saying these in an interview costs you the question
- Route::model() only works when the action type-hints the model
- Implicit binding runs first and explicit binders refine its result
- Route::bind() closures can only return Eloquent models
- Route::bind() applies to a single route only
- resolveRouteBinding() overrides can safely ignore the $field argument