mikebronner/laravel-pivot-events
Adds Eloquent model events for many-to-many pivot operations: sync, attach, detach, and updateExistingPivot on BelongsToMany/MorphToMany. Listen for pivotSyncing/Synced, pivotAttaching/Attached, pivotDetaching/Detached, and pivotUpdating/Updated.
Installation:
composer require mikebronner/laravel-pivot-events
Ensure your project meets the requirements: Laravel 11.0+ and PHP 8.2+.
Add the Trait:
Include GeneaLabs\LaravelPivotEvents\Traits\PivotEventTrait in your base model or specific models:
use GeneaLabs\LaravelPivotEvents\Traits\PivotEventTrait;
class User extends Model
{
use PivotEventTrait;
// ...
}
First Use Case:
Listen to a pivot event in your model’s boot() method. For example, log when a user’s roles are synced:
public static function boot()
{
parent::boot();
static::pivotSynced(function ($model, $relationName, $changes) {
\Log::info("Roles synced for {$model->id}. Changes: " . json_encode($changes));
});
}
attach, detach, sync, and updateExistingPivot.Event Listeners in Models:
Use the boot() method to register listeners for pivot events. This is the most common pattern:
public static function boot()
{
parent::boot();
static::pivotAttached(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) {
// Handle attached pivots
});
static::pivotDetached(function ($model, $relationName, $pivotIds) {
// Handle detached pivots
});
}
Global Event Listeners:
Register listeners globally using Laravel’s Event facade for cross-model pivot events:
use Illuminate\Support\Facades\Event;
Event::listen('eloquent.pivotAttached', function ($event) {
// Handle all pivotAttached events globally
});
Custom Event Classes:
For complex logic, create custom event classes implementing GeneaLabs\LaravelPivotEvents\Contracts\ReceivesPivotPayload:
use GeneaLabs\LaravelPivotEvents\Contracts\ReceivesPivotPayload;
class RoleAssigned implements ReceivesPivotPayload
{
public function __construct(public array $payload) {}
}
// Dispatch in your model:
Event::dispatch(new RoleAssigned($payload));
Conditional Logic:
Use the $changes payload in pivotSynced to conditionally trigger actions:
static::pivotSynced(function ($model, $relationName, $changes) {
if (!empty($changes['attached'])) {
// Trigger logic for new attachments
}
});
Audit Logging: Log pivot changes to a database or external service:
static::pivotUpdated(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) {
foreach ($pivotIdsAttributes as $id => $attributes) {
\DB::table('pivot_audit')->insert([
'model_id' => $model->id,
'relation' => $relationName,
'pivot_id' => $id,
'changes' => json_encode($attributes),
'created_at' => now(),
]);
}
});
Real-Time Notifications: Use Laravel Echo or similar to notify users of pivot changes:
static::pivotAttached(function ($model, $relationName, $pivotIds) {
broadcast(new RoleAssignedEvent($model, $relationName, $pivotIds))->toOthers();
});
Data Synchronization: Sync external systems when pivots are updated:
static::pivotSynced(function ($model, $relationName, $changes) {
if (!empty($changes['updated'])) {
$this->syncExternalSystem($model, $relationName, $changes['updated']);
}
});
Validation: Validate pivot attributes before they are saved:
static::pivotUpdating(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) {
foreach ($pivotIdsAttributes as $id => $attributes) {
if (!$this->validatePivotAttributes($attributes)) {
throw new \Exception("Invalid pivot attributes for ID {$id}");
}
}
});
Leverage Laravel’s Queues: Dispatch pivot events to queues for async processing:
static::pivotDetached(function ($model, $relationName, $pivotIds) {
dispatch(new HandlePivotDetached($model, $relationName, $pivotIds))->delay(now()->addSeconds(10));
});
Combine with Observers: Use observers for additional logic alongside pivot events:
class UserObserver
{
public function syncingRoles(User $user)
{
// Pre-sync logic
}
public function syncedRoles(User $user)
{
// Post-sync logic
}
}
Testing: Test pivot events using Laravel’s event testing helpers:
public function test_pivot_attached_event()
{
$user = User::factory()->create();
Event::fake();
$user->roles()->attach(1);
Event::assertDispatched(PivotAttached::class);
}
Performance:
For high-frequency pivot updates, consider batching events or using syncWithoutEvents to disable events temporarily:
$user->roles()->syncWithoutEvents([1, 2, 3]);
Event Order:
sync() dispatches pivotDetaching/pivotDetached before pivotAttaching/pivotAttached. This is because sync() first detaches all existing pivots, then attaches new ones.Payload Structure:
$changes payload in pivotSynced includes arrays like ["attached" => [1, 2], "detached" => [3]]. Ensure your code handles missing keys (e.g., empty($changes['attached'])).$pivotIdsAttributes in pivotAttached/pivotUpdated may include empty arrays for pivots without custom attributes. Validate before accessing nested keys.Duplicate Events:
boot() and globally). Use Event::forget() if needed:
Event::forget('eloquent.pivotAttached', $listener);
MorphToMany:
MorphToMany relationships follow the same pattern as BelongsToMany, but ensure your models are correctly set up for polymorphic relations.Event Suppression:
pivotSynced and pivotDetached are suppressed if no changes occur. This can break assumptions in older code expecting events for "no-op" syncs.Model Caching:
GeneaLabs/laravel-model-caching, ensure pivot events are not cached unintentionally. The package is designed to work with it, but test thoroughly.Event Not Firing:
PivotEventTrait is included in the model.pivotAttaching vs. pivotAttached).Event::listen('eloquent.*', function ($eventName) { \Log::info($eventName); }); to debug dispatched events.Payload Issues:
static::pivotSynced(function ($model, $relationName, $changes) {
\Log::debug(['model' => $model, 'relation' => $relationName, 'changes' => $changes]);
});
Performance Bottlenecks:
Naming Conventions:
roles instead of pivots) for clarity in event listeners.Custom Payloads:
How can I help you explore Laravel packages today?