huang-yi/laravel-rbac
Laravel RBAC package for assigning roles and permissions to users and protecting routes with middleware. Includes Blade directives and helpers for permission checks, plus commands to manage roles, permissions, and their relationships in your application.
composer require huang-yi/laravel-rbac
On Laravel 5.5+ the service provider and role / permission middleware aliases are auto-discovered. On older versions register HuangYi\Rbac\RbacServiceProvider in config/app.php and add the aliases in Kernel.php.
Publish and migrate:
php artisan vendor:publish --provider="HuangYi\Rbac\RbacServiceProvider"
php artisan migrate
This gives you a config/rbac.php (where the permission map lives) and the roles / role_user tables. Permissions themselves are not stored in the database — they are declared in config and mapped to role names:
// config/rbac.php
'permissions' => [
'admin' => ['user.index', 'user.create', 'user.delete'],
'editor' => ['post.index', 'post.update'],
],
Wire the trait onto your user model:
use HuangYi\Rbac\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
}
Seed a role and assign it:
$role = \HuangYi\Rbac\Models\Role::create(['name' => 'admin']);
$user->roles()->attach($role);
First use case: protect an admin-only route group.
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get('/admin', AdminController::class)->name('admin.dashboard');
});
Route::middleware(['auth', 'permission:post.update'])->group(function () {
Route::put('/posts/{post}', [PostController::class, 'update'])->name('post.update');
});
The permission string you check should match the route name (post.update) or whatever naming convention you standardize on — the package does a plain lookup against the config array, so consistency is on you.
Two layers, two mental models. Role checks (role:admin) are coarse-grained gates on whole sections; permission checks (permission:post.update) are fine-grained and belong on individual resource routes. Keep role middleware on route groups and permission middleware on single routes — mixing them on the same route is redundant and makes auditing harder.
Treat config/rbac.php as your permission catalog. Since permissions are code, version them alongside routes. A common pattern is naming permissions after route names so Route::currentRouteName() and the config key always line up:
Route::middleware('permission:'.Route::currentRouteName())
That indirection is handy in a base controller or a macro, but only if your route names are already disciplined.
Blade-level guards. Use the trait directly in views rather than adding new directives:
@if(auth()->user()->hasPermission('user.create'))
<a href="{{ route('user.create') }}">New user</a>
@endif
The same calls are what the middleware uses internally, so view and route behavior never drift.
Role management screens. Build a small CRUD over Role for admins and sync assignments with $user->roles()->sync($ids). Remember that renaming a role in that UI silently breaks every config key referencing the old name — either lock role names or add a post-rename note to your deploy checklist.
Seed roles deterministically. Don't let roles be created only through the UI. A RoleSeeder with firstOrCreate(['name' => ...]) for each role in config('rbac.permissions') keys keeps local, staging, and production consistent and makes the config the single source of truth:
foreach (array_keys(config('rbac.permissions')) as $name) {
Role::firstOrCreate(['name' => $name]);
}
Interop with Gates/Policies. The package doesn't replace Laravel's authorization. A pragmatic split: use role: middleware for route-level access, and keep Policies for record-level ownership checks ($user->can('update', $post)). Inside a policy you can consult the trait if you want RBAC to influence record decisions.
Eager-load roles on any page that renders many users or permission-dependent UI to avoid N+1 on the pivot: User::with('roles')->paginate().
php artisan config:cache (and a deploy). config:clear locally when edits don't seem to apply. This is the number one source of "I added a permission but it's still 403."roles.name, so a rename cascades into silent failures. Prefer stable, lowercase names (admin, editor) and never rename casually.auth (or auth:sanctum) before role/permission; otherwise the guard returns a null user and you get an opaque 403 or a null-property error rather than a redirect to login.post.* will not cover post.update unless the package version you pulled supports it (verify against the tag you installed). Copy-paste typos between route names and config are the second most common cause of unexpected 403s.$routeMiddleware manually, or the role:/permission: middleware will throw "class not found."composer.json and run the suite on your target framework before committing — the trait and model are small enough that a fork or a quick patch is realistic if something breaks.auth()->user()->roles to confirm the pivot row exists, then confirm the role name matches a key in config('rbac.permissions'), then confirm the exact permission string. Working outward from the user object localizes the failure in three steps.Role, extend the trait, or override the middleware — plan for that rather than expecting a config flag.hasPermission are cheap compared to queries but still worth avoiding in tight render loops; precompute a $permissions = $user->getPermissions()-style array once per request if you find yourself doing dozens of checks.How can I help you explore Laravel packages today?