zendframework/zend-permissions-rbac
Role-based access control component for Zend Framework and PHP applications. Define hierarchical roles and permissions, attach assertions for dynamic rules, and check user access through a simple, flexible API.
This package is archived (last release 2019-06-25). For any new work, use laminas/laminas-permissions-rbac instead — it is the maintained fork with the same API, just a different namespace (Laminas\Permissions\Rbac instead of Zend\Permissions\Rbac). For a Laravel app, Laravel's native Gate/Policy system is usually the better default; use this package only if you need a framework-agnostic RBAC graph or are maintaining a legacy Zend/Laminas application.
If you must use it:
composer require zendframework/zend-permissions-rbac
# or, preferably:
composer require laminas/laminas-permissions-rbac
Look first at these classes:
Rbac — the registry and query entry point (addRole, getRole, hasRole, isGranted)Role / RoleInterface — role name, permissions, parent/childrenAssertionInterface — optional dynamic check during isGrantedMinimal first use case — a config-driven role hierarchy:
use Zend\Permissions\Rbac\Rbac;
use Zend\Permissions\Rbac\Role;
$rbac = new Rbac();
$rbac->addRole(new Role('guest'));
$rbac->addRole(new Role('member'), 'guest'); // parent must already exist
$rbac->addRole(new Role('admin'), 'member');
$rbac->getRole('member')->addPermission('posts.view');
$rbac->getRole('admin')->addPermission('posts.delete');
$rbac->isGranted('admin', 'posts.view'); // true — inherited from member/guest graph
$rbac->isGranted('member', 'posts.delete'); // false
The package only models roles and permissions. It does not manage users, persist role assignments, cache anything, or integrate with Laravel. You own those pieces.
Bind one Rbac instance in a service provider. Build it from config or a repository, then resolve it through the container.
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Zend\Permissions\Rbac\Rbac;
use Zend\Permissions\Rbac\Role;
class RbacServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(Rbac::class, function () {
$rbac = new Rbac();
// Register parents before children, or topologically sort the config.
foreach (config('rbac.roles', []) as $name => $definition) {
$role = new Role($name);
foreach ($definition['permissions'] ?? [] as $permission) {
$role->addPermission($permission);
}
$rbac->addRole($role, $definition['parent'] ?? null);
}
return $rbac;
});
}
}
Bridge to Laravel Gate once, then use normal Laravel authorization everywhere. A Gate::before callback lets @can, can: middleware, $this->authorize(), and policies continue to work.
use Illuminate\Support\Facades\Gate;
use Zend\Permissions\Rbac\Rbac;
public function boot(): void
{
Gate::before(function ($user, string $ability) {
$role = $user->rbac_role ?? null; // your user-to-role mapping
if ($role === null) {
return null; // fall through, do not deny globally
}
return app(Rbac::class)->isGranted($role, $ability)
? true
: null; // returning false here would deny every ability
});
}
Then application code stays Laravel-native:
// Controller
$this->authorize('posts.delete');
// Route
Route::delete('/posts/{post}', [PostController::class, 'destroy'])
->middleware('can:posts.delete');
// Blade
@can('posts.delete')
<button>Delete</button>
@endcan
Use a permission naming convention. Treat permissions as stable ability strings such as posts.view, posts.create, posts.delete, users.impersonate. Define them as constants or an enum so typos fail fast and IDE completion works.
Support multi-role users by OR-ing checks. The package checks one role at a time. If users can have several roles, loop over them:
public function userIsGranted($user, string $permission): bool
{
$rbac = app(Rbac::class);
foreach ($user->roles as $role) {
if ($rbac->isGranted($role, $permission)) {
return true;
}
}
return false;
}
Cache the role/permission definition, not necessarily the object. The role graph is often static per deployment. Cache the config array or a serialized structure keyed by an application version, then rebuild the Rbac object. This avoids stale object graphs after deploys and keeps invalidation explicit.
Use assertions only for coarse dynamic checks. AssertionInterface receives the Rbac instance, role, and permission string — not the Eloquent model. It is a poor fit for record-level ownership checks. Use Laravel policies for “can this user edit this specific post?” and keep RBAC for “does this role have the posts.edit ability at all?”
Test the permission matrix directly. A small table-driven test catches accidental inheritance changes:
public function testRoleMatrix(): void
{
$rbac = app(Rbac::class);
$this->assertTrue($rbac->isGranted('admin', 'posts.delete'));
$this->assertTrue($rbac->isGranted('admin', 'posts.view')); // inherited
$this->assertFalse($rbac->isGranted('member', 'posts.delete'));
}
laminas/laminas-permissions-rbac; the migration is mostly a namespace swap from Zend\Permissions\Rbac to Laminas\Permissions\Rbac. Do not install both packages at once.posts.* will not match posts.delete unless you expand wildcards yourself or implement a custom wrapper/matcher. Prefer enumerating concrete permissions.addRole($role, 'parent-name') resolves the parent immediately and will fail if the parent has not been registered. Register parents first or topologically sort your config.$rbac->hasRole($name) or build from a clean instance.strtolower, constants) to avoid Admin vs admin bugs.isGranted() throws for an unknown role; verify in your installed version. Fail closed in your wrapper.can:posts.delete), not role names (if ($user->role === 'admin')). Role checks make the hierarchy rigid and spread policy across the codebase.Role objects are mutable. A container singleton persists across requests in long-running workers. Do not mutate roles per request; rebuild or treat the graph as immutable.getRole($name), its parent/children, and its permissions when a check unexpectedly fails. Most issues are a missing addPermission, a parent registered after a child, or a typo in the ability string.Rbac in your own AuthorizationManager if you need wildcard matching, multi-role OR-ing, audit logging, or a cleaner Laravel-facing API. Keep the Zend/Laminas object as an internal implementation detail.How can I help you explore Laravel packages today?