Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Rbac Laravel Package

artisanpack-ui/rbac

ArtisanPack UI RBAC brings role-based access control to Laravel apps with a ready-made UI for managing roles, permissions, and user assignments. Define access rules and administer authorization from your admin panel, simplifying RBAC setup and day-to-day permission management.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and boot

composer require artisanpack-ui/rbac
php artisan vendor:publish --tag=rbac-migrations
php artisan migrate

Since v1.1.0 artisanpack-ui/hooks: ^1.3 is pulled in automatically — there is nothing extra to install, but check composer.lock if you were already requiring the hooks package at a different constraint (see Gotchas).

Wire the trait onto your user model

use ArtisanPackUI\Rbac\Concerns\HasRoles;
use ArtisanPackUI\Rbac\Concerns\HasPermissions;

class User extends Authenticatable
{
    use HasRoles, HasPermissions;
}

First use case: seed one role, gate one route. Don't try to model the whole permission matrix up front — the package's value shows up fastest when you seed a single admin role with two or three abilities and put @can in a Blade view:

$admin = Role::create(['name' => 'admin']);
$admin->givePermissionTo('users.manage');

$user->assignRole('admin');
@can('users.manage')
    <a href="{{ route('users.index') }}">Manage users</a>
@endcan

Where to look first

  1. HasPermissions::getAbilities() — the new method in v1.1.0. It is the canonical, post-filter list of everything the user can do. Read it once early to understand what the package thinks the user's ability surface is before you start debugging why @can doesn't render.
  2. The Gate::before shim — understand that the package resolves abilities through a global gate hook before your policies run. That single fact explains most "why did this pass?" questions.
  3. The hooks fired at each seam (app.rbac.*) — these are your extension points and where a lot of the v1.1.0 value sits. Register them in a service provider's boot().

Implementation Patterns

Grafting permissions with the abilitiesForUser filter

This is the headline feature. Instead of subclassing the user model or writing a custom resolver, you attach abilities from an external source and every downstream consumer — $user->can(), Gate::allows(), @can — honors them for free:

use function ArtisanPackUI\Hooks\add_filter;

add_filter('ap.rbac.abilitiesForUser', function (array $abilities, $user) {
    if ($user->tenant?->isEnterprise()) {
        $abilities[] = 'reports.export';
    }

    return $abilities;
}, 10, 2);

Good fits for this seam:

  • LDAP / SSO group claims — map directory groups to ability strings inside the filter rather than syncing rows on every login.
  • Feature flags — if (Feature::active('beta.reports')) { $abilities[] = 'reports.beta'; }. Keeps rollout state out of your RBAC tables entirely.
  • Tenant / plan policies — read the tenant model and add plan-specific abilities.

Keep the filter pure and fast: return a modified array, don't persist anything, and don't re-enter ability resolution from inside it (see Gotchas).

Auditing with checkingAbility

add_action('ap.rbac.checkingAbility', function ($user, string $ability, mixed $arguments) {
    if (in_array($ability, config('rbac.audited_abilities'), true)) {
        AuditLog::record($user, $ability, $arguments);
    }
}, 10, 3);

Treat this as a logging seam, not a decision seam. It fires before resolution, so it cannot veto a check — return values are ignored. That makes it safe for compliance trails and unsafe for anything you want to enforce.

Reacting to role changes

add_action('ap.rbac.role.assigned', function ($user, $role) {
    Cache::forget("abilities:{$user->id}");
    $user->notify(new RoleAssignedNotification($role));
}, 10, 2);

add_action('ap.rbac.role.revoked', function ($user, $role) {
    Cache::forget("abilities:{$user->id}");
    Session::forget("active_role:{$user->id}");
}, 10, 2);

Because assignRole()/removeRole() fire these after the pivot is written, the listener sees a consistent state — $user->hasRole($role) will already be true/false respectively. Pair every cache key derived from abilities with a matching invalidation here, or you'll ship a stale-permissions bug that only shows up in production.

Using getAbilities() for UI and serialization

public function toArray(): array
{
    return [
        ...parent::toArray(),
        'abilities' => $this->getAbilities(),
    ];
}

