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 Bundle Laravel Package

short-edition/acl-bundle

A Laravel ACL package for role- and permission-based access control. Define roles and permissions, assign them to users, and protect routes, controllers, and UI elements with simple authorization checks that fit Laravel’s middleware and Gate patterns.

View on GitHub
Deep Wiki
Context7

Getting Started

Read the DI extension first. This is a thin Symfony wrapper bundle — almost all of its behavior lives in DependencyInjection/*Extension.php, one Configuration.php, and a couple of service definition files. Those files, not the README, tell you the exact service IDs (ACL provider, voter, cache pool), the config tree keys, and which symfony/security-acl classes are aliased. Start there.

Verify framework compatibility before you write a line of app code. This bundle is a Symfony bundle; it assumes a Symfony kernel, config/bundles.php registration, compiler passes, and Doctrine for the ACL tables. It cannot be dropped into Laravel — only the underlying symfony/security-acl component can be Composer-installed there, and wiring it into Auth/Gate is bespoke work with no precedent. On the Symfony side the bigger problem is that symfony/security-acl was deprecated in 3.4 and removed from core in 4.0. Confirm what this fork actually pins:

composer require short-edition/acl-bundle
composer show short-edition/acl-bundle
composer why-not symfony/security-acl

Register the bundle, then initialize the schema. The ACL model needs five tables (acl_classes, acl_entries, acl_object_identities, acl_object_identity_ancestors, acl_security_identities). Run the init command once per environment and, if you generate migrations rather than letting Doctrine schema-tool create tables, commit the result:

// config/bundles.php
return [
    // ...
    ShortEdition\AclBundle\ShortEditionAclBundle::class => ['all' => true],
];
bin/console init:acl      # Symfony's InitAclCommand, provided by SecurityBundle

First use case: role hierarchy + one object check. The most common day-to-day pattern is "does this user hold permission X on record Y". Model the coarse roles in security.yaml so ROLE_ADMIN inherits ROLE_EDITOR, then use the ACL provider for the per-object grant:

# config/packages/security.yaml
security:
    role_hierarchy:
        ROLE_ADMIN:  [ROLE_EDITOR]
        ROLE_EDITOR: [ROLE_USER]
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;

// Grant ROLE_EDITOR the EDIT permission on a specific Post (id = 42)
$oid = new ObjectIdentity('post', Post::class);
$acl = $aclProvider->createAcl($oid);
$sid = new RoleSecurityIdentity('ROLE_EDITOR');          // or UserSecurityIdentity::fromAccount($user)
$acl->insertObjectAce($sid, MaskBuilder::MASK_EDIT);
$aclProvider->updateAcl($acl);
// Somewhere in a controller / service
if (!$this->isGranted('EDIT', $post)) {
    throw $this->createAccessDeniedException();
}

That check is the whole point — once entries exist, isGranted($attribute, $object) routes through the bundle's voter, which consults the ACL provider. Everything else you build day-to-day is either granting entries, revoking them, or wrapping this in services so it is testable.

Implementation Patterns

Wrap the provider, don't inject it everywhere. MutableAclProviderInterface leaking into ten services makes swapping strategies later (or porting to Laravel Gates) painful. Put a thin PostAcl service on top:

final class PostAcl
{
    public function __construct(private MutableAclProviderInterface $provider) {}

    public function grant(Post $post, string $identity, int $mask): void
    {
        $oid = new ObjectIdentity((string) $post->getId(), Post::class);
        $acl = $this->provider->findAcl($oid);          // reuse if it exists
        $acl->insertObjectAce(new RoleSecurityIdentity($identity), $mask);
        $this->provider->updateAcl($acl);
    }

    public function revoke(Post $post, string $identity, int $mask): void
    {
        $oid = new ObjectIdentity((string) $post->getId(), Post::class);
        $acl = $this->provider->findAcl($oid);
        $acl->deleteObjectAce($acl->getObjectAces()[0]); // match on $identity + $mask in real code
        $this->provider->updateAcl($acl);
    }
}

This is also the seam you would port to Laravel if that decision ever comes up: an interface with one implementation backed by ACL tables, one by Gate/Policy.

Prefer annotations/attributes for controller-level checks. The bundle supports declarative checks, which keeps authorization visible next to the route rather than buried in a service call:

#[IsGranted('EDIT', subject: 'post')]
public function edit(Post $post): Response { /* ... */ }

For anything the annotation cannot express (conditional on status, multi-object), fall back to $this->isGranted('EDIT', $post) — same voter, same result, no surprises.

Use RoleSecurityIdentity for broad grants, UserSecurityIdentity for exceptions. Roles are cheap to maintain and audit; per-user entries are what blow up the acl_entries table. Default to roles, and only add user-level entries where the product genuinely needs "share this one document with Alice".

