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

Acl Laravel Package

nuewire/acl

A Laravel ACL package for managing roles and permissions, letting you define access rules and gate application features through a simple, configurable access control list.

View on GitHub
Deep Wiki
Context7

Getting Started

Pull the package in alongside its host package — nuewire/acl is an optional add-on to nuewire/users, so nuewire/users (and therefore its User model/trait) must already be installed and working before ACL means anything:

composer require nuewire/acl
php artisan vendor:publish --tag=acl-config
php artisan vendor:publish --tag=acl-migrations
php artisan migrate

If those tags don't exist verbatim (the release notes for 1.3.0 contain no itemized changelog), list what the provider actually registers:

php artisan vendor:publish --provider="Nuewire\\Acl\\AclServiceProvider"

Then open the published config/acl.php and record three things before writing any code: the pivot/table names, the default guard, and whether the permission cache is enabled. Those three settings cause most of the "it works in tinker but not in the app" reports later.

The fastest end-to-end vertical slice, and the one to do first on any new install:

  1. Confirm the ACL trait is on your user model. Because the package is optional, the base nuewire/users model may not carry it — add the trait that ships with nuewire/acl to App\Models\User.
  2. Create one role and one permission in tinker and attach them.
  3. Protect one route and check the same ability in a Blade view.
// tinker
$role = Role::create(['name' => 'admin']);
$role->givePermissionTo('users.view');

$user = User::first();
$user->assignRole('admin');

$user->hasRole('admin');          // true
$user->can('users.view');         // true — permissions flow through Laravel's Gate
// routes/web.php
Route::middleware(['auth', 'role:admin'])->group(function () {
    Route::get('/admin/users', [UserController::class, 'index']);
});
@role('admin')
    <a href="/admin/users">Users</a>
@endrole

Once that slice passes, immediately convert the tinker commands into a seeder — tinker-created roles are the number one reason staging and production diverge.

Implementation Patterns

Make the permission graph a deployable artifact. Roles and permissions are data, and data created by hand drifts. Keep a dedicated RolesAndPermissionsSeeder (or an acl:sync command if the package exposes one) that is idempotent and safe to re-run:

foreach (['users.view', 'users.edit', 'billing.manage'] as $permission) {
    Permission::findOrCreate($permission, 'web');
}

$admin = Role::findOrCreate('admin', 'web');
$admin->syncPermissions(['users.view', 'users.edit', 'billing.manage']);

Run it in every environment as part of the release pipeline, not just on a fresh DB. syncPermissions (rather than givePermissionTo) means removing a line from the seeder actually revokes the permission on the next deploy.

Route middleware for coarse access, policies for record-level access. Apply role: / permission: middleware at the route-group level so whole sections are gated in one line, and push "can this user edit this post" into a policy. The policy can still lean on the package for the cheap check:

public function update(User $user, Post $post): bool
{
    return $user->hasPermissionTo('posts.edit')
        && ($post->author_id === $user->id || $user->hasRole('editor'));
}

This split keeps ACL vocabulary (admin, editor, posts.edit) out of your domain model and keeps your controllers free of authorization noise.

Centralize the super-admin bypass. One Gate::before in a service provider beats if ($user->hasRole('super-admin')) scattered across twenty controllers — and it makes audits and tests trivial:

Gate::before(fn (User $user) => $user->hasRole('super-admin') ? true : null);

Return null, not false, when the check doesn't apply, or you'll short-circuit every other gate.

Multiple guards: name roles per guard. If you run web and admin guards, a role attached under one guard will silently fail checks under the other. The reliable day-to-day pattern is distinct role sets per guard (admin.web, admin.api) rather than sharing one role across guards, and always passing the guard explicitly when creating roles and permissions.

Enable the permission cache and warm it at deploy. Permission resolution is read-heavy and happens on nearly every request that touches a policy or @can. Turn the cache on in config/acl.php, and add a cache-bust step to your deploy script so a freshly migrated permission set isn't shadowed by a stale cache from the previous release.

Wrap role mutations in a transaction. Assigning roles in a service or an admin controller is a write to a pivot table plus a cache invalidation. Put both inside DB::transaction() so a failed assignment can't leave a half-applied role set with a warm cache.

A repeatable checklist for adding one new permission — this is the actual day-to-day loop, so script it:

  1. Add the string to the seeder's permission list.
  2. Attach it to the appropriate roles in the same seeder.
  3. Reference it in exactly one place — a route middleware, a policy, or a Blade @can.
  4. Add one authorized and one forbidden request test.
  5. Deploy; the seeder syncs and the cache is cleared.

Factory states for tests. Add a ->admin() / ->withPermission('posts.edit') state to UserFactory so authorization tests read as intent, not setup. For route protection, a small data-provider matrix asserting assertOk for the allowed role and assertForbidden for the others is the cheapest regression insurance you'll ever write:

it('forbids non-admins from the user index', function () {
    $this->actingAs(User::factory()->create())
        ->get('/admin/users')
        ->assertForbidden();
});

Eager-load when listing users. Any admin table showing each user's roles will N+1 without User::with('roles', 'permissions').

Gotchas and Tips

  • Stale permission cache is the classic ACL bug. After any programmatic assignRole / givePermissionTo / syncPermissions, explicitly forget the cache (spatie's app(PermissionRegistrar::class)->forgetCachedPermissions(), or whatever nuewire/acl wraps it with). A role granted in a queued job or an artisan command will not be visible until the cache is dropped.
  • Debug in a fixed order. When a check unexpectedly fails, walk: does the user have the role → does the role have the permission → is it the guard the request is actually using → is the cache stale. Logging $user->getAllPermissions() at the failure point usually ends the investigation in one step.
  • Guard mismatch is the silent failure. No exception, just a false negative. If role:admin rejects a user you know is an admin, check the guard on the role record against the guard on the route before touching any other code.
  • Backfill before you enforce. Publishing and running the ACL migrations on an existing production database leaves every current user with zero roles. Run a one-off backfill command (grant admin to existing staff) before flipping middleware on, or you'll lock out your own team on deploy.
  • Config beats editing migrations. Table names, guard defaults, and cache settings belong in config/acl.php. Renaming a table in a published migration means the next package upgrade either conflicts or silently recreates it. Change the config, leave the migration alone.
  • Don't edit vendor code. Custom role/permission models are swappable via config; audit logging belongs in model events or an observer, not a patched Role class. The first composer update will erase anything you hand-edited.
  • Resist direct user→permission grants. They work, and they create a permission matrix nobody can reason about six months later. If someone needs an exception, make a narrow role for it — it stays visible and revocable.
  • Hiding UI is not security. @role / @can in Blade only controls rendering. Every gated route needs a matching server-side middleware or policy check; pair the two in the same pull request so they can't drift.
  • Treat 1.3.0's contents as unverified. The release notes are a bare compare link with no itemized entries. Before bumping, read the actual diff between 1.2.1 and 1.3.0, pin "nuewire/acl": "^1.3", and run the full suite under strict error reporting to surface deprecations. Do not assume "minor" means "no behavior change" when the changelog is empty.
  • Small-package risk is real. Zero stars and a single maintainer means you are effectively the QA department. Keep your ACL wiring behind your own thin service or trait layer so swapping to raw spatie/laravel-permission (which this wraps) is a config change, not a rewrite.
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