• Reading time ~ 9 min
  • 11.08.2023

In your Laravel projects, how do you go about setting up routes for a particular resource? Do you do it in the following manner?

Route::get('/articles/{id}', [ArticlesController::class, 'show']);

Then, do you implement it like this in your ArticlesController?

public function show($id)
{
	$article = Article::findOrFail($id);

	return view('articles.show', [
		'article' => $article,
	]);
}

If this is your usual process, Laravel offers a more efficient alternative called Route Model Binding . This feature automatically fetches the relevant model and passes it into your method. All you have to do is use the model’s name as a parameter placeholder:

Route::get('/articles/{article}', [ArticlesController::class, 'show']);

In the ArticlesController, you can then set it up like so:

public function show(Article $article) {
    return view('articles.show', [
        'article' => $article
    ]);
}

With this, whenever you access the URL, you’ll receive the expected details:

my-blog.local/articles/1

Customizing the Key

There might be instances where the ‘id’ isn’t the key you’re looking for. In our case, we’d like to locate an article using its slug. There are two ways to approach this:

  1. Specify the key in the placeholder
Route::get('/articles/{article:slug}', [ArticlesController::class, 'show']);

This will now query the ‘Articles’ model by slug rather than by id:

Article::where('slug', $value)->firstOrFail();
  1. Define a routeKeyName method in the Model:
class Article extends Model  {
    public function getRouteKeyName()  {
        return 'slug';
    }
}

The advantage of employing getRouteKeyName is its ease of use. You won’t have to type {article:slug} every time you define a route that requires an ‘Article’ instance.

Query Conditions

Route Model Binding also allows you to modify the way your model queries are executed. For instance, with our ‘Article’ model, we may only want to query articles that have been published. As it stands, even drafted articles could be retrieved if anyone correctly guessed the slug of a future article.

We can override the resolveRouteBinding method in our model to do this:

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

While we could bypass the $field and hardcode ‘slug’ as our key, it’s better to fall back on getRouteKeyName. This supports varying scenarios throughout our app and doesn’t restrict us to using only ‘slug’.

Scoping

Presently, we are displaying all articles as we only need a slug to fetch the necessary information. But what if we want to display an article under its respective author? This can be achieved simply by defining a new route that combines the author’s name and the article’s slug:

1Route::get('/author/{user:username}/articles/{article:slug}', [AuthorArticlesController::class, 'show']);

In the AuthorArticlesController, you would do the following:

1public function show(User $user, Article $article)
2{
3    return view('articles.show', [
4        'article' => $article,
5        'author' => $user
6    ]);
7}

Laravel will automatically ensure that the article is linked to the correct user. It does this by looking for the articles relation in the User model and fetching it as follows:

1$user->articles()->where('slug', $value)->firstOrFail();

This process happens automatically, but&mldr;## Without Scoping

But what if you don’t want to restrict articles to their authors? Take, for instance, a reader’s favorites list. How can you display this under their name?

1Route::get(
2	'/read/{user:username}/articles/{article:slug}',
3	[ArticlesController::class, 'show']
4)->withoutScopedBindings();

In this case, the withoutScopedBindings() method can be used. This will ensure the articles aren’t scoped to a particular user, allowing for broader access.

Custom Classes

There may also be instances where you want to bind objects that aren’t models, like a UUID object, for example.

Suppose we’re generating UUIDs for our invoices:

1Route::get('/invoices/{uuid}', [InvoiceController::class, 'show']);

We can create a binding of UUID to pass it as an object:

1public function show(LazyUuidFromString $uuid) {
2	// do something
3}

To convert the UUID string back into an object, we can define the binding in the AppServiceProvider:

1class AppServiceProvider extends ServiceProvider
2{
3    public function register(): void
4    {
5        Route::bind('uuid', function ($value) {
6            return Uuid::fromString($value);
7        });
8    }
9}

Route Binding is a powerful tool that can be used in many cases to simplify your controllers, making them easier to manage and maintain.

