elpandape/filament-bouncer
Filament Bouncer integrates the Bouncer authorization package with Filament, adding role and permission management resources, user ability assignment, and policy-aware controls so you can manage access directly from your Filament admin panel.
This release adds a write-boundary event to the role/stance store, so the first thing to do is decide whether your application currently listens to AbilityStanceChangedEvent for anything other than cell-level bookkeeping.
composer require the new version), then check the package's Events namespace — AbilityStancesSavedEvent ships alongside the existing AbilityStanceChangedEvent.AbilityStanceChangedEvent. Audit logs, cache busters, webhooks, and notification listeners are the usual suspects, and they are exactly the ones that should often move to the new event.AbilityStancesSavedEvent arrives carrying the role and the number of changed cells, after the per-cell events.A minimal listener:
Event::listen(function (AbilityStancesSavedEvent $event) {
Cache::forget("role:{$event->role->getKey()}:stances");
Log::info('stances saved', [
'role' => $event->role->getKey(),
'changed' => $event->changedCount,
]);
});
Start by replacing one "flush on every cell" listener with the aggregate event and verifying the cache still clears correctly for a multi-cell save, a saveRow() save, and a no-op save.
Split your listeners by intent. Keep AbilityStanceChangedEvent for anything that genuinely needs per-cell granularity (row highlighting, fine-grained diffs, per-ability audit trails). Move anything that represents "a user did a thing" — notifications, webhooks, activity log entries, cache invalidation — to AbilityStancesSavedEvent. The release note is explicit that the aggregate event belongs in the store because only the store knows where a save ends; mirror that reasoning in your app and stop trying to infer save boundaries from request lifecycle.
Use the changed count as a guard. AbilityStancesSavedEvent carries the role plus the number of cells changed, so you can skip work cheaply:
public function handle(AbilityStancesSavedEvent $event): void
{
if ($event->changedCount === 0) {
return; // defensive; the package already suppresses no-op saves
}
$this->invalidate($event->role);
$this->dispatchSync(new SyncRoleStances($event->role));
}
Bulk UI saves. A form that marks ten checkboxes and posts once will produce ten AbilityStanceChangedEvents and exactly one AbilityStancesSavedEvent. If you have a "you changed permissions" notification, hook it to the aggregate event so users get one notification per save instead of ten.
Row-level saves. saveRow() closes the write with a single change, so row-level editors can use the same listener as grid editors — no special-casing. This is the main reason to adopt the event early: it gives you one code path for both interaction styles.
Ordering. The aggregate event fires after the last cell event and never instead of it. If you have both listeners registered, do not increment the same counter in both — pick one as the source of truth (usually the aggregate) and keep the per-cell listener purely observational.
Queue long-running work. Because the aggregate event is one-per-save, it is the natural place to dispatch a job that recalculates effective permissions, warms caches, or pushes to an external authorization service. Put the listener behind ShouldQueue and consider afterCommit() so the job doesn't run for a rolled-back save.
saveRow() fires the aggregate event too. If you were assuming the aggregate event only accompanies grid saves, that assumption will produce double-processing or missed processing at the row level.AbilityStanceChangedEvent before deploying. Nothing is removed in this release, but listeners that were effectively "once per user action" by accident (because single-cell saves were the norm) will now fire once per cell in bulk flows, which can multiply side effects.How can I help you explore Laravel packages today?