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

Laravel Acl Laravel Package

yajra/laravel-acl

A lightweight Laravel ACL package for role and permission management. Assign roles to users, attach permissions to roles, and check access with simple Eloquent-friendly APIs and Blade helpers.

View on GitHub
Deep Wiki
Context7

Getting Started

Install against your Laravel major, not a floating constraint. The package versions track the framework major, so match them explicitly:

composer require "yajra/laravel-acl:^13.1"

If you are on Laravel 13 / PHP 8.2+, ^13.1 is the floor, not ^13.0 — v13.1.0 carries the serializable class-handling fix that bites the first time an ACL model rides inside a queued job. On Laravel 12, pin the matching v12 line rather than bumping the framework for an authorization package.

Publish, migrate, read the config.

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

That yields roles, permissions, and the pivot tables plus a config file. Read the config before writing any code — it is the shortest document that tells you how the package registers into Gate, which tables it uses, and which cache store backs permission checks. Everything else you might want to change later is a config override, not a fork.

Add the trait to the model your guard actually resolves.

use Yajra\Acl\Traits\HasRole;

class User extends Authenticatable
{
    use HasRole;
}

If you have more than one authenticatable model (admins, customers), add it to each one you intend to authorize — the trait instance is the unit of authorization.

Register middleware aliases in Laravel 11+ style:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'role'       => \Yajra\Acl\Middleware\RoleMiddleware::class,
        'permission' => \Yajra\Acl\Middleware\PermissionMiddleware::class,
    ]);
})

Seed a real catalogue. Run php artisan list acl to see the shipped helper commands, then translate what you need into a seeder that uses firstOrCreate/updateOrCreate keyed on name. Do not hand-create permissions in tinker and expect them to survive a fresh environment.

First use case — close the whole loop on day one. One admin route group behind role:admin, one Blade nav item behind @permission('users.view'), one test proving a roleless user gets a 403. That exercises schema → trait → middleware → presentation in an afternoon, and it is the slice worth demoing. Where to look first: the published config, the HasRole trait source (short, and it is the real API surface — it tells you exactly which methods exist and how caching behaves), and the two middleware classes. Skim the trait before you write anything; it converts the package from a black box into something you could own if maintenance stalled.

Implementation Patterns

Permission names live in constants, never string literals.

final class Permission
{
    public const USERS_VIEW   = 'users.view';
    public const USERS_MANAGE = 'users.manage';
    public const BILLING_EDIT = 'billing.edit';
}

Then @permission(\App\Support\Permission::BILLING_EDIT) and $user->can(\App\Support\Permission::BILLING_EDIT). Typos in string permissions fail toward silent denial in some paths and toward pure confusion in all of them; constants also make the catalogue greppable, diffable in review, and refactorable in one commit.

Role-with-overrides is the shape this package wants. Coarse roles for the bulk of users, per-user grants for exceptions:

$user->attachRole(Role::whereName('manager')->firstOrFail());
$user->attachPermission(Permission::whereName(Permission::BILLING_EDIT)->firstOrFail());

That maps directly onto plan tiers and customer-configured roles. Resist building a roles-per-role hierarchy on top — this is a flat RBAC layer, and pretending otherwise ends in a rewrite of the pivot schema.

Standardize on can() in application code. The package registers permissions with Gate, so:

// FormRequest
public function authorize(): bool
{
    return $this->user()->can(Permission::BILLING_EDIT);
}

works in controllers, requests, policies, and Blade with no extra plumbing. Sprinkling raw hasRole('admin') / hasPermission() calls through feature code is the single most common way these installs rot: those calls bypass Gate, bypass policies, and go invisible to the super-admin hook below.

One Gate::before for super-admins, not a preamble in every policy.

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

Return null, never false — null means "no opinion, continue checking," false would deny everything forever.

Middleware on route groups, grouped by capability. Put the policy where it is reviewable in one screen:

