Laravel has many undocumented features, and unfortunately, unless you're dedicating large chunks of time to digging through the source code and tracking what's going on, these features are likely to pass you by. Fortunately, I have done, and continue to do, exactly that.

Most of you reading this will be familiar with the concept of Laravel's route model binding, but for those who aren't, allow me to explain. When a route is being handled, if that route has a parameter whose name matches a parameter on its handler, which is typed as a model, Laravel will attempt to resolve the model using the value from the URL.

Take this route, with its user route parameter, and a handler parameter user , with the type of AppModelsUser .

use App\Models\User;

Route::get('/profile/{user}', function (User $user) {
    //
});

When someone visits https://mysite.com/profile/1, this route will be hit, and the function that handles the route will receive an instance of User that represents the user with ID 1. You could also define the route parameter like {user:username}, so you could instead use https://mysite.com/profile/ollieread, and have the user resolved by the username field/column.

Laravel's documentation covers this in more detail, and even briefly touches on how the model is actually resolved. If you aren't familiar with the ins and outs of this feature, I highly recommend reading the docs before continuing with this article.

Assuming that you've either done that, or don't need to, let's continue.

How it Works

Anyone who has created a route within Laravel and either not attached the web middleware group, or created a custom middleware group, knows that there's a very important piece of middleware when it comes to route model binding. I'm talking about the IlluminateRoutingMiddlewareSubstituteBindings middleware which is responsible for resolving all bindings, whether implicit or explicit.

The most important bits of this middleware, for our current purpose at least, are lines 40-41 .

$this->router->substituteBindings($route);
$this->router->substituteImplicitBindings($route);

The first line deals with explicit bindings , and the second deals with implicit bindings . Explicit bindings are where you explicitly tell Laravel specifically how to resolve a route parameter, and it's the way that we're encouraged to handle non-model route parameters that are not injectable through the container.

It's only Fair

I think it's only fair that readers of this article should nod their head in recognition of the fact that I did not make a very obvious, and easy joke about the usage of the term explicit.

After all, I am a professional.

However, we don't care about explicit bindings, we care about the implicit ones, the ones where the binding resolution is implied (read: magic). Because, what if we're writing an ORM as an alternative to Eloquent, and we want the objects in there to be automatically resolvable for routes? Or perhaps we're using objects to represent static data on the filesystem, and we want to be able to use them in routes?

So how exactly does it work?

The Magic of Implicit Bindings

It all obviously starts with the call to IlluminateContractsRoutingRegistrar substituteImplicitBindings , which by default is implemented by IlluminateRoutingRouter , which you can see here and below.

public function substituteImplicitBindings($route)
{
    $default = fn () => ImplicitRouteBinding::resolveForRoute($this->container, $route);

    return call_user_func(
        $this->implicitBindingCallback ?? $default, $this->container, $route, $default
    );
}

This method offloads the job of resolving the implicit bindings to IlluminateRoutingImplicitRouteBinding whose full source can be found here .

Complete Customisation

Nine times out of ten, your implicit bindings will be handled this way, though as you can see from this method, it's possible to change that. If you call IlluminateRoutingRouter substituteImplicitBindingsUsing with a Closure , it will be used to handle the binding. The callback will receive the following arguments in the following order.

  • IlluminateContainerContainer - The typing is never explicitly stated, but unless you're doing something very different, or only using parts of Laravel, this will actually be an instance of IlluminateFoundationApplication .
  • IlluminateRoutingRoute - An object representing the route that is being handled.
  • Closure - A callback for the default handling of implicit bindings, which accepts no arguments and calls IlluminateRoutingImplicitRouteBinding resolveForRoute .

This is a very good way to customise this logic, though it's only really useful if you're able to write a callback that can handle a large number of custom classes without having to update it for each one. If you're having to add every new class to your custom handler, it's just implicit bindings with extra steps.

