dlnsk/h-rbac
Hierarchical role-based access control for Laravel. Define roles and permissions, assign roles to users, and inherit permissions through role hierarchies. Includes middleware, Blade directives, caching, and a simple API for checking access throughout your application.
composer require dlnsk/h-rbac
php artisan vendor:publish --provider="Dlnsk\HierarchicalRBAC\RBACServiceProvider"
php artisan migrate
Publishing gives you config/h-rbac.php and the role/permission/pivot migrations. Add the trait to your authenticatable model:
use Dlnsk\HierarchicalRBAC\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
}
If you need custom behavior on roles or permissions (extra columns, relationships, scopes), extend the package models and point the config at your classes:
// config/h-rbac.php
'models' => [
'role' => \App\Models\Role::class,
'permission' => \App\Models\Permission::class,
],
Where to look first, in order:
config/h-rbac.php — model mapping, table names, cache settings, callback registry.parent_id on permissions and the pivot tables.Dlnsk\HierarchicalRBAC\Traits\HasRoles — this is the real public API (assignRole, removeRole, hasRole, can integration).First use case — seed a role with a permission, assign it, check it through the native gate:
$role = Role::create(['name' => 'editor']);
$permission = Permission::create(['name' => 'edit-post']);
$role->permissions()->attach($permission);
$user->assignRole('editor');
$user->can('edit-post'); // true
Because it sits on top of native gates, @can, Gate::allows, middleware (can:edit-post), and policies continue to work — you are adding a permission source, not replacing the gate system.
Model the permission tree explicitly. Hierarchy is the whole point: assign the parent, inherit the children.
$manage = Permission::create(['name' => 'manage-posts']);
$edit = Permission::create(['name' => 'edit-post', 'parent_id' => $manage->id]);
$delete = Permission::create(['name' => 'delete-post', 'parent_id' => $manage->id]);
$role->permissions()->sync([$manage->id]); // editor gets manage-posts only
$user->assignRole('editor');
$user->can('delete-post'); // true — inherited
Keep the tree shallow (2–3 levels). Deep trees make “why does this user have this ability?” hard to answer and make cache invalidation harder to reason about.
Seed idempotently. Use firstOrCreate/updateOrCreate keyed by permission name, then resolve parents by name so seeders can be re-run safely in CI and local resets:
$manage = Permission::firstOrCreate(['name' => 'manage-posts']);
Permission::firstOrCreate(['name' => 'edit-post'], ['parent_id' => $manage->id]);
Treat roles as bundles, not users. Assign roles at lifecycle points (registration, invite acceptance, team membership). Avoid ad-hoc assignRole sprinkled in controllers; centralize it in a service or observer so you can invalidate caches in one place.
Use callbacks for ownership and context-dependent checks. The callback mechanism is what distinguishes this package from plain RBAC — it lets a permission consult the actual model being acted on:
// config/h-rbac.php
'callbacks' => [
'update-post' => \App\Hrbac\Callbacks\UpdatePost::class,
],
namespace App\Hrbac\Callbacks;
class UpdatePost
{
public function check($user, $post)
{
return $post->author_id === $user->id;
}
}
$user->can('update-post', $post); // callback receives the extra argument
Prefer class-strings over closures in config — closures break php artisan config:cache.
Compose with policies instead of fighting them. Let the package answer “does this role grant this ability?” and let policies answer “is this particular model in a valid state?”. If both a policy method and a permission share a name, decide deliberately which should win and test it — the gate short-circuits can surprise you.
Super-admin via Gate::before. The package resolves through the gate; a global before-hook is the cleanest escape hatch:
Gate::before(fn ($user) => $user->isSuperAdmin() ? true : null);
Return null (not false) so normal resolution continues for everyone else.
Invalidate caches after bulk changes. If caching is enabled, attaching/detaching permissions or editing the tree leaves users with stale abilities. Flush the affected user's cache (or disable caching in config/h-rbac.php during development) after seeders and admin CRUD operations.
Test through the gate, not the trait. Assertions against $user->can(...) and HTTP responses catch integration mistakes (middleware, policies, cache) that trait-level assertions miss:
$this->actingAs($editor)->get("/posts/{$post->id}/edit")->assertOk();
attach, detach, sync, or tree reshuffle, clear the relevant cache. This is the most common “it works in tinker but not in the browser” bug with this package.roles/permissions tables will collide with spatie/laravel-permission if both are installed. Rename via config/h-rbac.php or run only one.config:cache and callbacks. Closures in the callback map are silently dangerous in production. Always register class-strings, and re-run config:cache after changing mappings.null falls through to the normal role check or denies — this determines whether a callback can be a partial override. Encode the answer in a test.can() arguments are ignored without a callback. Calling $user->can('update-post', $post) when no callback or policy exists will not magically scope the check; the argument is dropped.roles.permissions when listing users or rendering menus, and consider a per-request memoized “can” map for views that check dozens of abilities.parent_id values. Use a FK with cascade, or reparent in an observer.Gate::before returns a non-null value, the package never runs. Audit Gate::before/Gate::after registrations when adding this to an existing app.$user->roles and each role's permissions, then compare to the tree; most “wrong answer” reports are stale cache or a mistyped permission name (the check is name-based and fails closed).How can I help you explore Laravel packages today?