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

Filament Acl Laravel Package

coringawc/filament-acl

Role and permission management for Filament panels. coringawc/filament-acl adds ACL resources to create roles, assign permissions, and control access to your admin resources, pages, and widgets, integrating with Laravel's authorization layer for straightforward access control.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm you are inside the tested envelope before anything else. The package declares PHP ^8.3, Filament ^5.0, and spatie/laravel-permission ^8.2. Laravel 13 and PHP 8.5 are explicitly untested — if your composer.json floats to those, treat this dependency as unproven rather than inferring compatibility from the green CI badge. Also note v2.0.1 is behavior-preserving: adopting it is a dependency bump, not a feature upgrade.

Install and wire up:

composer require coringawc/filament-acl
php artisan vendor:publish --tag="filament-acl-config"
php artisan vendor:publish --tag="filament-acl-migrations"   # verify tags in the package README
php artisan migrate

Register the plugin on the panel you want the role/permission UI in:

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugin(FilamentAclPlugin::make());
}

Where to look first, in this order:

  1. config/filament-acl.php — especially integration.require_explicit_opt_in. The package defaults to explicit opt-in, so a freshly installed panel shows an empty permission UI until you add a trait to an owner class. If "nothing appears" is your first symptom, this is why — not a bug.
  2. agents/AGENTS.md in the repo. It is unusually good and states the design contract: trait-first, automatic-by-default, override-by-method, concrete-class-only attributes, no Shield coupling. Read it before you file an issue or write a base class.
  3. The built-in roles/permissions resource — it is the canonical UI for what the sync command discovers.

First use case (10 minutes): add HasResourcePermissions to a single PostResource, run the package's sync artisan command (php artisan list will show it under the filament-acl namespace), then open the built-in roles resource. You should see a PostResource tab with the derived viewAny/view/create/update/delete permission keys. That round trip — trait → sync → tab → role assignment → gate check — is the whole package in miniature.

Implementation Patterns

Opt in per owner with the matching trait

One trait per owner type; no base classes required.

class PostResource extends Resource
{
    use HasResourcePermissions;
    // ...
}

The same shape applies to HasRelationManagerPermissions, HasPagePermissions, and HasWidgetPermissions. Because the default is "everything on," the trait is the opt-in — a resource without the trait is invisible to sync and to package-level checks.

Prefer attributes for static facts, methods for anything computed

Attributes are the day-to-day ergonomic surface:

#[PermissionSubject('blog.posts')]
#[PermissionActions(['view', 'viewAny', 'create', 'update', 'delete', 'publish'])]
#[CustomPermissionActions(['archive', 'restore'])]
class PostResource extends Resource
{
    use HasResourcePermissions;
}

Override the methods when the answer depends on runtime state — the documented hooks are exactly these, and each should stay optional:

public function getPermissionSubject(): ?string      // rename the subject key
public function shouldRegisterPermissions(): bool    // opt this class back out
public function getSharedPermissionOwner(): ?string  // delegate to a canonical owner
public function getPermissionCustomActions(): array
public function getPermissionActions(): array
public function getPermissionPanel(): ?string

Authorize custom actions through native Laravel, not a bespoke Filament action class

This is the strongest design decision in the package and the one most likely to be fought by muscle memory. Keep it native:

// Policy
public function archive(mixed $user, Post $record, PermissionAction|string|null $permissionAction = null): Response
{
    if ($response = $this->denyUnlessPermitted($user, 'archive', $permissionAction)) {
        return $response;
    }

    return $record->isDraft() ? Response::allow() : Response::deny('Already published.');
}
// Resource table action
->authorize('archive', PostResource::class)   // or: auth()->user()?->can('archive', [$record, PostResource::class])

Two rules that matter: the extra permission argument goes last in every policy signature, and never let a policy infer the owner from the route/request when it can be passed explicitly. The package's ChecksPermission::denyUnlessPermitted() is opt-in — a policy that omits it is still a valid policy, just not ACL-aware, which is useful for resources you deliberately keep outside the system.

Shared owners for resources that are really one permission domain

When two owners are conceptually the same thing (e.g. ArticleResource and DraftArticleResource), have the secondary delegate:

public function getSharedPermissionOwner(): ?string
{
    return ArticleResource::class;
}