Route::middleware(['auth', 'role:admin'])->prefix('admin')->group(function () {
    Route::middleware('permission:users.manage')->group(function () {
        Route::resource('users', UserController::class);
    });
});

Split coarse and fine-grained deliberately. Use the ACL for module-level access ("can reach the billing area") and native Laravel Policies for ownership ("can edit this invoice"). Ownership in ACL tables is the anti-pattern — the table can't express "row 42 belongs to user 7" without becoming a second, worse ORM.

Blade stays declarative. @role('admin'), @permission('users.manage') in nav and destructive buttons means the "who sees this" spec item shows up as a diff rather than tribal knowledge. Keep the same constants in the directives so renaming a permission is a single-file change.

Flush the ACL cache at the tail of every seeder and every deploy that touches roles or permissions. Make it a line in the deploy script, not a thing someone remembers.

Test as a matrix, not a happy path. Minimum suite:

  • user with role → 200
  • user without role → 403
  • user with a direct permission overriding a role denial → 200
  • guest → 401/redirect
  • super-admin → 200 everywhere

Gotchas and Tips

The dangerous failure mode is fail-open, not fail-closed. A misspelled alias in bootstrap/app.php, a guard misconfigured, or a route group registered outside auth can silently permit rather than deny. Write a CI test that hits a protected route as an unauthenticated user and as a roleless user and asserts 401/403 explicitly — not a redirect, which masks the real state.

Cache staleness generates the tickets. Permission checks are cache-backed, so every "we revoked their access and they still have it" incident traces back here. Establish the flush-on-deploy convention before launch, and treat the cache store as an inherited infrastructure dependency: a shared cache across environments leaks authorization state between them. Support hotfixes done via direct table edits need the flush too — write that down somewhere support can find it.

Pin the exact major. The version contract is effectively "Laravel major → ACL major." A floating constraint across a framework upgrade can resolve to a line that mishandles serialization, and you won't find out until the queue drains.

Prove serialization round-trips before you queue anything holding ACL models. The v13.1.0 fix targets Laravel 13's serializable handling for models embedded in jobs. If roles or permissions get passed into an audit or notification job, add a test that serializes and unserializes the payload. This class of failure surfaces at queue-drain time in production, which is the worst possible moment to discover it.

Seeder idempotency is non-negotiable. Production catalogues drift. updateOrCreate keyed on the permission name reconciles on re-run; create() duplicates and then your whereName(...)->firstOrFail() starts returning whichever row sorted first.

The published config is the extension point. Table names, Gate registration, cache behaviour are all there. If you need a custom cache key or non-standard table names, override the config — never fork the trait. Forking makes framework upgrades painful, and tracking framework majors is this package's main advantage.

Multi-tenancy is not a config flag. If per-tenant role scoping is anywhere on the 12-month roadmap, treat this as a v1 accelerator with a planned re-evaluation, not a foundation. Retrofitting tenant scope means rewriting the pivot schema and revisiting every check site.

It is not an audit system. There is no immutable log of who changed whose role when. If compliance or support needs that, you build it — start with an event listener on role attach/detach that writes to your own table, rather than discovering the gap during evidence collection.

Debugging a confusing denial is usually ordering. Check, in this order: auth runs before role/permission; the trait is on the model the guard actually resolved (not a stale subclass); the route is genuinely in the group you think it is. php artisan route:list --columns=uri,middleware settles the third question in seconds and beats re-reading the routes file.

Bus factor is real but bounded. 113 stars, no commercial support, zero dependents — you are an early adopter. The mitigation is that the code is thin trait-and-middleware composition over a small Eloquent schema, so a read-and-fork fallback genuinely exists. Spend the hour reading the trait source now; it's the difference between a dependency and a liability.

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.
muka/shape-reader
tdt/json
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules
loco/loco
starfederation/datastar-php
yupmin/modern-phystrix
stephenjude/extended-artisan-commands
lumphp/laravel-macroable
lumphp/laravel-collections
lumphp/laravel-contracts
sermepa/sermepa
arkecosystem/fortify