The method that handles the parameter resolution is IlluminateRoutingImplicitRouteBinding resolveForRoute , which you can see here . The method itself is huge, so I'm not going to go through it all here, but I will highlight a few key parts that are important to our purposes.

Version Discrepancy

The code in this article is taken from the latest version of Laravel, which is v12.41.1 , so some of the code may not match what you have in front of you. This particular class was modified slightly in v12.19.0 .

On line 29 the method loops over the return from a call to IlluminateRoutingRoute signatureParameters which in this instance, will return all parameters from the route handler whose type is a subclass of IlluminateContractsRoutingUrlRoutable .

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

This is actually where we could stop, because we've reached the bit we need to know about, which is the IlluminateContractsRoutingUrlRoutable interface. Feel free to skip to the next section, though there are a couple of additional bits that, while not massively relevant right now, are useful to know.

Handler parameters whose name does not match the route parameter name, which are checked here on lines 30 through 32, are skipped. The IlluminateRoutingImplicitRouteBinding getParameterName method will only return a valid value if the handler parameter's name exactly matches a route parameter, or whose name is exactly the same when converted to snake case using IlluminateSupportStr snakeCase .

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

The final bit of this method that's worth noticing may possibly save you a headache or two when debugging in the future. On lines 36-38 there's a short-circuiting statement that skips the rest of the loop's iteration if the route parameter is already an instance of IlluminateContractsRoutingUrlRoutable . This is primarily here so that binding resolution doesn't generate multiple unnecessary queries, attempting to implicitly resolve a route parameter that has already been resolved explicitly.

if ($parameterValue instanceof UrlRoutable) {
    continue;
}

It should be noted that the specific way that this handles existing values allows you to both explicitly and implicitly resolve the same parameter. For example, if the route parameter should contain a UUID, you could add an implicit binding that validates and converts it to an instance of RamseyUuidUuidInterface . If the matching handler parameter is a model, this value will be passed to the appropriate methods, all of which accept mixed .

Taking the Model out of Route Model Binding

Making a custom non-model class work with implicit route parameter binding is as simple as implementing the IlluminateContractsRoutingUrlRoutable interface. Before you get to that, let's take a quick look at the methods on the interface to help avoid any possible confusion or oddities.

The getRouteKeyName Method

This isn't the first method in the interface, but it should be mentioned before the actual first method. It is responsible for returning the name of the column, attribute, or field that contains the route key. Its default implementation , IlluminateDatabaseEloquentModel getRouteKeyName returns the name of the attribute containing the primary key. It's also present on IlluminateHttpResourcesJsonJsonResource using the IlluminateHttpResourcesDelegatesToResource trait, whose implementation proxies the call to the underlying resource.

The only usages of this method are in the IlluminateDatabaseEloquentModel class, and specifically relate to implementations of the IlluminateContractsRoutingUrlRoutable interface. They are IlluminateDatabaseEloquentModel getRouteKey and IlluminateDatabaseEloquentModel resolveRouteBindingQuery . Your custom class most likely won't need to care about this method, though you do still need to implement it. Its docblock specifies the type string , but the method itself has no return type.

My Recommendation

If the concept of a "route key name" doesn't apply to your implementation of IlluminateContractsRoutingUrlRoutable , I suggest throwing an exception inside this method, to avoid any odd cases where it's called. If the exception is thrown, it's a good indicator that something somewhere has gone wrong.

The getRouteKey Method

This method should return the value that will be used to resolve the route parameter. Its default implementation , IlluminateDatabaseEloquentModel getRouteKey returns the model's primary key via IlluminateDatabaseEloquentModel getRouteKeyName . The IlluminateHttpResourcesJsonJsonResource uses the IlluminateHttpResourcesDelegatesToResource trait, which also implements this method , and just proxies it to the resource.

This method is used by the IlluminateRoutingUrlGenerator class when formatting parameters , which allows you to provide instances of IlluminateContractsRoutingUrlRoutable , such as models, when generating URLs. It's also used by the URI helper class IlluminateSupportUri when providing query parameters .

