Voters
| A voter class | |
| Check the attribute | |
| Check the subject | |
| Deny by default |
A voter class
A voter is a small class that answers one question, namely whether the current user may perform an action on an object. Symfony calls each voter and grants access when at least one voter agrees.
Create a class that extends Voter and implement two methods called supports and voteOnAttribute. Keep one voter per resource so the logic stays easy to test.
namespace App\Security; use App\Entity\Post; use App\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; class PostVoter extends Voter { protected function supports(string $attribute, mixed $subject): bool { return $attribute === "POST_EDIT"; } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { return true; } }
Check the attribute
The attribute is a plain string such as POST_EDIT or POST_VIEW that names the requested action. The supports method must return true only for attributes the voter understands.
Ignore unknown attributes by returning false, which tells Symfony to ask the next voter. Use constants or clearly spelled strings so template and controller code stay consistent.
namespace App\Security; use App\Entity\Post; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; class PostVoter extends Voter { protected function supports(string $attribute, mixed $subject): bool { return $attribute === "POST_EDIT" || $attribute === "POST_VIEW"; } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { return true; } }
Check the subject
The subject is the object under review, for example a post entity loaded from the database. First confirm the subject type, then compare the current user with the owner stored on the object.
Read the user with getUser on the token and reject anonymous visitors before any other check. This pattern keeps ownership logic in one place instead of scattered conditions.
namespace App\Security; use App\Entity\Post; use App\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; class PostVoter extends Voter { protected function supports(string $attribute, mixed $subject): bool { return $attribute === "POST_EDIT" && $subject instanceof Post; } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { $user = $token->getUser(); if (!$user instanceof User) { return false; } return $subject->getOwner() === $user; } }
Deny by default
A safe voter denies access unless every required condition passes. Return false for unknown users, unknown attributes, and any branch that does not grant access explicitly.
End the method with a plain false so new attributes added later stay closed until you allow them. This default closed rule prevents accidental grants when the code evolves.
namespace App\Security; use App\Entity\Post; use App\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; class PostVoter extends Voter { protected function supports(string $attribute, mixed $subject): bool { return $attribute === "POST_EDIT" && $subject instanceof Post; } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { $user = $token->getUser(); if (!$user instanceof User) { return false; } if ($attribute === "POST_EDIT") { return $subject->getOwner() === $user; } return false; } }
Next: Console commands
Article author: Arthur Isaev