RBAC - v1.1.0

Hooks

Since v1.1.0, the package integrates with artisanpack-ui/hooks to expose lifecycle seams that let external packages graft onto the role / permission flow without subclassing traits or wiring up event listeners.

Hooks complement — they do not replace — the Events system. Events are best for coarse-grained observation (audit logging, notifications); hooks are best for extension seams where the package needs to compose the return value with contributions from other packages (filters) or expose a low-friction audit / policy seam that fires unconditionally on every call (actions).

Hook reference

Hook Type Fired from Payload
ap.rbac.abilitiesForUser filter HasPermissions::getAbilities() (array $abilities, User $user)
ap.rbac.role.assigned action HasRoles::assignRole() (User $user, Role $role)
ap.rbac.role.revoked action HasRoles::removeRole() (User $user, Role $role)
ap.rbac.checkingAbility action Gate::before (registered by RbacServiceProvider) (User $user, string $ability, mixed $arguments)

The role.assigned and role.revoked actions fire only when the pivot actually changes state — identical to their sibling rbac.user.role_assigned / rbac.user.role_removed events.

ap.rbac.abilitiesForUser

The canonical extension seam. HasPermissions::getAbilities() collects every ability reachable through the user's role hierarchy (both name and slug for each permission), then passes the flat, de-duplicated list through this filter. Because hasPermissionTo(), $user->can('slug'), Gate::allows('slug'), and @can('slug') all resolve through getAbilities(), anything a filter grafts on is honored everywhere.

Typical use cases:

  • Graft ability strings from an external directory (LDAP groups, Okta, Azure AD).
  • Merge feature-flag-derived abilities so a flag flip immediately widens or narrows what @can renders.
  • Overlay tenant-specific abilities in a multi-tenant app.
use function addFilter;

addFilter( 'ap.rbac.abilitiesForUser', function ( array $abilities, $user ): array {
    return array_merge( $abilities, resolveLdapGroups( $user ) );
} );

The filter fires inside the resolved-permissions path, which sits behind the per-user permission cache. If your external source changes and you need the next getAbilities() call to see the new value, invalidate that cache with $user->flushPermissionCache().

ap.rbac.role.assigned / ap.rbac.role.revoked

Actions dispatched from HasRoles::assignRole() and HasRoles::removeRole() after the pivot mutation succeeds and after the sibling rbac.user.role_assigned / rbac.user.role_removed events fire.

They exist alongside the events so downstream packages that already speak the hooks vocabulary (cms-framework, security-analytics extensions) can subscribe without setting up Laravel event listeners for a single side effect.

use function addAction;

addAction( 'ap.rbac.role.assigned', function ( $user, $role ): void {
    dispatch( new SyncRoleToIdp( $user, $role ) );
} );

Pivot mutations that bypass the trait helpers ($user->roles()->attach($id) etc.) do not fire either the events or the hooks.

ap.rbac.checkingAbility

An audit seam. Every Gate check on a user carrying the HasPermissions trait triggers this action from the RBAC Gate::before shim, before the shim decides whether the ability matches an RBAC permission and before the shim falls through to standard policies. It fires unconditionally per call — matching abilities and non-matching abilities both surface here.

This is deliberate: compliance-heavy applications typically need to log every attempted authorization decision, not just the ones that ended up hitting an RBAC row.

use function addAction;
use Illuminate\Support\Facades\Log;

addAction( 'ap.rbac.checkingAbility', function ( $user, string $ability, $arguments ): void {
    Log::channel( 'audit' )->info( 'rbac.check', [
        'user_id'   => $user->getKey(),
        'ability'   => $ability,
        'arguments' => $arguments,
    ] );
} );

Because this fires from Gate::before, it runs on every ability check on the user — including checks that end up handled by non-RBAC policies. Keep the listener cheap or defer heavy work (queued jobs, buffered log flushes) to avoid making Gate checks a hot path.

The sibling hooks ap.rbac.roleRegistered and ap.rbac.permissionRegistered are documented in and fired from artisanpack-ui/cms-framework when it discovers and registers roles / permissions declared by packages. This package does not re-fire them.