Vortos
Authorization

RoleVoter

Role-based checks with hierarchy expansion — hasRole, atLeast, hasAny, hasAll. Inject in policies for clean role decisions.

RoleVoter

RoleVoter provides role-based checks with role hierarchy support. Inject it in your policies for clean, readable authorization logic. The hierarchy is resolved at compile time — expansion is O(1) at runtime.

Methods

use Vortos\Authorization\Voter\RoleVoter;

// Check exact role or any inherited role via hierarchy
$roles->hasRole($identity, 'ROLE_ADMIN')          // bool

// Alias for hasRole — reads better in policy match statements
$roles->atLeast($identity, 'ROLE_USER')           // bool

// User must have ANY of these roles (or inherit one via hierarchy)
$roles->hasAny($identity, ['ROLE_ADMIN', 'ROLE_SUPER_ADMIN'])  // bool

// User must have ALL of these roles (or inherit all via hierarchy)
$roles->hasAll($identity, ['ROLE_USER', 'ROLE_VERIFIED'])      // bool

// Return all roles the identity holds, including inherited ones
$roles->expand($identity)  // string[]  e.g. ['ROLE_ADMIN', 'ROLE_USER']

Role Hierarchy

Define role inheritance in config/authorization.php. A parent role automatically grants all child roles:

config/authorization.php
use Vortos\Authorization\DependencyInjection\VortosAuthorizationConfig;

return static function (VortosAuthorizationConfig $config): void {
    $config->roleHierarchy([
        'ROLE_SUPER_ADMIN'      => ['ROLE_ADMIN'],
        'ROLE_ADMIN'            => ['ROLE_MANAGER', 'ROLE_SUPPORT'],
        'ROLE_MANAGER'          => ['ROLE_USER'],
        'ROLE_SUPPORT'          => ['ROLE_USER'],
        'ROLE_USER'             => [],
    ]);
};

With this hierarchy, a user with ROLE_ADMIN passes any check for ROLE_MANAGER, ROLE_SUPPORT, or ROLE_USER. Hierarchy resolution is recursive — ROLE_SUPER_ADMIN also passes checks for ROLE_MANAGER, ROLE_SUPPORT, and ROLE_USER.

Hierarchy Expansion Example

User has: ['ROLE_ADMIN']

expandRoles(['ROLE_ADMIN'])
    → add ROLE_MANAGER (child of ROLE_ADMIN)
    → add ROLE_SUPPORT (child of ROLE_ADMIN)
    → add ROLE_USER    (child of ROLE_MANAGER and ROLE_SUPPORT)

Result: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_SUPPORT', 'ROLE_USER']

hasRole($identity, 'ROLE_USER') returns true even though the user only has ROLE_ADMIN in their JWT.

Inject in a Policy

#[AsPolicy(resource: 'competitions')]
final class CompetitionPolicy implements PolicyInterface
{
    public function __construct(private RoleVoter $roles) {}

    public function can(
        UserIdentityInterface $identity,
        string $action,
        string $scope,
        mixed $resource = null,
    ): bool {
        return match($action) {
            'list'   => $this->roles->atLeast($identity, 'ROLE_USER'),
            'read'   => $this->roles->atLeast($identity, 'ROLE_USER'),
            'create' => $this->roles->atLeast($identity, 'ROLE_FEDERATION_ADMIN'),
            'update' => $this->roles->atLeast($identity, 'ROLE_FEDERATION_ADMIN'),
            'delete' => $this->roles->hasAny($identity, ['ROLE_ADMIN', 'ROLE_SUPER_ADMIN']),
            default  => false,
        };
    }

    public function supports(string $resource): bool { return $resource === 'competitions'; }
}

Without Hierarchy

If you do not configure a role hierarchy, hasRole() does exact matching only:

// No hierarchy configured — these are equivalent:
$roles->hasRole($identity, 'ROLE_ADMIN')
in_array('ROLE_ADMIN', $identity->roles(), true)

hasAll Example

Require multiple roles simultaneously — for multi-factor authorization:

// User must be both authenticated AND email-verified
$roles->hasAll($identity, ['ROLE_USER', 'ROLE_EMAIL_VERIFIED'])

// User must have both admin and auditor roles
$roles->hasAll($identity, ['ROLE_ADMIN', 'ROLE_AUDITOR'])

Direct Injection (without a policy)

You can inject RoleVoter directly in controllers or handlers for quick checks without writing a full policy:

use Vortos\Authorization\Voter\RoleVoter;
use Vortos\Auth\Identity\CurrentUserProvider;

final class AdminDashboardController
{
    public function __construct(
        private RoleVoter $roles,
        private CurrentUserProvider $currentUser,
    ) {}

    public function dashboard(): JsonResponse
    {
        $identity = $this->currentUser->get();

        if (!$this->roles->atLeast($identity, 'ROLE_ADMIN')) {
            return new JsonResponse(['error' => 'Forbidden'], 403);
        }

        // ...
    }
}

RoleVoter vs PolicyEngine

Use RoleVoter directly for simple role checks in controllers or handlers. Use PolicyEngine when you need the full resource permission system — policy lookup, resource loading, scope handling, and structured AccessDeniedException.

On this page