iya30n/dynamic-acl
A Laravel package for dynamic, database-backed role and permission management. Define roles and permissions at runtime, assign them to users, and protect routes or views with middleware and Blade directives without hardcoding gates.
Install and publish first, integrate second. The package is small and pre-1.0 in spirit, so treat the vendor directory as the source of truth — read it before trusting any README.
composer require iya30n/dynamic-acl
php artisan vendor:publish --provider="Iya30n\DynamicAcl\DynamicAclServiceProvider"
php artisan migrate
vendor/iya30n/dynamic-acl/src/ immediately. Confirm: the publish tags it registers, whether migrations are auto-loaded, and the exact trait name (HasAcl, HasRoles, or similar). This takes two minutes and saves an hour of guessing.role_user, permission_role, etc.) since you'll query them when debugging.User model:
use Iya30n\DynamicAcl\Traits\HasAcl;
class User extends Authenticatable
{
use HasAcl;
}
admin, editor, viewer) and attach permissions in a seeder. Do this before writing any authorization logic — an empty ACL table makes every can() return false, which looks like a broken install.Gate::before super-admin bypass, then verify with php artisan tinker:
User::first()->hasPermission('posts.edit'); // confirm method name from source
The first meaningful win is replacing hardcoded $user->role === 'admin' checks with permission checks that a non-developer can later reconfigure from a UI or seeder.
Permission naming: use resource.action. posts.create, posts.edit, billing.view. Flat, dot-delimited names survive being rendered in checkbox lists and are trivial to glob-match if the package supports wildcards. Avoid sentence-style names and avoid encoding hierarchy (posts.edit.own) unless the package's matching logic explicitly supports it.
Role seeding as a repeatable artifact, not a one-off.
// database/seeders/AclSeeder.php
$admin = Role::updateOrCreate(['name' => 'admin']);
$admin->syncPermissions(Permission::pluck('id')); // verify sync method exists
Use updateOrCreate + sync so the seeder is idempotent and re-runnable in CI and on deploy.
Enforce at all three layers, not just one.
->middleware('can:posts.edit') or the package's own middleware if it provides one.$this->authorize('update', $post) for object-level rules layered on top of the coarse permission check.@can('posts.edit') around action buttons. Never rely on hidden buttons for security; it's UX only.Wrap permission checks in a thin policy or service. If the package's API is thin or its method names shift, you want one place to change:
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->hasPermission('posts.edit')
&& ($user->id === $post->user_id || $user->hasRole('admin'));
}
}
This also gives you a seam to swap the package out later without touching controllers.
Dynamic management means a UI surface. The "dynamic" premise is that roles and permissions are data, not code. Plan for an admin screen or Artisan command from the start:
php artisan acl:grant admin posts.publish
Build the command early — it doubles as your test fixture factory and your emergency hotfix tool in production.
Cache aggressively, invalidate precisely. Permission lookups run on nearly every request. Cache the user's resolved permission set (e.g. Cache::remember("acl:{$user->id}", 3600, fn() => ...)) and forget it in whatever mutation path assigns roles or permissions. If the package ships its own cache layer, confirm the invalidation hooks actually fire on role/permission attach and detach.
The package is stale — verify framework compatibility before committing. Last release June 2023 means it predates recent Laravel majors. Check composer.json constraints for laravel/framework and php. If it won't install on your version, you have three options in order of preference: (1) fork and widen the constraints, (2) use spatie/laravel-permission for far better maintenance, (3) pin an older Laravel. Do not silently use --ignore-platform-reqs in production.
Empty ACL = everything is denied. This is the single most common "it's broken" report. When a can() returns false unexpectedly, run through this checklist in order:
$user->roles; // roles attached at all?
$user->roles->flatMap->permissions; // permissions on those roles?
config('dynamic-acl'); // is the super-admin bypass configured?
Then check Gate::before — if you registered a bypass, it can mask real failures during development.
Guard mismatch is silent. If your users authenticate on web but roles are seeded against a different guard, permission checks simply fail with no error. Keep a guard column or config aligned with your auth guards and assert it in a test.
Cache staleness after role changes. The most frequent production bug in ACL systems: an admin revokes a permission, but the affected user stays privileged until the TTL expires. Write a test that assigns a role, checks access, revokes, and checks again in the same process. If it fails, your invalidation is incomplete.
N+1 on permission checks. Checking permissions in a Blade loop over 50 posts will re-query roles and permissions 50 times. Eager load ($user->load('roles.permissions')) or lean on the cache layer. Watch the query log while loading the index page — this is where ACL packages quietly destroy performance.
Publish the migrations, don't rely on the package's. If the vendor migrations auto-run, later package upgrades can conflict with your applied schema. Copy them into database/migrations, take ownership, and add your own indexes on role_id/permission_id — small packages often omit them.
Extension points to check before forking: the config file (usually lets you override the Role, Permission, and User models) and any contract/interface bindings in the service provider. Overriding model classes via config is far cheaper than a fork when you need extra columns like label or description for your admin UI.
Budget for the exit. With 12 stars and a single 2023 release, bus factor is the real risk. Keep authorization logic behind policies and a small Acl service wrapper so migrating to spatie/laravel-permission is a one-file change plus a data migration — not a rewrite scattered across controllers. If this is going into anything long-lived, make that seam the first thing you build.
How can I help you explore Laravel packages today?