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 the created_at and updated_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 special pivot 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

Our example database structure.
Our example 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.

This technique can be used for more than relations

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 }
php

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)
php

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.

Look out for N+1

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 }
php

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 }~}
php

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
php

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.

Watch this space

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.