The resolveRouteBinding Method

The IlluminateContractsRoutingUrlRoutable resolveRouteBinding method is arguably the most important one. It's where the actual resolution of the final resource happens. It has a default implementation on the IlluminateDatabaseEloquentModel class and IlluminateHttpResourcesDelegatesToResource trait. As you can imagine, the one on the trait proxies, but the one on the model calls IlluminateDatabaseEloquentModel resolveRouteBindingQuery , which adds a where clause and returns the query object, which is then executed and its first result returned.

For your implementation of this method, that is a pretty big thing to consider that may change how you go about it. This method is not static , so an instance of the class will have already been created when this method is called. Luckily, this instance is created using the container, so you do have access to dependency injection, though I do know that this creates a bit of a dirty solution.

This method has the following two parameters, which appear in the following order.

While the method itself doesn't enforce a specific return type, the docblock specifies it as Model|null , so if you're using any static analysis tools, they're going to complain a lot.

My Recommendation

Have this method return this and populate itself in place, with any dependencies needed to perform the resolution injected in the constructor. If necessary, simply set a property with the passed-in value, and make the class instance a deferred object.

The resolveChildRouteBinding Method

The final method on this interface makes it glaringly obvious that this system was very much created for models, and model wrappers (resources), without much consideration for other classes. It has the same issues as the above method also not being static but again, the instance it's called on will have been through the container, so there is some saving grace there.

It is of course implemented on both the IlluminateDatabaseEloquentModel class and IlluminateHttpResourcesDelegatesToResource trait. Though the implementation on the trait throws an Exception , that's right, a base exception with the message Resources may not be implicitly resolved from route bindings.

This method has the following three parameters, which appear in the following order.

Unless otherwise specified, route parameters are scoped based on the order they appear. The assumption is made that every route parameter is a child of the one that comes before it. When the implicit bindings are being processed inside IlluminateRoutingImplicitRouteBinding resolveForRoute , an attempt is made to retrieve a parent. If scoped bindings are not disabled, and there's a route parameter before this one, its value, which should have already been resolved, will be used.

What this means for us is that the resolveChildRouteBinding will be called on the parent route parameter, and the childType parameter on the method, will contain the name of the child route parameter, which is the current one. Confused? Yeah, it took me a while to wrap my head around it.

Imagine the following route definition that lets you access entries within datasets that aren't typical databases, such as XML or CSV files.

use App\Data\Dataset;
use App\Data\Entry;

Route::get('/data/{dataset}/{entry}', function (Dataset $dataset, Entry $entry) {
    //
});

When the route parameters are being processed for a request to /data/agents/47, the following would happen.

I think this may be an oversight, but because of how the bindings are handled, an instance of AppDataEntry will have already been created by the container, even though it's only used in a call to get_class in the case of an error.

What's Next?

How you go about using this, whether you even want to or not, is entirely up to you. It's very clear from the code that although it looks very flexible on the surface, it's really intended just for models. I have ideas for how to improve this, and it's something I may very well PR soon. Like always, my intention was to share with you the inner details that aren't covered in the documentation. So go, have fun, create classes capable of being implicitly resolved, or look deeper into the implementation and get as frustrated as I did the first time I needed to investigate it.

The Hidden Parts of Laravel

This article was an interesting one to write, as I think it hits a nice balance between covering the internals of how a feature works, as well as expanding on the bit that isn't covered in the docs. I'll be covering more of those in coming articles, but for now I hope that you find this useful, whether as a solution to a problem you're having, or as a way to learn more about Laravel.

If you'd like to stay updated with my latest articles, tutorials, projects, and thoughts on everything Laravel and PHP, you can subscribe to my newsletter.

Got thoughts, questions, or just want to chat? I'm always up for a good conversation— get in touch, or find me on BlueSky, X or Discord