← Back to blog

Observe pivot tables in Laravel

| Laravel

This article was published over 2 years ago. Some information may be outdated.

By default, Laravel does not observe pivot tables. To understand why, look at the following tables:

posts
	- id
	- title
	- content
	- ...
tags
	- id
	- name
	- ...
post_tag
	- id
	- post_id
	- tag_id

The post_tag is a pivot table, and therefore it does not have a model by default.

Here is the relationship:

public function tags(): BelongsToMany
{
    return $this->belongsToMany(Tag::class);
}

To determine the relationship's intermediate table name, Eloquent joins the two related model names in alphabetical order. In this case, it is post_tag since P comes before T in the English alphabet.

Read more about Eloquent relationships

Now create an observer for the post_tag table so you get notified whenever a user attaches a tag to a post.

First, create a model for post_tag because pivot tables do not have one by default:

php artisan make:model PostTag

The model should look like this:

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\Pivot;

class PostTag extends Pivot
{
    protected $table = 'post_tag';

    public $timestamps = null;
}

The PostTag model must extend Illuminate\Database\Eloquent\Relations\Pivot and not Illuminate\Database\Eloquent\Model since it is a pivot table. Otherwise, you will get an error like this:

BadMethodCallException
Call to undefined method App\Models\PostTag::fromRawAttributes()

The second step is to create an observer for the PostTag model:

php artisan make:observer PostTagObserver --model=PostTag

Add the observer to the boot method in AppServiceProvider:

PostTag::observe(PostTagObserver::class);

Open the PostTagObserver and add the following code to log the PostTag model in the created event:

namespace App\Observers;

use App\Models\PostTag;

class PostTagObserver
{
    public function created(PostTag $postTag)
    {
        logger($postTag);
    }
}

Test it:

# Filename: routes/web.php

Route::get('/observer', function() {
    $post = \App\Models\Post::find(1);
    $tag = \App\Models\Tag::inRandomOrder()->first();
    $post->tags()->attach($tag->id);
});

Open the storage/logs/laravel.log file. Nothing is there.

The problem is that Laravel does not know about the PostTag model you created for the pivot table post_tag. Laravel relies on the table name, not the model name.

This should not be a problem in most cases because the pivot table is just an intermediate table and does not need a model unless you want to customize it.

The solution is straightforward. Tell Eloquent about the PostTag model:

# Filename: app/Models/Post.php

public function tags(): BelongsToMany
{
    return $this->belongsToMany(Tag::class)->using(PostTag::class);
}

The using method specifies the custom pivot model for the relationship.

Hit the same route again, and you will see a new logged entry:

[2021-04-03 12:59:46] local.DEBUG: {"tag_id":59,"post_id":1}

Summary

  • Pivot tables are not observed by default -- Laravel does not fire model events for pivot tables because they do not have associated models out of the box.
  • The pivot model must extend Pivot, not Model -- using the wrong base class causes a fromRawAttributes error.
  • The using() method connects the pivot model to the relationship -- without it, Eloquent has no way of knowing your custom pivot model exists, and the observer will never fire.
Share