sajad torkamani

In a nutshell

Symfony’s Voters help you implement authorization in your web applications.

Creating a Voter

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.

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:

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

Sources