diff --git a/security/voters.rst b/security/voters.rst index 01be3eb0745..c4ba988f9b1 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -48,15 +48,75 @@ which makes creating a voter even easier:: abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool; } -.. _how-to-use-the-voter-in-a-controller: -.. tip:: +The Cacheable Voter Interface +----------------------------- +When Symfony calls the ``isGranted()`` method during runtime, it checks each voter +until it meets the access decision strategy set by the configuration. +While this generally works well, it can slow down performance in some cases. + +Imagine a backend displaying 20 items, each with 6 properties and 3 actions +(like edit, show, delete). Checking permissions for all these requires 180 calls +to each voter. With 5 voters, that's 900 calls. + +Usually, voters only need to focus on a specific permission (e.g., ``EDIT_BLOG_POST``) +or object type (e.g., ``User``). This focus makes voters cacheable, so Symfony 5.4 introduces a +:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\CacheableVoterInterface` + + namespace Symfony\Component\Security\Core\Authorization\Voter; + + interface CacheableVoterInterface extends VoterInterface + { + public function supportsAttribute(string $attribute): bool; + + // $subjectType is the value returned by get_class($subject) or get_debug_type($subject) + public function supportsType(string $subjectType): bool; + } + +If your voter returns false for these methods, it will cache this result, +and your voter won't be called again for that attribute or type. + +If you extend from the abstract Voter class, you don't need to implement +the interface directly - just override ``supportsAttribute()`` and/or ``supportsType()``. + +For instance, if your voter handles multiple object types but all permissions +are prefixed with ``APPROVE_``, do this: + + namespace App\Security; + + use Symfony\Component\Security\Core\Authorization\Voter\Voter; + + class MyVoter extends Voter + { + public function supportsAttribute(string $attribute): bool + { + return str_starts_with($attribute, 'APPROVE_'); + } + + // ... + } + +If your voter handles various permissions for a specific type, do this: + + namespace App\Security; + + use App\Entity\BlogPost; + use Symfony\Component\Security\Core\Authorization\Voter\Voter; + + class MyVoter extends Voter + { + public function supportsType(string $subjectType): bool + { + // you can't use a simple BlogPost::class === $subjectType comparison + // here because the given subject type could be the proxy class used + // by Doctrine when creating the entity object + return is_a($subjectType, BlogPost::class, true); + } + + // ... + } - Checking each voter several times can be time consuming for applications - that perform a lot of permission checks. To improve performance in those cases, - you can make your voters implement the :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\CacheableVoterInterface`. - This allows the access decision manager to remember the attribute and type - of subject supported by the voter, to only call the needed voters each time. +.. _how-to-use-the-voter-in-a-controller: Setup: Checking for Access in a Controller ------------------------------------------