How It Works

Let’s explore a quick example to understand how we could implement this ourselves.

We could create a new middleware and set a binding for any route parameter named something Route::any('/load/{something}', ...)

 1class AutoBinding
 2{
 3    public function handle(Request $request, Closure $next): Response
 4    {
 5        /** @var Route $route */
 6        $route = $request->route();
 7        if($route->hasParameter('something')) {
 8            $route->setParameter('something', SomethingModel::findByUuid($route->parameter('something')));
 9        }
10        return $next($request);
11    }
12}

So this is the basic structure we need to set up a binding. Now, let’s see how Laravel does it.

Deep Dive into Route Binding

If you examine the $middlewareGroups array defined in app\Http\Kernel.php, you’ll notice the \Illuminate\Routing\Middleware\SubstituteBindings::class middleware is used for both the web and API. Let’s dive into it:

 1public function handle($request, Closure $next)
 2{
 3    try {
 4        $this->router->substituteBindings($route = $request->route());
 5
 6        $this->router->substituteImplicitBindings($route);
 7    } catch (ModelNotFoundException $exception) {
 8        if ($route->getMissing()) {
 9            return $route->getMissing()($request, $exception);
10        }
11
12        throw $exception;
13    }
14
15    return $next($request);
16}

The handle method performs two functions: substituteBindings and substituteImplicitBindings.

SubstituteBindings

The first function, substituteBindings, attempts to find any defined binding (much like how we used the Route::bind() method in the service provider):

 1// Illuminate\Routing\Router
 2public function substituteBindings($route)
 3{
 4    foreach ($route->parameters() as $key => $value) {
 5        if (isset($this->binders[$key])) {
 6            $route->setParameter($key, $this->performBinding($key, $value, $route));
 7        }
 8    }
 9
10    return $route;
11}

The performBinding function invokes the callback defined for your binders:

1protected function performBinding($key, $value, $route)
2{
3    return call_user_func($this->binders[$key], $value, $route);
4}

This process is very similar to the custom version we developed together in the AutoBinding middleware above.

SubstituteImplicitBindings

The second method, substituteImplicitBindings, is a bit more complicated as it attempts to gather everything needed to correctly replace the parameter:

1public function substituteImplicitBindings($route)
2{
3    ImplicitRouteBinding::resolveForRoute($this->container, $route);
4}

Let’s dig into the resolveForRoute method:

 1public static function resolveForRoute($container, $route)
 2{
 3    $parameters = $route->parameters();
 4
 5    $route = static::resolveBackedEnumsForRoute($route, $parameters);
 6
 7    foreach ($route->signatureParameters(['subClass' => UrlRoutable::class]) as $parameter) {
 8        if (! $parameterName = static::getParameterName($parameter->getName(), $parameters)) {
 9            continue;
10        }
11
12        $parameterValue = $parameters[$parameterName];
13
14        if ($parameterValue instanceof UrlRoutable) {
15            continue;
16        }
17
18        $instance = $container->make(Reflector::getParameterClassName($parameter));
19
20        $parent = $route->parentOfParameter($parameterName);
21
22        $routeBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
23                    ? 'resolveSoftDeletableRouteBinding'
24                    : 'resolveRouteBinding';
25
26        if ($parent instanceof UrlRoutable &&
27            ! $route->preventsScopedBindings() &&
28            ($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))) {
29            $childRouteBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
30                        ? 'resolveSoftDeletableChildRouteBinding'
31                        : 'resolveChildRouteBinding';
32
33            if (! $model = $parent->{$childRouteBindingMethod}(
34                $parameterName, $parameterValue, $route->bindingFieldFor($parameterName)
35            )) {
36                throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
37            }
38        } elseif (! $model = $instance->{$routeBindingMethod}($parameterValue, $route->bindingFieldFor($parameterName))) {
39            throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
40        }
41
42        $route->setParameter($parameterName, $model);
43    }
44}

