Writing a Custom Voter
Writing a Custom Voter
~16 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Inline security expressions (chapter 51) quickly become UNMANAGEABLE for COMPLEX logic – a voter encapsulates the decision "is this user allowed to edit THIS project?" in its OWN, TESTABLE class.
Creating the ProjectVoter class
docker compose exec php bin/console make:voter ProjectVoter<?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;
final class ProjectVoter extends Voter
{
public const EDIT = 'PROJECT_EDIT';
public const VIEW = 'PROJECT_VIEW';
protected function supports(string $attribute, mixed $subject): bool
{
return \in_array($attribute, [self::EDIT, self::VIEW], 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;
if (\in_array('ROLE_ADMIN', $user->getRoles(), true)) {
return true;
}
return match ($attribute) {
self::VIEW => true,
self::EDIT => $project->getOwner() === $user,
default => false,
};
}
}EXACTLY the same voter scaffolding as in the Symfony course (chapter 40) – supports() decides WHETHER the voter is even RESPONSIBLE, voteOnAttribute() makes the actual YES/NO decision.
Achtung: The voter references $project->getOwner() – this field does NOT exist YET. Chapter 54 adds it. Until then, EDIT fails for ALL users except ROLE_ADMIN, which is UNPROBLEMATIC for the INTERIM development.
Registration: automatic
EXACTLY as in the Symfony course, Symfony's autowiring recognizes the voter AUTOMATICALLY via the Voter interface – NO manual registration in services.yaml needed.
Tipp: PROJECT_VIEW/PROJECT_EDIT as STRING constants (instead of raw strings) prevents typos when calling them LATER in chapter 53 – a typo in a raw string would SILENTLY ALWAYS return "not allowed" instead of throwing an error.