RBAC - v1.0.0
Gate Integration
The service provider registers a Gate::before hook so any ability matching a known permission slug resolves through HasPermissions::hasPermissionTo(). Standard Laravel authorization patterns work out of the box.
What this gives you
$user->can('posts.publish'); // true | false
$user->cannot('posts.publish'); // true | false
Gate::allows('posts.publish'); // true | false
Gate::denies('posts.publish'); // true | false
@can('posts.publish')
<button>Publish</button>
@endcan
// In a controller, request, or policy
$this->authorize('posts.publish'); // throws AuthorizationException if denied
All of these route through the same Gate::before hook and ultimately call HasPermissions::hasPermissionTo().
How the hook works
Gate::before(function ($user, $ability) {
if (! method_exists($user, 'hasPermissionTo')) {
return null;
}
$permissionNames = $this->getCachedPermissionNames();
if (in_array($ability, $permissionNames, true)) {
return $user->hasPermissionTo($ability);
}
return null;
});
- Returning
trueorfalsefrom aGate::beforecallback short-circuits the entire authorization check. - Returning
nulldefers to subsequentGate::beforecallbacks and then to policies. - The hook only short-circuits when the ability matches a known permission slug. Abilities like
updateordelete-postthat don't correspond to RBAC permissions fall through to your policies untouched.
Coexistence with policies
This is the most important property of the integration. RBAC permissions and Laravel policies do not compete — they compose.
// PostPolicy
public function update(User $user, Post $post): bool
{
return $user->id === $post->author_id;
}
$user->can('posts.publish'); // resolves through RBAC (matches a permission slug)
$user->can('update', $post); // resolves through PostPolicy (no matching slug)
You can use RBAC for the "broad capability" question ("can this user publish posts at all?") and policies for the "specific instance" question ("can this user edit this post?").
Performance: the permission-names cache
Gate::before runs on every authorization check. Looking up every ability against the database would be expensive, so the hook keeps a cache of all known permission names + slugs and only delegates to hasPermissionTo() when there's a match.
The cache:
- Key:
rbac_permission_names - TTL:
artisanpack.rbac.cache.permission_names_ttl(default 3600 seconds) - Storage: tagged cache when available (
redis/memcached), plain cache otherwise - Invalidation: automatic on
Permission::created,Permission::updated,Permission::deletedviaPermissionObserver
See Caching for tuning.
Edge cases
-
User model without
hasPermissionTo: the hook returnsnullimmediately, so non-RBAC users (e.g. admin accounts on a separate model) fall through to standard authorization. -
Empty permissions table: the cache resolves to an empty array; every check falls through to policies.
-
Race condition during permission creation: if a new permission is created and a Gate check fires before the cache invalidator runs, the check falls through to policies. The next request picks up the fresh cache. This window is microseconds in normal apps; if it matters for your use case, invalidate manually after the create — conditionally on the cache driver:
if ( Cache::getStore() instanceof \Illuminate\Cache\TaggableStore ) { Cache::tags( config( 'artisanpack.rbac.cache.tag' ) )->flush(); } else { Cache::forget( 'rbac_permission_names' ); }