@@ -56,14 +56,6 @@ which makes creating a voter even easier::
5656
5757.. _how-to-use-the-voter-in-a-controller :
5858
59- .. tip ::
60-
61- Checking each voter several times can be time consuming for applications
62- that perform a lot of permission checks. To improve performance in those cases,
63- you can make your voters implement the :class: `Symfony\\ Component\\ Security\\ Core\\ Authorization\\ Voter\\ CacheableVoterInterface `.
64- This allows the access decision manager to remember the attribute and type
65- of subject supported by the voter, to only call the needed voters each time.
66-
6759Setup: Checking for Access in a Controller
6860------------------------------------------
6961
@@ -310,6 +302,89 @@ If you're using the :ref:`default services.yaml configuration <service-container
310302you're done! Symfony will automatically pass the ``security.helper ``
311303service when instantiating your voter (thanks to autowiring).
312304
305+ Improving Voter Performance
306+ ---------------------------
307+
308+ If your application defines many voters and checks permissions on many objects
309+ during a single request, this can impact performance. Most of the time, voters
310+ only care about specific permissions (attributes), such as ``EDIT_BLOG_POST ``,
311+ or specific object types, such as ``User `` or ``Invoice ``. That's why Symfony
312+ can cache the voter resolution (i.e. the decision to apply or skip a voter for
313+ a given attribute or object).
314+
315+ To enable this optimization, make your voter implement
316+ :class: `Symfony\\ Component\\ Security\\ Core\\ Authorization\\ Voter\\ CacheableVoterInterface `.
317+ This is already the case when extending the abstract ``Voter `` class shown above.
318+ Then, override one or both of the following methods::
319+
320+ use App\Entity\Post;
321+ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
322+ // ...
323+
324+ class PostVoter extends Voter
325+ {
326+ const VIEW = 'view';
327+ const EDIT = 'edit';
328+
329+ protected function supports(string $attribute, mixed $subject): bool
330+ {
331+ // ...
332+ }
333+
334+ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
335+ {
336+ // ...
337+ }
338+
339+ // this method returns true if the voter applies to the given attribute;
340+ // if it returns false, Symfony won't call it again for this attribute
341+ public function supportsAttribute(string $attribute): bool
342+ {
343+ return in_array($attribute, [self::VIEW, self::EDIT], true);
344+ }
345+
346+ // this method returns true if the voter applies to the given object class/type;
347+ // if it returns false, Symfony won't call it again for that type of object
348+ public function supportsAttribute(string $attribute): bool
349+ {
350+ // you can't use a simple Post::class === $subjectType comparison
351+ // because the subject type might be a Doctrine proxy class
352+ return is_a($subjectType, Post::class, true);
353+ }
354+ }
355+
356+ .. _security-voters-change-message-and-status-code :
357+
358+ Changing the message and status code returned
359+ ---------------------------------------------
360+
361+ By default, the ``#[IsGranted] `` attribute will throw a
362+ :class: `Symfony\\ Component\\ Security\\ Core\\ Exception\\ AccessDeniedException `
363+ and return an http **403 ** status code with **Access Denied ** as message.
364+
365+ However, you can change this behavior by specifying the message and status code returned::
366+
367+ // src/Controller/PostController.php
368+
369+ // ...
370+ use Symfony\Component\Security\Http\Attribute\IsGranted;
371+
372+ class PostController extends AbstractController
373+ {
374+ #[Route('/posts/{id}', name: 'post_show')]
375+ #[IsGranted('show', 'post', 'Post not found', 404)]
376+ public function show(Post $post): Response
377+ {
378+ // ...
379+ }
380+ }
381+
382+ .. tip ::
383+
384+ If the status code is different than 403, an
385+ :class: `Symfony\\ Component\\ HttpKernel\\ Exception\\ HttpException `
386+ will be thrown instead.
387+
313388.. _security-voters-change-strategy :
314389
315390Changing the Access Decision Strategy
@@ -481,35 +556,3 @@ must implement the :class:`Symfony\\Component\\Security\\Core\\Authorization\\Ac
481556 // ...
482557 ;
483558 };
484-
485- .. _security-voters-change-message-and-status-code :
486-
487- Changing the message and status code returned
488- ---------------------------------------------
489-
490- By default, the ``#[IsGranted] `` attribute will throw a
491- :class: `Symfony\\ Component\\ Security\\ Core\\ Exception\\ AccessDeniedException `
492- and return an http **403 ** status code with **Access Denied ** as message.
493-
494- However, you can change this behavior by specifying the message and status code returned::
495-
496- // src/Controller/PostController.php
497-
498- // ...
499- use Symfony\Component\Security\Http\Attribute\IsGranted;
500-
501- class PostController extends AbstractController
502- {
503- #[Route('/posts/{id}', name: 'post_show')]
504- #[IsGranted('show', 'post', 'Post not found', 404)]
505- public function show(Post $post): Response
506- {
507- // ...
508- }
509- }
510-
511- .. tip ::
512-
513- If the status code is different than 403, an
514- :class: `Symfony\\ Component\\ HttpKernel\\ Exception\\ HttpException `
515- will be thrown instead.
0 commit comments