Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Roles and Access Control With Voters

Roles and Access Control With Voters

~17 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

access_control from chapter 26 handles COARSE access rights (logged in or not) – for our actual goal from chapter 5 ("only project members see their own projects"), we need FINE-GRAINED, object-based checks: voters.

Why ROLE_USER alone isn't enough

ROLE_USER only answers "is this user LOGGED IN?" – NOT "is THIS user allowed to see THIS SPECIFIC project?". That question depends on the CONCRETE data (is the user a member of THIS project?), not a static role.

Generating a voter

php bin/console make:voter ProjectVoter
src/Security/Voter/ProjectVoter.php
<?php

declare(strict_types=1);

namespace App\Security\Voter;

use App\Entity\Project;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class ProjectVoter extends Voter
{
    public const string VIEW = 'PROJECT_VIEW';
    public const string EDIT = 'PROJECT_EDIT';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::VIEW, self::EDIT], true)
            && $subject instanceof Project;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();

        if (!$user instanceof User) {
            return false;
        }

        /** @var Project $project */
        $project = $subject;

        return match ($attribute) {
            self::VIEW => $project->getMembers()->contains($user),
            self::EDIT => $project->getMembers()->contains($user),
            default => false,
        };
    }
}

supports() and voteOnAttribute() in detail

  • supports() – a quick pre-check: "does THIS voter even feel responsible for this attribute/object combination?" Symfony only calls voteOnAttribute() when this returns true.
  • voteOnAttribute() – the actual LOGIC: true = access granted, false = denied.

self::VIEW/self::EDIT as named constants instead of raw strings prevents typos and makes it visible via IDE autocompletion WHICH attributes this voter even supports.

Using the voter in the controller

src/Controller/ProjectController.php
use App\Security\Voter\ProjectVoter;

#[Route('/projects/{id}', name: 'project_show', requirements: ['id' => '\d+'])]
public function show(int $id, ProjectRepository $projectRepository): Response
{
    $project = $projectRepository->find($id);

    if ($project === null) {
        throw $this->createNotFoundException();
    }

    $this->denyAccessUnlessGranted(ProjectVoter::VIEW, $project);

    return $this->render('project/show.html.twig', ['project' => $project]);
}

denyAccessUnlessGranted() (from AbstractController) calls voteOnAttribute() INTERNALLY and AUTOMATICALLY throws an AccessDeniedException (Symfony turns this into a 403 error page) if access is denied – NO manual if needed.

Checking voter attributes in Twig

{% if is_granted('PROJECT_EDIT', project) %}
    <a href="{{ path('project_edit', {id: project.id}) }}">Edit</a>
{% endif %}

is_granted() is Twig's counterpart to denyAccessUnlessGranted() – here without an exception, but for CONDITIONALLY displaying individual UI elements (only showing the edit link if it's actually ALLOWED to be used).

Simple role checks without a voter

For simple role checks (with no object relation), a voter is OVERKILL – #[IsGranted] as an attribute on the method is enough:

use Symfony\Component\Security\Http\Attribute\IsGranted;

#[Route('/admin/users', name: 'admin_user_index')]
#[IsGranted('ROLE_ADMIN')]
public function index(): Response
{
    // Only reachable for admins
}

Rule of thumb: voter vs. #[IsGranted] vs. access_control

ToolUse case
access_control (security.yaml)COARSE, URL-pattern-based rules – e.g. "the entire /admin area needs ROLE_ADMIN".
#[IsGranted('ROLE_...')]Simple role check on ONE controller method, with NO object relation.
VoterOBJECT-RELATED checks ("does THIS project belong to THIS user?") – like our example.

Tipp: A widespread anti-pattern: checking permission logic directly IN the controller with nested if statements. Voters keep this logic in EXACTLY one place, testable in isolation (chapters 42-43), and reusable across MULTIPLE controllers.