Demystifying Route Model Binding in Laravel

Route Model Binding

When I first started working with Laravel, one of the very first piece of code that I came across was similar to following:

//routes/web.php
Route::get('/post/{post}', 'PostsController@show');
//PostsController.php
public function show(Post $post) {
return view('posts.show', [
'post' => $post
]);
}

So when I went to URL /post/1, the Post with id 1 was available inside the controller as variable $post. I didn’t had to fetch myself anything from the Database. If I entered a URL with id that did not existed in the Database, I was even redirected to a 404 Page. Even though everything was working fine, I had no clue what was happening. Since I was new to Laravel, I didn’t know what this feature was and where to look into documentation as well.

Later on I came to know that this particular feature is known as Route Model Binding. And it turns out if the variable name provided in the Route matches the parameter name passed to the controller, then Laravel will automatically fetch the record from the DB based on the type-hinted Eloquent Model.

Below is the Variable defined in the route.

Route::get('/post/{post}', 'PostsController@show');

This Variable must match the following Parameter in the controller for Route Model Binding to work.

public function show(Post $post) {

Without Route Model Binding, our code would have looked something as follows:

public function show($id) {    $post = Post::find($id);
if( !$post) {
abort(404);
}
return view('posts.show', [
'post' => $post
]);
}

With Route Model Binding, it is super compact without affecting the readability. I was intrigued by how Laravel was managing it behind the scenes. So I took a dive into the code.

Turns out Laravel has a Middleware known as SubstituteBindings. It is located at:

\Illuminate\Routing\Middleware\SubstituteBindings.php

Now remember this Middleware runs before the request is handled by the controller. This Middleware had methods to handle both Implicit Bindings and Explicit Bindings. Explicit bindings were simply referred to as bindings. More on them later. The above example was of Implicit Binding. Diving more into the code I reached the class ImplicitRouteBinding which had a method called as resolveForRoute, where all the magic was happening.

Illuminate\Routing\ImplicitRouteBinding.php

This method loops through all the parameter passed to the controller. Remember Route Model Binding only works if the Parameter Name matches the Route Name. That was first condition being checked within the loop.

if (! $parameterName = 
static::getParameterName($parameter->name, $parameters)) {
continue;
}

If both the name matches, Laravel tries to fetch the corresponding Model based on the Type-Hinting we did in controller.

$instance = $container->make($parameter->getClass()->name);

Then it proceeds to fetch the record from the Model using below code.

$model = $instance->resolveRouteBinding($parameterValue, $route->bindingFieldFor($parameterName);

Here the first parameter is the value in the URL, which is 1. Second parameter was empty for our example. Now if we check the corresponding method in the Model.

public function resolveRouteBinding($value, $field = null)
{
return $this->where($field ?? $this->getRouteKeyName(), $value)->first();
}

Now it started to make sense. This is where the Laravel was fetching the row from Database based on the id. If the record was not found, it threw an exception as well.

throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);

And finally if all went fine, it override the value of the parameter using the row that we got from DB.

$route->setParameter($parameterName, $model);

So this is how the DB Record was being available inside the controller using Route Model Binding.

Now couple of observation looking at the method resolveRouteBinding()

Route::get('/post/{post:slug}', 'PostsController@show');

This way it would look for slug column in the Post Table.

So that is how the Implicit Binding worked. However, lets imagine a common scenario. You only want to display the Post when the Post is published. Lets say there is a column status in the Post Model to identify the same. You could then check for the status as below and abort as below:

public function show(Post $post) {
if( $post->status != 1) {
abort(404);
}
return view('posts.show', [
'post' => $post
]);
}

However, a better approach is to use Explicit Binding. You need to define the Explicit Binding in the boot method of the RouteServiceProvider like below:

Route::bind('activePost', function ($slug) {
return \App\Post::where('slug', $slug)
->where('status', 1)->firstOrFail();
});

We pass a key, activePost, to the bind() method of the Route. Make sure that this key is unique. And then we pass a closure where we define the logic to fetch the record from the Database. Here we are looking for the column Slug as well as making sure that status column has value 1, indicating its published. We then need to tell Router that we are going to use Explicit Binding for this Route.

Route::get('/post/{activePost:slug}', 'PostsController@show');

This Explicit Binding is also handled by the Middlware SubstituteBindings by calling the substituteBindings() method of the Router. Please take note that in this case we do not need to make sure that our Route Parameter, which is named as activePost, matches the Controller Parameter Name. We can keep our Controller Parameter as $post itself. We just need to make sure that our key passed to bind method of Route is unique.

So that is how Router Model Binding works behind the scene, keeping your Controllers Clean and your code maintainable. Just one of the many features of Laravel that made me fall in love with it.

You can view the working example of different types of Route Model Bindings by playing with this Laravel Playground, which is a free to use Service to try your Laravel Code directly in the Browser. You can also copy the link from below:

https://laravelplayground.com/#/snippets/a8cd46db-c2cd-4658-acdf-7f6aab55ad22

Freelancer Developer.