There’s a lot going on in this method, so let’s break it down point by point to understand each line:

The first line retrieves the parameters array from the route, then the following steps occur:

Resolve Enums

Bindings are not only linked to Models, but they can also automatically resolve the enum values:

 1protected static function resolveBackedEnumsForRoute($route, $parameters)
 2{
 3    foreach ($route->signatureParameters(['backedEnum' => true]) as $parameter) {
 4        if (! $parameterName = static::getParameterName($parameter->getName(), $parameters)) {
 5            continue;
 6        }
 7
 8        $parameterValue = $parameters[$parameterName];
 9
10        $backedEnumClass = $parameter->getType()?->getName();
11
12        $backedEnum = $backedEnumClass::tryFrom((string) $parameterValue);
13
14        if (is_null($backedEnum)) {
15            throw new BackedEnumCaseNotFoundException($backedEnumClass, $parameterValue);
16        }
17
18        $route->setParameter($parameterName, $backedEnum);
19    }
20
21    return $route;
22}

The resolveBackedEnumsForRoute method retrieves the parameter name and then tries to get the value by using tryFrom(). If it can’t resolve the value, it throws a BackedEnumCaseNotFoundException.

Resolve Model Bindings

After the enums are resolved, Laravel performs another loop over all the parameters in the route action to get all UrlRoutable objects:

1foreach ($route->signatureParameters(['subClass' => UrlRoutable::class]) as $parameter) {

All Models in Laravel implement the UrlRoutable interface.

But once it gets the UrlRoutable parameters in the loop, it checks if the route parameter has already been resolved:

1$parameterValue = $parameters[$parameterName];
2
3if ($parameterValue instanceof UrlRoutable) {
4    continue;
5}

The next step is to resolve this new parameter and replace it with the corresponding Model instance. So, Laravel tries to infer the model name, then creates a new object:

1$instance = $container->make(Reflector::getParameterClassName($parameter));

The following line retrieves the parent of the model, if needed:

1$parent = $route->parentOfParameter($parameterName);

Note: The ‘parent’ here is related to the concept of ‘scoping’ we discussed earlier.

Next, Laravel determines which method to use based on whether we want to retrieve trashed records or not:

1$routeBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
2            ? 'resolveSoftDeletableRouteBinding'
3            : 'resolveRouteBinding';

At this stage, Laravel has all the information it needs. The final two parts are only about deciding whether to resolve the route from the instance or from its parent.

Retrieving an Instance from the Parent

The first condition checks if the parent exists, if it implements UrlRoutable, and if scoping is permitted. If all these conditions are met, it will call either resolveChildRouteBinding or resolveSoftDeletableChildRouteBinding:

 1if ($parent instanceof UrlRoutable &&
 2    ! $route->preventsScopedBindings() &&
 3    ($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))) {
 4    $childRouteBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
 5                ? 'resolveSoftDeletableChildRouteBinding'
 6                : 'resolveChildRouteBinding';
 7
 8    if (! $model = $parent->{$childRouteBindingMethod}(
 9        $parameterName, $parameterValue, $route->bindingFieldFor($parameterName)
10    )) {
11        throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
12    }

Then it executes a call to the parent:

1$model = $parent->{$childRouteBindingMethod}($parameterName, $parameterValue, $route->bindingFieldFor($parameterName))

which would resolve to a regular relation call like:

1$user->posts->find($value)

Resolving from the Instance

So if there is no parent or if withoutScope is enforced, then Laravel will execute a call to the model directly:

1} elseif (! $model = $instance->{$routeBindingMethod}($parameterValue, $route->bindingFieldFor($parameterName))) {
2    throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
3}

If the instance is not found, it will throw a ModelNotFoundException.

The last thing at the end is that it replaces the parameter:

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

And that’s all there is to Route Binding in Laravel.

Happy coding!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

ABOUT

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

About author CrazyBoy49z
WORK EXPERIENCE
Contact
Ukraine, Lutsk
+380979856297