The delegating owner inherits the canonical owner's permissions and disappears from discovery and from the permission UI tabs. Expected behavior, not a defect — don't "fix" it by reintroducing duplicate tabs, which the AGENTS.md contract explicitly warns against.

Panel scoping is a first-class integration choice

Roles and permissions scope to panels separately. Set them deliberately and remember that the sync command, the built-in resource queries, hidden-role helpers, and permission lookup helpers all read the same configuration. If you add a custom admin screen that lists roles, filter it the same way or you will surface cross-panel entries.

Protected role + Gate::before() for the super-admin

Configure the protected role name; the package hides it from package UI, protects it from edit/delete via RolePolicy, optional-bypasses package-level checks through Gate::before(), and assigns it through the admin-user command rather than the normal select. This is the intended path to a super-admin — do not hand-roll a second one.

Sync in the deploy pipeline, and gate on drift

Run the sync command as part of deploy before your cache clears, and add a CI check that sync produces no diff. Because permissions are discovered from classes, a rename or a deleted resource silently orphans rows in permissions — a drift check turns that into a red build instead of a mysterious blank tab in staging.

Gotchas and Tips

Attributes are read from the concrete class only. #[PermissionSubject], #[PermissionActions], #[CustomPermissionActions], and #[RegisterPermissions(false)] are not inherited from a parent resource/page/widget. Subclass a resource and you must redeclare the attribute or override the corresponding method — otherwise the child silently gets defaults while you assume it inherited the parent's subject. This is the single most likely source of "why does this resource have the wrong permission key."

"Nothing shows up" checklist. In order: (1) require_explicit_opt_in is true and the class lacks the trait; (2) shouldRegisterPermissions() returns false; (3) the class returns a getSharedPermissionOwner() and is therefore hidden by design; (4) you're looking at a different panel than getPermissionPanel() resolves to; (5) permissions were renamed but sync never re-ran. Work top-down before touching config.

The protected role is deliberately invisible. It won't appear in normal selects and it's guarded by RolePolicy. If a stakeholder asks "why can't I edit super-admin in the UI," the answer is the design, not a permission bug. Assignment goes through the admin-user command.

shouldRegisterPermissions(): false must be respected everywhere. It means not synced, not shown in the permission UI, and package-level checks skipped. If you write your own discovery or your own role screen, honour it — regressing this is called out as essential in the package's own rules.

Spatie's permission cache will lie to you. After any sync, reset it (php artisan permission:cache-reset) before concluding anything about a 403. In dev, keep the cache off; in CI, treat the reset as part of the sync step rather than a separate manual intervention.

Don't double-authorize. Filament will already call viewAny/view/create etc. against your policy. Adding a second explicit ->authorize(...) or ->visible(...) on the same action is a common over-correction that produces confusing 403s and duplicated logic. Let the policy's denyUnlessPermitted() be the single gate; reserve explicit authorize calls for the custom actions that have no built-in Filament hook.

Renaming a permission subject is a data migration. Changing #[PermissionSubject('blog.posts')] to 'content.posts' creates new permission rows; the old ones stay attached to roles and gates. Write the cleanup step explicitly — usually reassigning role permissions then pruning orphans — and ship it in the same PR as the rename.

App-specific logic belongs in your app. The package is deliberately generic about discovery, subjects, keys, sync, panel scoping, protected roles, and the built-in resource — and deliberately silent on domain policy rules, custom action business logic, extra role metadata, and naming conventions. Fighting for a BaseResource in the package is the wrong direction; extend in your application layer.

Extension points worth knowing: getManagedPermissionPanel() plus plugin configuration lets the built-in resource manage another panel from where it's mounted — useful when you host admin roles in a central panel and consume permissions in tenant panels. The two bundled Laravel Boost skills are also fair game for onboarding: they're part of the shipped and tested surface, but note that if your org forbids agent tooling you're carrying content your policy doesn't cover.

Test against the tested matrix. Pin CI to PHP 8.3/8.4 and exercise both the minimum and current dependency resolutions, mirroring the package's own matrix. And when you pin, pin exactly — a patch bump here is genuinely behavior-preserving per the release notes, but the Laravel 13 / PHP 8.5 boundary means a broader composer update can move you onto a runtime nobody has validated.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource