🤔Many-to-Many Automatic Cache Invalidation

Automatic invalidation can be quite tricky for Many-To-Many relationships because natively, there is not a specific event that gets triggered upon attach, detach, sync or other many-to-many variations called directly on the relation method.

For many-to-many automatic cache invalidation, a package that will enable events for the many to many relationships is needed: chelout/laravel-relationship-events

Installing the package

composer require chelout/laravel-relationship-events

Given the Laravel documentation's example on pivots, we might have the same example with User and Role.

Note: The HasBelongsToManyEvents trait is the one that will respond to BelongsToMany. Read more about Laravel Relationship Events to use the correct trait for your relationship:

For the observer to work, \Chelout\RelationshipEvents\Traits\HasRelationshipObservables must also be implemented in the parent model (in this example, it's User).

use Rennokki\QueryCache\Traits\QueryCacheable;

class Role extends Model
{
    use QueryCacheable;
}
use Chelout\RelationshipEvents\Concerns\HasBelongsToManyEvents;
use Chelout\RelationshipEvents\Traits\HasRelationshipObservables;
use Rennokki\QueryCache\Traits\QueryCacheable;

class User extends Model
{
    use HasBelongsToManyEvents;
    use HasRelationshipObservables;
    use QueryCacheable;
    
    /**
     * Invalidate the cache automatically
     * upon update in the database.
     *
     * @var bool
     */
    protected static $flushCacheOnUpdate = true;
    
    /**
     * The roles this user has.
     *
     * @return mixed
     */
    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
    
    /**
     * When invalidating automatically on update, you can specify
     * which tags to invalidate.
     *
     * @param  string|null  $relation
     * @param  \Illuminate\Database\Eloquent\Collection|null  $pivotedModels
     * @return array
     */
    public function getCacheTagsToInvalidateOnUpdate($relation = null, $pivotedModels = null): array
    {
        // When the Many To Many relations are being attached/detached or updated,
        // $pivotedModels will contain the list of models that were attached or detached.
        
        // Based on the roles attached or detached,
        // the following tags will be invalidated:
        // ['user:1:roles:1', 'user:1:roles:2', ..., 'user:1:roles']

        if ($relation === 'roles') {
            $tags = array_reduce($pivotedModels->all(), function ($tags, Role $role) {
                return array_merge($tags, ["user:{$this->id}:roles:{$role->id}"]);
            }, []);
            
            return array_merge($tags, [
                "user:{$this->id}:roles",
            ]);
        }
        
        return [
            //
        ];
    }
}

In the example above, the flushCacheOnUpdate should be on the model from which you call the many-to-many relation. This will trigger automatic cache invalidation for the following query:

$roles = $user->roles()
    ->cacheTags(["user:{$user->id}:roles"])
    ->get();

Upon triggering the ->attach() method, the cache associated with the tag user:[user_id]:roles will be flushed.

$user->roles()->attach(1);

As you might have seen, the getCacheTagsToInvalidateOnUpdate will also pass the related model on which the many-to-many relation will take action. In the above example, it will invalidate the roles of the user by using a Selective cache invalidation for specific tags.

Last updated