Laravel – Custom pivot model in Eloquent

Sometimes you may wish to use custom pivot model when working with many-to-many relationships in Eloquent, in order to add some behaviour on top of the core features.

There are a few things to consider before you start.

First things first: belongsToMany relationship can presented as double hasMany + belongsTo relations.

Imagine we have categories of posts and users who can subscribe for multiple categories:

User belongsToMany Category

// the same as:

User hasMany CategoryUser

CategoryUser belongsTo User

Category hasMany CategoryUser

CategoryUser belongsTo Category

// CategoryUser is additional model for the pivot table:

use Illuminate\Database\Eloquent\Model; // *

class CategoryUser extends Model {

protected $table = ‘category_user’;

// … relations

}

* I prefer to use Model class name instead of its Eloquent alias.

This additional model can be used if we need to hook on the pivot table events (what cannot be done on the relation itself – read here for a workaround https://stackoverflow.com/a/28934080)

Another use case would be pivot table that links more than just 2 other tables.

However, this model is just like any other and it is not used by Eloquent, when we call belongsToMany relation on the user or category. So next, let’s define that custom pivot model (we can use both, or just one of these, depending on the needs):

use Illuminate\Database\Eloquent\Relations\Pivot;

class CategoryUserPivot extends Pivot {

// let’s use date mutator for a field

protected $dates = [‘completed_at’];

}

Now, in order to let Eloquent grab this pivot model, we also need to override newPivot

// User model

public function newPivot(Model $parent, array $attributes, $table, $exists)

{

if ($parent instanceof Category) {

return new CategoryUserPivot($parent, $attributes, $table, $exists);

}

return parent::newPivot($parent, $attributes, $table, $exists);

}

// Category model

public function newPivot(Model $parent, array $attributes, $table, $exists)

{

if ($parent instanceof User) {

return new CategoryUserPivot($parent, $attributes, $table, $exists);

}

return parent::newPivot($parent, $attributes, $table, $exists);

}

The if statement is required, so only this particular relation grabs our custom pivot model. Obviously  we don’t want it to work for other m-m relations if it is specific to user-category only.

Basically this is all we have to do, so let’s check it:

>>> $user = User::first()

>>> $category = $user->categories->first()

>>> $category->pivot // our custom pivot

=> <App\Models\CategoryUserPivot #0000000052d0621800000000619c04ac> {

user_id: 1,

category_id: 20,cutsom

completed_at: «2015-05-02 13:41:31»

}

>>> $category->pivot->completed_at // mutators working as usually

=> <Carbon\Carbon #0000000052d0623800000000619c15cc> {

date: «2015-05-02 13:41:31.000000»,

timezone_type: 3,

timezone: «Europe/Warsaw»

}

// the same for other side of the relation

>>> $cat = Category::first()

>>> $cat->users->first()->pivot

=> <App\Models\CategoryUserPivot #0000000052d0621200000000619c04ac> {

category_id: 1,

user_id: 9,

completed_at: «2015-03-18 15:24:09»

}

As you can see all is working as expected. Now, you’re free to add custom behaviour to the pivot model, so it will work best for you.

There is just one more thing to remember – this custom pivot model won’t be used during sync, attach, detach calls – these are simple queries not touching eloquent.