Eager Loading in Route Models

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:

  1. I only need the count, not a collection of all the related notes.
  2. 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.

writing

I write about technology I'm using and the tech ecosystem.

speaking

I speak at tech and other events. You can see the recordings/slides.

resume

Here's my work history.

contact me

If you're interested in working together, let me know!