Custom Pivot Table Models, or Choosing the Right Technique in Laravel

Saw this tweet a few days ago:

My first thought was, “There must be”. This is exactly the type of thing that I feel a lot of people who write critiques — pros and cons pieces, as opposed to simple trolling- often gloss over or are not even aware of. Laravel is “opinionated”, but that usually just means that there are a lot of defaults set up that push you toward a certain style. Over the years I have found very few cases where you can’t easily override these; more often than not, it is simply a case of adding another function call when you instantiate something.

As I thought about the way I would normally code around Mark’s problem, and then searched and found the more direct answer from the docs, it occurred to me that this was a perfect example.

First a quick review of the standard way of working with pivot tables. In this example we have a many-to-many relationship between Users and Subjects. So a typical update statement might look like:

User::find($id)->subjects()->updateExistingPivot($subjectId, ['status' => 'enrolled']);

If you wanted to fire an event or some other action with this statement, you could just write it underneath. Hopefully if you are using this all throughout your code you had the foresight to put this statement into a service class function of some sort, and so you can update in one place.

That would work. There’s nothing inherently “wrong” about that. It does feel a bit procedural, though, doesn’t it? Leaves you feeling like maybe there’s something a little bit better you could be doing? What if this code is all over the place and we didn’t create it in service class? What if it turns out there are a lot of other pivot table fields that can be updated, with different actions on each?

This is where programming becomes “hard”. Not writing the code, but designing the code. Thinking ahead about how this class will be used and making an informed choice, rather than just slapping in the first technique you learned because it’s easy and you can copy/paste it from somewhere else without remembering how it works (speaking from experience, so don’t feel attacked).

For example, what if the pivot table contained information that was valuable in and of itself, independent of the users or specific subjects? This might be a case where you might choose to build it as its own entity, complete with controller, model and validation rules. This is an idea that’s gained some steam over the last year or so; you might enjoy listening to the Full Stack Radio episode with DHH where he discusses how this was a central idea when they built BaseCamp. A UserSubject might be a thing all by itself that you create, update and report on. The important point is to realize that pivot table classes are a default choice in Laravel, not a dictated one.

A second option— probably the most common one —is to do as we described above and create a UserSubjectService class. Any time you want to update the linking pivot you will go through an update() function on the service class. This gives you complete control over every step of the process, making everything completely transparent at the same time. It is, of course, completely testable. Some of the downside of this can be all of the overhead of constantly declaring the new class, passing it the instance of the User, Subject and update data, and possibly calling the same event function everywhere you need a separate update function. Service classes are more commonly used to manage several other classes, so if we were doing a lot of back and forth between the User and Subject, or adding additional functionality, this would be a good choice. This is probably too much for something as simple as what we are after.

A last option I’ll show you is an example of how easy it can be to override Laravel’s defaults. We already know that every Eloquent model has Observers — built in listener hooks to many common actions in the model such as completed, saving … and updating. Since we would like to fire an event every time the pivot table updates, we just need to tap into that model.

Eloquent uses a special model called — wait for it — a Pivot which is found at Illuminate\Database\Eloquent\Relations\Pivot and is an inherited class from Model with some functions to deal with the many-to-many relationship. In order to customize any of these functions, we first extend Pivot with, for example, class UserSubject extends Pivot to add our code, and then call it by adjusting our model relationship functions to:

class Subject extends Model
{
public function users()
{
return $this->belongsToMany('App\User')
->using('App\UserSubject');
}
}

Now whenever the relationship is called, anything you’ve added to your own class will be accessed, just like with an inherited model. In this specific case, we would register a UserSubjectObserver class and add code to the updating() or updated() function, as we saw fit.

Three ways to do the same thing — and not an exhaustive list by any means. The point of this article, besides introducing the Pivot class and using() function, is to get you to realize that the “Laravel Way” of writing code is a thing, but designing code is your job!

Hope that helps!