Symfony Voters reference
In a nutshell
Symfony’s Voters help you implement authorization in your web applications – the process of checking that a user has the right permission to do a particular action (e.g., publish an article).
Creating a Voter (Permission checker)
The access checking code
Suppose you have a Post
object and you need to decide whether the authenticated user can view or edit the post. In your controller, you might have code like this:
// src/Controller/PostController.php
// ...
class PostController extends AbstractController
{
#[Route('/posts/{id}', name: 'post_show')]
public function show($id): Response
{
// get a Post object - e.g. query for it
$post = ...;
// check for "view" access: calls all voters
$this->denyAccessUnlessGranted('view', $post);
// ...
}
#[Route('/posts/{id}/edit', name: 'post_edit')]
public function edit($id): Response
{
// get a Post object - e.g. query for it
$post = ...;
// check for "edit" access: calls all voters
$this->denyAccessUnlessGranted('edit', $post);
// ...
}
}
The denyAccessUnlessGranted
(and isGranted
) method will invoke Symfony’s “voter” system. At the moment, no voters will vote on whether the user can “view” or “edit” a Post
. But you can implement your own voter that decides this using whatever logic you want.
Creating the custom voter
// src/Security/PostVoter.php
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
{
// these strings are just invented: you can use anything
const VIEW = 'view';
const EDIT = 'edit';
protected function supports(string $attribute, mixed $subject): bool
{
// if the attribute isn't one we support, return false
if (!in_array($attribute, [self::VIEW, self::EDIT])) {
return false;
}
// only vote on `Post` objects
if (!$subject instanceof Post) {
return false;
}
return true;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
// you know $subject is a Post object, thanks to `supports()`
/** @var Post $post */
$post = $subject;
return match($attribute) {
self::VIEW => $this->canView($post, $user),
self::EDIT => $this->canEdit($post, $user),
default => throw new \LogicException('This code should not be reached!')
};
}
private function canView(Post $post, User $user): bool
{
// if they can edit, they can view
if ($this->canEdit($post, $user)) {
return true;
}
// the Post object could have, for example, a method `isPrivate()`
return !$post->isPrivate();
}
private function canEdit(Post $post, User $user): bool
{
// this assumes that the Post object has a `getOwner()` method
return $user === $post->getOwner();
}
}
Custom voters must implement the VoterInterface
or extend the abstract class Voter
.
Voter::supports
You use the Voter::supports
method to determine whether the current voter applies to the attribute and subject combination:
abstract protected function supports(string $attribute, mixed $subject): bool;
If you return true, the voteOnAtttribute
method is called. Otherwise, your voter is done and other voters (if any) will be processed.
Voter::voteOnAttribute
If your voter returns true
from supports()
, this method is called. You return true
to allow access and false
to deny access.
$attribute
: The string passed todenyAccessUnlessGranted
orisGranted()
(e.g., the"view"
string in$this->denyAccessUnlessGranted('view', $post)
).$subject
: The object that is being voted on (e.g., the$post
object in$this->denyAccessUnlessGranted('view', $post)
).$token
: Can be used to retrieve the authenticated user (if any).
Check for roles inside a Voter
// src/Security/PostVoter.php
// ...
use Symfony\Bundle\SecurityBundle\Security;
class PostVoter extends Voter
{
// ...
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
protected function voteOnAttribute($attribute, mixed $subject, TokenInterface $token): bool
{
// ...
// ROLE_SUPER_ADMIN can do anything! The power!
if ($this->security->isGranted('ROLE_SUPER_ADMIN')) {
return true;
}
// ... all the normal voter logic
}
}
Access Decision Strategies
The access decision manager uses a “strategy” to carry out permission checks. There are four strategies available:
affirmative
(default) – This grants access to a resource as soon as there is one voter granting access.consensus
– This grants access if more voters grant access than deny (i.e., there is a consensus). In case of a tie, the decision is based on theallow_if_equal_granted_denied
config options (defaults totrue
).unanimous
– This grants access only if there is no voter denying access.priority
– This grants or denies access by the first voter that does not abstain, based on their service priority.
Regardless of the strategy configured, if all voters abstain from voting (e.g., none return true
in the Voter::supports
method), the decision will be based on the allow_if_all_abstain
config option (defaults to false
).
You can configure the strategy in config/packages/security.yaml
:
# config/packages/security.yaml
security:
access_decision_manager:
strategy: unanimous
allow_if_all_abstain: false
You can use a custom access decision strategy or even a custom access decision manager, if required.
Other notes
- Each time you call the
isGranted()
ordenyAccessUnlessGranted()
methods, or use access controls, Symfony will call all your voters. Symfony will take the responses from all your voters and make a final decision – whether to allow or deny access to a resource. - If you have a lot of voters and calling them all leads to performance issues, you can make your voters implement the
CacheableVoterInterface
to cache the permissions checks.
Sources
Thanks for your comment 🙏. Once it's approved, it will appear here.
Leave a comment