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
, which by default is implemented by
IlluminateContractsRoutingRegistrar
substituteImplicitBindingsIlluminateRoutingRouter
, 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
with a
IlluminateRoutingRouter
substituteImplicitBindingsUsingClosure
, 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 ofIlluminateFoundationApplication. -
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.IlluminateRoutingImplicitRouteBindingresolveForRoute
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
, 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.
IlluminateRoutingImplicitRouteBinding
resolveForRoute
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
which in this instance, will return all parameters from the route handler whose type is a subclass of
IlluminateRoutingRoute
signatureParametersIlluminateContractsRoutingUrlRoutable
.
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
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
IlluminateRoutingImplicitRouteBinding
getParameterName
.
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
,
returns the name of the attribute containing the primary key. It's also present on
IlluminateDatabaseEloquentModel
getRouteKeyNameIlluminateHttpResourcesJsonJsonResource
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
and
IlluminateDatabaseEloquentModel
getRouteKey
.
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
IlluminateDatabaseEloquentModel
resolveRouteBindingQuerystring
,
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
,
returns the model's primary key via
IlluminateDatabaseEloquentModel
getRouteKey
. The
IlluminateDatabaseEloquentModel
getRouteKeyNameIlluminateHttpResourcesJsonJsonResource
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
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
IlluminateContractsRoutingUrlRoutable
resolveRouteBindingIlluminateDatabaseEloquentModel
class and
IlluminateHttpResourcesDelegatesToResource
trait. As you can imagine, the one on the trait proxies, but the one
on the model
calls
, which adds a where clause and returns the query object, which is then executed and its first result returned.
IlluminateDatabaseEloquentModel
resolveRouteBindingQuery
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.
-
mixedvalue- This is the value of the route parameter as it was provided, whether from the URL or an explicit binding. The parameter type is only specified in the docblock. -
string|nullfield- This is usuallynull, unless the binding field was provided when defining the route. This is the:usernamebit from the start of this article.
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.
-
stringchildType- This is the "name of the child type", which when dealing with a model would be the relationship name, though could in theory be anything with a custom implementation. -
mixedvalue- This is the value of the route parameter as it was provided, whether from the URL or an explicit binding. The parameter type is only specified in the docblock. -
string|nullfield- This is usuallynull, unless the binding field was provided when defining the route. This is the:usernamebit from the start of this article.
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
, 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.
IlluminateRoutingImplicitRouteBinding
resolveForRoute
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.
-
The
{dataset}parameter would come first, which would be mapped toAppDataDataset, resulting in a call to.AppDataDatasetresolveRouteBinding'agents'null -
Next comes the
{entry}parameter, but since scoped bindings are enabled, this would result in a call to.AppDataDatasetresolveChildRouteBinding'entry'47null
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.