Currently Available: Need a skilled Software Developer for your next project?
Categories
Laravel

How Laravel Route Model Binding Works

In a Laravel application, a URL often contains an identifier such as /posts/42. Route model binding uses that URL value to find an Eloquent model, a PHP object that represents a database record, before the controller action runs. The controller receives the model object, so it does not have to look up the record itself.

By convention, Laravel matches a route’s placeholder to a typed controller parameter, a controller argument whose declared type names the model. For example, {post} in the route matches Post $post in the action. The request passes through middleware, code that handles a request before the controller action, and Laravel replaces the placeholder value with a model. This mechanism also supports custom lookup fields, responses for missing models, and explicit rules for cases that do not follow the convention.

From URL to controller argument

A route placeholder captures part of the URL. In this example, {post} captures the value from a path such as /posts/42:

use App\Models\Post;
use Illuminate\Support\Facades\Route;

Route::get('/posts/{post}', [PostController::class, 'show']);

class PostController
{
    public function show(Post $post)
    {
        return $post;
    }
}

Laravel first matches the incoming request to this route. The route parameter initially holds the value captured from the URL, such as the string "42". Laravel then runs the route’s middleware, code that handles a request before the controller action.

The SubstituteBindings middleware first applies bindings registered explicitly in code, then resolves implicit bindings. For implicit binding, Laravel matches a typed model parameter in the controller action to a route placeholder by name, then looks up the model using the placeholder value. The framework’s implicit binding resolver changes the route parameter from its raw URL value to the matching model object. The controller dispatcher then passes the resolved route parameters to the action.

When the route uses {post} and the action declares Post $post, Laravel matches the names and passes a Post instance as $post when show runs. The route must run this middleware for Laravel to replace the URL value with a model. Laravel’s binding middleware performs that lookup.

Choosing a lookup key

By default, Laravel checks the URL value against the model’s route key. The route key is typically the model’s primary key, commonly id. To look up a post by its slug for one route, specify the field in the placeholder:

Route::get('/posts/{post:slug}', [PostController::class, 'show']);

Laravel then searches for a Post whose slug matches the URL value. In Laravel 13, a model can also declare a default route key with the RouteKey attribute:

use Illuminate\Database\Eloquent\Attributes\RouteKey;

#[RouteKey('slug')]
class Post extends Model
{
    // ...
}

Set the field in the route placeholder when only one route needs a different lookup field. Set the model attribute when slug should be that model’s default route key. Laravel documents these options in its route model binding documentation.

Nested routes can also constrain a child lookup to its parent. For example, /users/{user}/posts/{post:slug} can resolve the post through the user’s posts relationship, so a post belonging to another user does not match that nested URL. Laravel uses relationship naming conventions for this scoped lookup. You can request scoping with scopeBindings() or disable it with withoutScopedBindings().

When Laravel cannot find a model

When no model matches the URL value, Laravel raises a model-not-found exception during binding. Laravel’s default response is HTTP 404, and the controller action receives no bound model.

A route can provide a different response with missing():

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;

Route::get('/locations/{location:slug}', [LocationsController::class, 'show'])
    ->missing(function (Request $request) {
        return Redirect::route('locations.index');
    });

Here, Laravel redirects to the locations index when binding cannot find the requested location. The binding middleware checks for this route-level handler when it catches the model-not-found exception.

By default, implicit binding excludes soft-deleted models, records marked as deleted but still kept in the database. Add withTrashed() to a route when that route should include them. Laravel documents both the missing-model handler and soft-delete option.

When to define a binding explicitly

Use implicit binding when the route placeholder and typed model parameter have matching names and the model’s route-key lookup does what you need. A slug alone does not require explicit binding because {post:slug} already selects that field.

Use explicit binding when you need to choose which model a route parameter represents or when the URL value needs custom lookup logic. Laravel's Route::model() associates a named parameter with a model class. You can use Route::bind() to register a function that receives the URL value and returns the model to bind. A model can also customize its lookup by overriding resolveRouteBinding(); nested child lookups can use resolveChildRouteBinding().

For example, Route::model('post', Post::class) associates the post route parameter with Post. Use Route::bind() or a model override when the lookup must follow rules specific to your application that the usual route-key lookup cannot express.

What I'm building

Delegate tasks. Get software.

Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.

Take a look at vroni.com

Email updates

Usually a new article and a few links I found interesting.

No spam. Unsubscribe with one click.

Leave a Reply

Your email address will not be published. Required fields are marked *