Laravel
Pivoting to Eloquent Pivot Models
Many-to-many relationships are well managed within Laravel, using the BelongsToMany
relationship type within Eloquent. This relationship type provides a range of configuration options. Most commonly, you have:
withTimestamps()
: This tells Eloquent to set or update thecreated_at
andupdated_at
columns on the joining table during inserts or updates.withPivot([])
: This allows an array of column names to be passed in, making these columns available within the specialpivot
property when loading the relationship.
Additionally, there is the using
method, which can be very useful for certain table structures. It has limitations around eager loading and the N+1 performance concern but can be quite powerful when used appropriately.
## Laravel's Pivot Model
When accessing the ->pivot
property on a BelongsToMany
relationship, the pivot property returns a pivot model instance containing the data from the pivot table. These columns must be defined using the withPivot
method when configuring the relationship.
The pivot model returned by default is a full Eloquent model with some additional logic to handle the pivot table. Laravel's using
method allows defining a custom pivot model that represents the row in the pivot table. This custom pivot model extends the Pivot
class and can include any methods a normal Eloquent model would.
Database Structure
The example database structure includes two tables connected via a pivot table, which also contains additional data. In this case, the additional data is the "type" of the link, but it could be any other column.
If your tables have additional columns you may want to cast, like money to render it to two decimal places or add a symbol, you can do that with pivot models too!
For this example, we have an organisation
table and a person
table. A person can be associated with multiple organizations, and that association has a "type".
Setting up base models
Now, let’s create some simple Laravel models to represent our database.
1 class Person extends Model
2 {
3 public function organisation(): BelongsToMany
4 {
5 return $this
6 ->belongsToMany(Organisation::class, 'organisation_person')
7 ->withPivot([
8 'organisation_type_id',
9 ]);
10 }
11 }
12
13 class Organisation extends Model
14 {
15 public function people(): BelongsToMany
16 {
17 return $this
18 ->belongsToMany(Organisation::class, 'organisation_person')
19 ->withPivot([
20 'organisation_type_id',
21 ]);
22 }
23 }
24
25 class OrganisationPersonType extends Model
26 {
27 // This model represents the type of link in the pivot table
28 }
Before diving into creating a custom pivot model, lets first start by looking at an approach we may use instead.
1 $organisation = Organisation::first();
2 $firstPerson = $organisation->people->first();
3 $type = OrganisationPersonType::find($firstPerson->pivot->type_id)
We're wanting to load a person from an organisation and get their type. So we use the people relationship against our organisation, for this example we then just grab the first. Then we make use of Laravel's default pivot model, via the pivot attribute, to fetch the type_id
and use that to lookup the type model.
This approach when looking over all the people associated with the organisation would trigger an n+1 query. However, out the box even pivot models have this same problem. There are some packages that help to improve this though.
This will work great, however, I personally prefer the chaining benefit that Eloquent provides. Not only do I think it helps make the code readable as it clearly shows there's a relationship between two pieces of data, but it also means you have less boilerplate code having to fetch various models manually.
Creating a Custom Pivot Model
First, create a new pivot model class:
1 class OrganisationPerson extends Pivot
2 {
3 public function type(): BelongsTo
4 {
5 return $this->belongsTo(OrganisationPersonType::class);
6 }
7 }
This custom pivot model extends Laravel's built-in Pivot
class. Here, we add a type
relationship. Additional features like casts, traits, and mutators can also be included based on requirements.
Next, update the BelongsToMany
relationships to use the custom pivot model:
1 class Person extends Model
2 {
3 public function organisation(): BelongsToMany
4 {
5 return $this->belongsToMany(Organisation::class, 'organisation_person')
6 ->withPivot([
7 'organisation_type_id',
8 ])
9 + ->using(OrganisationPersonType::class);
10 }
11 }
12
13 class Organisation extends Model
14 {
15 public function people(): BelongsToMany
16 {
17 return $this->belongsToMany(Organisation::class, 'organisation_person')
18 ->withPivot([
19 'organisation_type_id',
20 ])
21 + ->using(OrganisationPersonType::class);
22 }
23 }
By adding the using
call, Laravel uses our custom OrganisationPerson
class as the pivot model. Now, accessing ->pivot
from a person or organisation model retrieved by the relationship will be an instance of our new class.
1 $organisation = Organisation::first();
2 $firstPerson = $organisation->people->first();
3 - $type = OrganisationPersonType::find($firstPerson->pivot->type_id)
4 + $type = $firstPerson->type
This approach is cleaner and provides additional quality-of-life features. However, it does not resolve the N+1 query issue.
The N+1 Problem
This approach, while clean, does not solve the N+1 query problem when fetching the type for multiple pivot records. Eager loading would typically be useful, but Laravel does not support eager loading a pivot's relationships. Although a pull request was made to add this feature, it was declined.
There are packages available to allow eager loading of relations on pivots:
For Laravel <8: Package by ajcastro implements a custom Eloquent Builder to add methods for handling pivot relations.
For Laravel 8+: Fork by audunry is based on the pull request and introduces additional logic to the belongs to many relation for handling pivot relations.
Hopefully, this functionality will be introduced into Laravel's core in the future, as it is useful for common table structures.
Conclusion
Custom pivot models in Laravel can be powerful for complex many-to-many relationships, despite potential performance issues. Existing packages can mitigate these issues until official support for eager loading of pivots is introduced in Laravel.
I've been experimenting quite a lot of Eloquent recently, learning new things and discovering some techniques that I haven't seen commonly covered online. I'm hoping to post more about Eloquent and the powerful, and sometimes undocumented or tricky to find, features it supports.