June 21, 2019 by Glenn

Listen for events fired from pivot tables in Laravel

I was recently tasked with solving an interesting problem for one of my contract clients on their Laravel app.

The company was using a polymoprhic pivot table called taggables to manage the relationship between a tag and any one of a number of different models. However on the taggables table, they had a submitted_by_id field that they wanted to contain the id of the user who attached the tag. It wasn’t working properly and they asked me to fix it.

Most of the code was setup to do something like:

 $post->tags()->sync($array_of_tag_ids)

One way I could of handled it was to provide an associative array where the key was the tag id and the value was an array of attributes to store on the pivot table.

$post->tags()->sync([
   ‘1’ => [‘submitted_by_id’ => auth()->user()->id],
   ‘2’ => [‘submitted_by_id’ => auth()->user()->id],
]);

But the problem this would create is that any existing tag would have it’s submitted_by_id changed to the new users id. We wanted to maintain the data of who first attached the tag to the post. In order to acheive this I would have to get all the current tags, and preform some kind of logic ahead of time to decide who the submitted_by_id should actually be before syncing.

Another problem is that this tags()->sync() functionality was happening all over a code base that has no test coverage; causing the refactoring process to be somewhat risky.

One way around this is to listen for the events that eloquent fires off (creating, created, updating, updated, etc), specifically the creating event, and use that to set the submitted_by_id to the authenticated users id. This way existing tags won’t get updated, but newly created tags will have the propper submitted_by_id. The problem is that Laravel doesn’t fire off events on a pivot table.

It turns out, there is a method called using that lets you define a model for your pivot table to use on the relationship. This will allow Laravel to start firing off these events, and for us to start listening for them.

You need make sure that your pivot table model is either extending Illuminate\Database\Eloquent\Relations\MorphPivot or Illuminate\Database\Eloquent\Relations\Pivot.

    /**
     * Get all of the tags for the post.
     */
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable')
            ->using(TaggablePivot::class)
            ->withTimestamps();
    }
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\MorphPivot;

class TaggablePivot extends MorphPivot
{
    protected $table = 'taggables';

    public $incrementing = true;
}

I ended up creating an observer and registering it in AppServiceProvider (this project ended up having a number of other events that needed to be listed for, otherwise I’d of just stuck it in the boot method on the model.)

<?php

namespace App\Providers;

use App\Models\TaggablePivot;
use Illuminate\Support\ServiceProvider;
use App\Observers\TaggablePivotObserver;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        TaggablePivot::observe(TaggablePivotObserver::class);
    }
}
<?php

namespace App\Observers;

use App\Models\TaggablePivot;

class TaggablePivotObserver
{
    /**
     * Handle the taggable pivot "creating" event.
     *
     * @param  \App\Models\TaggablePivot  $taggablePivot
     * @return void
     */
    public function creating(TaggablePivot $taggablePivot)
    {
        $user = auth()->user();

        $taggablePivot->submitted_by_id = $user ? $user->id : 0;
    }

Doing this prevented me from having to spend time doing a potentially risky refactor of a lot of untested code, and helped to save a lot of time.

Join my mailing list

Subscribe now to stay up to date on whatever cool stuff I'm working on.