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

Authorization Laravel Package

directorytree/authorization

An easy, native role and permission management system for Laravel. Assign roles and permissions to your models, check abilities, cache lookups, and protect routes with built-in middleware.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up (about 10 minutes):

composer require directorytree/authorization
php artisan migrate   # service provider auto-registers its migrations

Add the trait to your user model:

use DirectoryTree\Authorization\Traits\Authorizable;

class User extends Authenticatable
{
    use Authorizable;
}

That's it — permissions you create are automatically registered with Laravel's Gate, so can(), @can, authorize(), and Gate::allows() all work with permission names as abilities. No custom facade or DSL to learn; everything is native Eloquent relationships.

Where to look first:

  1. DirectoryTree\Authorization\Authorization — the static config class (useUserModel, disablePermissionCache, cacheKey, cacheExpiresIn, disableGateRegistration, ignoreMigrations). This is your control panel.
  2. PermissionRegistrar — the class that pushes permissions into the Gate. You'll touch this in tests.
  3. The traits (Authorizable, ManagesPermissions, HasUsers, HasRoles, ClearsCachedPermissions) — this is where the actual behavior lives, and it's all overridable.

First real use case — a seeded catalog plus one guarded route:

// database/seeders/AuthorizationSeeder.php
use DirectoryTree\Authorization\Permission;
use DirectoryTree\Authorization\Role;

$catalog = collect([
    ['name' => 'users.index',   'label' => 'List Users'],
    ['name' => 'users.create',  'label' => 'Create Users'],
    ['name' => 'users.destroy', 'label' => 'Delete Users'],
])->map(fn ($p) => Permission::firstOrCreate(['name' => $p['name']], $p));

$admin = Role::firstOrCreate(['name' => 'administrator'], ['label' => 'Admin']);
$admin->grantOnly($catalog);   // syncs: removes anything not in the list
// routes/web.php
Route::middleware(['auth', 'permission:users.create'])
    ->post('/users', [UsersController::class, 'store']);
@can('users.destroy')
    <button>Delete</button>
@endcan

That's the whole day-one loop: seed names, grant to roles, assign roles to users, guard routes and views.

Implementation Patterns

Keep the permission catalog in code, not in the admin UI. Treat Permission rows as deployable reference data. A seeder using firstOrCreate keyed on name is idempotent and lets you run it on every deploy. Use grantOnly() (not grant()) when defining role defaults so that removing a permission from the seeder actually revokes it instead of leaving orphans.

Use grant()/revoke() polymorphically — they accept names, models, arrays, and collections:

$role->grant(['users.index', 'users.create']);
$role->grant(Permission::where('name', 'like', 'reports.%')->get());
$user->revoke($permissionModel);
$role->revokeAll();

Passing models is safer in critical paths than passing strings (no name-resolution surprises), and passing collections makes bulk operations cheap.

Layer permissions over policies. Permissions are coarse ("can this user reach the feature at all"); policies are fine-grained ("can this user edit this post"). Keep both:

class PostPolicy
{
    public function update(User $user, Post $post): bool
    {
        return $user->can('posts.update') && $post->user_id === $user->id;
    }
}

Gate-registered permissions and policies coexist fine — the ability name just resolves to whichever is registered.

Authorize at the request boundary, not in the model. The idiomatic path is FormRequest authorize() plus route middleware for bulk guarding:

public function authorize(): bool
{
    return $this->user()->can(['users.index', 'users.create']); // AND semantics
}

Route group + middleware avoids repeating authorize() in every controller method:

Route::middleware('permission:reports.view')->group(function () {
    Route::get('/reports', ...);
});

Grant direct user permissions for one-off exceptions. $user->permissions()->save($p) and $user->grant($p) give user-specific overrides without inventing a role per employee. Use it sparingly — direct grants aren't discoverable from role definitions, so audit them.

Customize models via AuthServiceProvider::boot() before anything else runs:

Authorization::useUserModel(\App\Models\User::class);
Authorization::useRoleModel(\App\Models\Role::class);
Authorization::usePermissionModel(\App\Models\Permission::class);

Your Role needs ManagesPermissions; your Permission needs HasUsers, HasRoles, ClearsCachedPermissions. Missing any of these breaks caching or the inverse relationships silently.

Super-admin bypass with Gate::before — the package has no built-in "all permissions" flag:

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

Return null (not false) so normal checks continue for everyone else.

Tune caching per environment. Keep the cache on in production, disable it locally so DB edits show up instantly:

public function boot()
{
    if (! app()->isProduction()) {
        Authorization::disablePermissionCache();
    }
}

Testing pattern — this is mandatory, not optional:

protected function setUp(): void
{
    parent::setUp();
    app(PermissionRegistrar::class)->register();
}

Then seed a known catalog in setUp or per-test and use actingAs($user). Assert against the same ability strings the middleware and Blade use, so a typo in a Blade @can gets caught.

Gotchas and Tips

Missing PermissionRegistrar::register() in tests is the #1 failure. Gate abilities are registered at boot from cached permissions; in a fresh test DB the cache is empty, so every can() returns false and you'll chase a phantom bug in your trait or seeder.

The permission cache doesn't know about raw DB writes. It flushes on Permission model create/update/delete, but direct DB::table() inserts, manual SQL, or an unusual pivot attach path can leave it stale. If a permission "isn't working" in production, forget the cache key (authorization.permissions by default) before debugging anything else. In local, just disable the cache.

hasPermissions() and can([...]) are AND; hasAnyPermissions() is OR. Mixing these up produces routes that quietly demand more than you intended. hasAnyPermissions has no can() equivalent — that's when you use the package method directly.

Middleware aliases moved in Laravel 11+. README says app/Http/Kernel.php, but on 11/12/13 register in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'role'       => \DirectoryTree\Authorization\Middleware\RoleMiddleware::class,
        'permission' => \DirectoryTree\Authorization\Middleware\PermissionMiddleware::class,
    ]);
})

Always put auth before role/permission. The middleware throws a 403 for a guest (no user to check), which is confusing when you actually wanted a login redirect. Group order matters.

Adopt resource.action naming from day one and enforce it. There's no namespace or validation on name — users.create, users.create , and Users.Create are three different permissions depending on DB collation. Add a unique index and stick to lowercase dot notation.

Custom migrations are an all-or-nothing move. Authorization::ignoreMigrations() must be in AppServiceProvider::register(), and after publishing you own the schema. If you rename tables or columns, you must also override the models and relationship methods — the traits assume the package's conventions. Rename nothing unless you need to.

Custom models must be registered before first use. Setting useUserModel in boot() after another provider has already resolved permissions is a race. Keep it at the top of boot(), and re-run the permission seeder after switching — cached permissions are keyed to the old model's table.

grant('typo.name') behaves differently than you expect. String grants resolve against the catalog; verify in your version whether an unknown name creates a row or silently no-ops, and prefer passing Permission models inside transactional code where a typo would be expensive.

No multi-tenancy. Roles and permissions are global — there's no team/organization scoping. For a tenant app you need either per-tenant role rows plus app-level filtering (which the traits don't provide), or you accept global roles. Evaluate this before committing; retrofitting tenancy into a global permission cache is painful.

Performance in listings. hasPermission() on a collection of users triggers relationship loads. Eager load roles.permissions when rendering permission-heavy tables, or precompute the cache and lean on Gate instead of per-model method calls.

Extension points are your escape hatch. Because everything is trait-based, you can override hasPermission, grant, or the ManagesPermissions sync logic on your own models without forking. If the package's semantics don't fit (e.g., you want permission inheritance through role hierarchy), subclass the model and override the check rather than fighting the cache.

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.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log