TLDR: Override the resolveRouteBinding
method on your Eloquent model in order
to eager-load relations that you know will be used.
The problem: Limits To Route Model Binding
In a Laravel application I’m writing, I have a few dozen routes that all have basically
the same format: /jobs/{job}
as a prefix and then particular actions for that
job. In Laravel, we have route model binding, which is to say that by convention,
you can use the name of a model (the Job model, in this case) in the braces of
your route and when your application reaches the controller method, the model is
sent in as a parameter.
For example, with /jobs/{job}
this means my controller method signature would
look like this:
public function show(Job $job){ /* ... */ }
This is a very powerful function and it saves us a lot of boilerplate Eloquent code searching for the model from the database explicitly in our controller methods.
I’m working on adding notes to each job, which is something I created using a
new model JobNote
which has a one to many relationship with each Job
.
Now one thing that I want to display in my sub-navigation for all these Jobs is a count of how many JobNotes each Job has. In Laravel, we can eager-load the count by running:
$job->loadCount('notes')
on the job model I receive as a parameter.
Here’s the issue, I have 40+ routes that use this same job model prefix and they all need the count since they all share the same sub-navigation where the count will be used. This would mean I’d have to add that line to 40+ spots in the code (once for each controller method) as well as remember to write it in when I write new controller methods in this route group.
Not my idea of a simple or maintainable coding experience.
The First Idea: Default Eager Loading
Eloquent has the ability to always load a relationship by default using the
protected $with
variable on my Eloquent model, but there are two issues here:
- I only need the count, not a collection of all the related notes.
- The Job index route loads lots of jobs, and doesn’t use the single Job model binding, so I don’t want to get this relationship there.
That second point got me thinking, “Is there a way to hook into a model when it’s being resolved from a route?”
The answer: Yes!
The Real Solution: resolveRouteBinding
Eloquent models have a function, resolveRouteBinding
, which can be overriden
to add extra functionality.
Since this would only happen when a specific job is being asked for in the route, it would cleanly leave out the job index page, resolving the second problem I had with default eager loading.
The function as I needed it, looks like this:
public function resolveRouteBinding($value, $field = null)
{
return $this->where('id', $value)
->withCount('notes')
->firstOrFail();
}
First we search for the actual model, since the id
is what’s passed in, then
we make sure to load it with the count we want, and make sure we only get one
model! Now the model is loaded with the field notes_count
available, with the
data we need by default. We haven’t loaded all the notes, which solves the 1st
problem I had with the default eager loading.
It’s a regular PHP block so I could imagine adding more functionality to support extra loads depending on the route.
Going this route meant that instead of having to add the
$job->loadCount('notes')
to each controller method (40+ times just to start!)
I just added a few lines to my model code. Much easier to maintain.
If you like this, let me know on Twitter.