Custom voters for logic that isn't an ACL entry. The bundle advertises "extensible strategies" — in practice this means registering a custom Voter (or delegating to one from the bundle's) for rules that depend on object state, e.g. "editors can edit drafts, admins can edit published". ACL entries model who; voters model when. Mixing them into the ACL table produces unreadable masks.

final class PostStateVoter extends Voter
{
    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, ['EDIT','DELETE'], true) && $subject instanceof Post;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        /** @var Post $subject */
        if ($subject->isPublished() && $attribute === 'EDIT' && !$token->isGranted('ROLE_ADMIN')) {
            return false;
        }
        return true; // fall through to ACL voter
    }
}

Keep the voter's priority in mind — ACL checks should generally run before state checks, or after, but consistently, or you'll get divergent results in different code paths.

Cache at the provider boundary, not per check. ACL resolution is a chain of DB queries (object identity → ancestors → entries → mask merge). If the bundle exposes a cache pool, wire it to a real backend (Redis/APCu), and invalidate on updateAcl() — every grant/revoke must bust the cache, or you get stale allows. Do not cache in the controller; the same object is checked in many places.

Test the negative path explicitly. The most dangerous failure mode for any ACL layer is a silent bypass — a code path where the voter isn't consulted, or where a missing ACL entry is treated as "allow". Write functional tests that assert 403 for an unpermitted user on every gated route. Given zero stars and zero dependents, there is no upstream test suite catching this for you.

Gotchas and Tips

This is a Symfony bundle, not a Laravel package. If the target stack is Laravel, stop here. There is no bundle concept in Laravel, no auto-discovery, and no bridge to Illuminate\Contracts\Auth\Access\Gate. The only viable Laravel path is to port the data model (object identities, security identities, ACE masks) onto Eloquent and resolve through Gate::before or a custom gate — at which point this package is a schema reference, not a dependency.

symfony/security-acl is deprecated and removed. On modern Symfony (5+/6+/7+) this bundle cannot work as-is unless it vendors the old component. Assume breakage on recent PHP versions until you have run the full test suite against your pinned versions. Treat it as a short-term bridge with a documented migration to voters/expression-based access.

Schema maintenance is on you. Five tables whose rows scale with objects × identities. Seeding 10k posts × 4 roles = 40k ACL rows plus ancestor rows. init:acl gets you the schema; pruning, archiving and re-indexing are your problem. Plan indexes on acl_object_identities.object_identifier and on acl_entries before you ship.

Object identity needs a stable, stringable identifier. new ObjectIdentity((string) $post->getId(), Post::class) — not the entity, not the slug. If the ID changes (re-imports, soft deletes, re-seeds), you orphan the ACL rows silently. Use getObjectIdentifier() consistently across grant and check code, or grants will "disappear".

ACE ordering and masks are not intuitive. MaskBuilder composes permissions with bitwise OR, but resolution merges multiple ACEs and ancestor ACEs. Two edits that look equivalent (one MASK_EDIT, one MASK_VIEW | MASK_EDIT) produce different behaviour when a deny or an ancestor grant is also present. When debugging "why is this allowed?", dump every matching ACE with $acl->getObjectAces() and $acl->getClassAces() — do not reason about masks from memory.

Revoking ≠ deleting the object. deleteAcl($oid) removes the ACL and its entries; deleteAclEntries($acl) prunes selected entries but keeps the ACL. Use the former when the entity is gone (tie it to a Doctrine postRemove listener), the latter for permission changes. Doing one when you meant the other either leaks grants or loses the whole ACL on the next grant.

Cache invalidation is the #1 operational bug. Every updateAcl() must invalidate whatever pool the voter reads from. If you see "grant didn't take effect until the cache expired", the pool wiring in the DI extension is the place to look — trace which service ID the voter receives vs. which your grant path updates.

Annotations need the routing/_token context to be set. The IsGranted attribute evaluates against the current token; in CLI commands, message handlers, and console-triggered jobs there is no token unless you set one. Guard long-running workers: re-authenticate per message, or you will authorize every job as the last user.

Fail closed. Where ACL entries are missing, the voter should deny, not allow. If you find any call path where isGranted() is skipped "because the user is already authenticated", treat it as a defect. Add a middleware/firewall listener or a controller-level assertion so every gated route goes through the same check.

No maintenance, no CVE response. Zero stars, zero dependents, no visible release cadence. If you adopt it: fork to an internal namespace, pin versions, add it to your security review queue, and write integration tests covering mask resolution, identity mapping, and cache invalidation. That is permanent ownership of a Symfony component fork — budget for it as such.

Where to migrate if you can. Laravel → native Gates/Policies + spatie/laravel-permission; Symfony → voters/ExpressionLanguage; cross-service/fine-grained → OPA/Rego, Cedar, Casbin, or SpiceDB. All of them avoid ACL table growth and all of them have maintainers.

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.
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
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect