Eloquent Events on Pivot Tables

May 19th, 2024

I recently needed to listen to events on a pivot table within the Codecourse codebase. When a user resets progress for a course, and all the episodes they've completed are detached, episode progress (stored in Redis) should be removed.

Short answer, no. When you detach a bunch of related models like this, Laravel performs this within a single query. Any kind of batch updating in Laravel doesn't trigger Eloquent events.

$user->episodes()->whereBelongsTo($course)->detach();

Previously, I was iterating over every single episode manually (before it got detached) to clear the Redis key. However, I wanted something a little more automatic.

The laravel-pivot package solves our problem by listing all IDs that get attached/detached/synced when we perform an action like this. Let's see how it works.

Once installed with composer require fico7489/laravel-pivot, we add a trait to our root model. In this case, it's the User model, since we're performing the pivot update on the user's episodes.

class User extends Model
{
    use PivotEventTrait;
}

Next, let's add an event handler for this, either within the booted method for the model or within an observer.

class User extends Model
{
    use PivotEventTrait;

    static::pivotDetached(function ($model, $relationName, $pivotIds) {
        //
    });
}

We can register plenty of listeners here, like pivotAttaching or pivotUpdating. Choose which one you need for your scenario.

We have four arguments available within this event callback:

  1. $model. This gives you an instance of the model you're listening to. In this case, it's User.
  2. $relationName. This is the relation name for what's being acted on. If we're detaching episodes, this would be a string (episodes).
  3. $ids. These are all the affected IDs.
  4. $pivotIdsAttributes. This is an array of additional attributes you may be passing to your pivot actions. For example, when attaching or updating episode progress, I store the seconds duration of how much of the episode has been completed.

With all this event's data, let's update the pivotDetached to remove these episodes!

class User extends Model
{
    use PivotEventTrait;

    static::pivotDetached(function ($model, $relationName, $pivotIds) {
        if ($relationName === 'episodes') {
            collect($pivotIds)->each(function ($id) use ($model) {
                $model->removeEpisodeProgress($id);
            });
        }
    });
}

As you can see, I first scope this down to the $relationName. Then, I collect all the $ids and iterate over them, and for each affected ID, remove episode progress.

Listening to Eloquent pivot events can clear up repetition in your code, and it helps that small actions are taken care of automatically wherever you modify the pivot data from — just be aware of the side effects of listeners like this when writing tests and seeding data!

Thanks for reading! If you found this article helpful, you might enjoy our practical screencasts too.
Author
Alex Garrett-Smith
Share :

Comments

No comments, yet. Be the first to leave a comment.