One flat list beats N @can checks in a loop, and it's the only place the filter's output is visible without going through the gate. Good for:

  • Building a nav/config-driven UI where the frontend decides visibility.
  • Handing to Inertia/Livewire as a single prop.
  • Asserting in tests: $this->assertEqualsCanonicalizing([...], $user->getAbilities()).

Note the list mixes names and slugs — expect both granularities and normalize in the consumer if you show them to users.

Test pattern

it('lets tenant admins export reports', function () {
    $user = User::factory()->create();
    $user->assignRole('admin');

    add_filter('ap.rbac.abilitiesForUser', fn ($a) => [...$a, 'reports.export']);

    expect($user->getAbilities())->toContain('reports.export')
        ->and($user->can('reports.export'))->toBeTrue();
});

Filter through getAbilities() rather than testing the filter callback directly — it proves the graft actually reaches the gate, which is the thing that broke in older hand-rolled implementations.

Gotchas and Tips

artisanpack-ui/hooks is now a hard dependency. This is the one change that can bite on upgrade: call sites are now unconditional, so the rbac package cannot degrade gracefully if hooks is missing or mis-versioned. If your app already requires hooks at ^1.0–^1.2, resolve that constraint before running composer update, or you'll get a conflict that looks like an rbac problem but isn't. Run composer why artisanpack-ui/hooks first when debugging a failed upgrade.

Never call hasPermissionTo() or can() from inside the abilitiesForUser filter. hasPermissionTo() now resolves through getAbilities(), so a check inside the filter re-enters the filter. You get infinite recursion, or — worse — silent truncation if something memoizes mid-flight. If you need conditional logic in the filter, read models and config directly.

Register hooks in a provider that boots early. AppServiceProvider::boot() is fine; a provider registered after the RBAC gate shim may be too late for anything that resolves abilities during boot. If a hook seems to never fire, the first thing to check is registration order, not the callback body.

checkingAbility fires on every check — including every policy call and every @can. A DB write per invocation will quietly tank a page with a permission-heavy nav. Filter aggressively by ability name, and prefer queueing over synchronous inserts. Also watch volume in tests: an audit listener that throws will fail unrelated feature tests.

The Gate::before shim runs before your policies. A true from the RBAC layer short-circuits your Policy::view() — including its false return. If a policy denies but @can passes, the shim found an ability string; the fix is usually to rename the ability or remove the policy's overlap, not to fiddle with gate ordering.

Cache the ability list, but invalidate on both role and filter inputs. Role events give you an obvious invalidation seam; LDAP claims, feature flags, and tenant plan changes do not. Key your cache by user and by whatever the filter reads (tenant_id, flag version) or you will serve stale permissions after a plan upgrade.

Distinguish role name vs. ability string. getAbilities() returns names and slugs flattened; assignRole('admin') and givePermissionTo('users.manage') are different namespaces that happen to share one string space. A typo'd ability silently becomes a no-op check rather than an error — use a backed enum or a constants class for both, and add a test asserting every seeded ability is getAbilities()-visible for at least one role.

role.revoked fires after detach, so guard against double-removal. Calling removeRole() twice usually throws or no-ops before reaching the listener; if your listener performs destructive work (revoking tokens, deleting tenant data), make it idempotent anyway — other code paths may revoke the role too.

Debugging order for "the user should have this ability but doesn't": (1) $user->getAbilities() — is it there at all? (2) If no, is the filter registered and does it receive this user? dd inside the filter, not after. (3) If yes but @can fails, look at the slug/name mismatch. (4) If @can passes but the policy denies, you're hitting the shim short-circuit described above.

Octane / long-running workers: any state your filters or listeners accumulate (memoized ability arrays, resolved tenant) persists across requests. Scope per-request caching to the request lifecycle or flush on RequestHandled.

Extension points to prefer: all four hooks are the intended customization surface. Reach for them before subclassing HasRoles/HasPermissions or swapping the resolver — subclassing puts you off the upgrade path for exactly the code the package is now actively growing (the getAbilities() refactor in v1.1.0 is a good example of internals moving under you).

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource