Route Model Binding Alternatives in Laravel

A middleware-based approach to route parameter resolution.

June 23, 2023

Route model binding is a feature in the Laravel PHP framework that allows you to bind a route parameter to an Eloquent (ORM) model. It’s a way of automatically injecting model instances into controller actions by resolving route parameter values in URIs. This allows a URI path like /users/1 to map to a controller action receiving a model instance for user ID 1 without requiring any lookups for that user to be done in the controller.

Binding can be achieved in one of two ways: implicitly or explicitly. Implicit binding works by automatically matching a parameter to a type-hinted parameter in a controller action, while explicit binding works by specifying how to map a parameter name to a model (using Route::model) or providing custom resolution logic (using Route::bind). Examples of both approaches are given in the docs.

The problem

While this is a convenient feature that can help reduce the burden on controllers (and perhaps form requests, depending on your application) to resolve dependencies, it has a few drawbacks.

The first is that implicit binding is highly abstract (to the point of being considered ‘magic’): the link between the substring corresponding to the model in the route pattern and the model instance received by the controller action is extremely tenuous, and without knowing about this feature in the first place, it may be very difficult to understand the behaviour.

The second is that even with explicit binding (which goes some way towards mitigating the above issue), bindings are not scoped or namespaced in any way; they’re globally registered on the router and the router will attempt to apply them to every single route registered in your app (even the ones that don’t contain the corresponding parameters, which is probably most of them). This is especially an issue when using the feature in a package, because packages can’t know or presume anything about the routes that might be registered in the host application, which can in turn lead to conflicts when resolving parameters with name collisions—but it’s also an issue for routes defined in an application, because a package with conflicting parameter names may be introduced later and cause issues (although unlikely).

One way to mitigate that second issue is to choose very specific parameter names that are not likely to clash. This approximates some kind of namespacing mechanism, but it’s crude and not a guarantee. So what’s the solution?

The route model binding feature could probably be improved through PRs. I’m not sure exactly what form improvements should take, but they would probably include a way of scoping bindings to a subset of routes by substring matching (e.g. “all routes beginning with /user”) or matching route names using wildcards. Taking one of the explicit binding examples from the docs and imagining how the syntax might be improved, it could look like this:

// Assuming all user routes are named with the `user.*` pattern
Route::model('user', User::class)->on('user.*');

But even that wouldn’t be ideal, because it would require its own validation or error handling (what should happen if the route matching doesn’t actually match any defined routes?) and adds complexity to an already complex router.

In fairness, I don’t think the problems I’ve described in this post are by any means common. I’ve used route model binding in numerous projects without trouble. But that doesn’t eliminate the possibility of it leading to problems further down the line—potentially ones that aren’t very easy to debug.

The solution

One of the issues I mentioned above is the potential for conflicts between multiple bindings for the same parameter name. I encountered this exact problem in a package I maintain some months ago, and ultimately decided to replace all usages of route model binding with a custom middleware solution. This makes a lot of sense for many reasons:

  • It leverages another existing feature of the framework
  • It provides an intuitive place to contain all of your binding logic
  • It removes the responsibility of managing bindings from your router calls, so they can deal exclusively with defining routes and their groups
  • Middleware can be applied as granularly as required, so you get the ability to scope your bindings for free
  • It’s barely any more effort to implement or maintain than explicit binding

Here’s an example of how an implementation might look for resolving user-related parameters:

<?php

namespace App\Http\Middleware;

use App\Models\User;
use App\Models\UserGroup;
use App\Models\UserProfile;
use Closure;
use Illuminate\Http\Request;

class ResolveUserParameters
{
    public function handle(Request $request, Closure $next)
    {
        $parameters = $request->route()->parameters();

        if (array_key_exists('user', $parameters)) {
            $user = User::findOrFail($parameters['user']);
            $request->route()->setParameter('user', $user);
        }

        if (array_key_exists('user_group', $parameters)) {
            $group = UserGroup::findOrFail($parameters['user_group']);
            $request->route()->setParameter('user_group', $group);
        }

        if (array_key_exists('user_profile', $parameters)) {
            $profile = UserProfile::findOrFail($parameters['user_profile']);
            $request->route()->setParameter('user_profile', $profile);
        }

        // Resolve more parameters as needed

        return $next($request);
    }
}

In this example, the resolution logic is identical for all three parameters. This could be refactored into something a little less repetitive, but there may be cases where you need to do something with the query or result before setting the parameter value, such as authorising an action to determine if additional data (such as soft-deleted rows) should be included. If your application has a large number of route parameters that are all resolved in the same way, you may find it preferable to write a base class for this type of middleware with methods for resolving parameters using a map of keys to model class names (which would not be dissimilar to implicit route model binding, but at least the logic would be adjacent to the middleware in your application source).

You can also divide the middleware up based on model, route group, or anything else that makes sense for your situation.

It’s also worth mentioning that if your project is simple enough to only need a handful of model lookups for a small set of routes, receiving the raw parameter values in your controller actions and manually performing the lookups in them is an acceptable solution, but the middleware approach is a scaleable alternative worth considering.


Update (2023-06-26): previously, I used a more generic middleware example that didn’t convey the intention very well. I’ve replaced it with a more specific example and elaborated on it.