Skip to content

Commit 7ed9a6f

Browse files
committed
minor #21040 [Security] Update the main voters article (javiereguiluz)
This PR was merged into the 6.4 branch. Discussion ---------- [Security] Update the main voters article I propose to add a proper section for cacheable voters ... and move another section because it's a more common use case than the last section of the article. Commits ------- 3fd1265 [Security] Update the main voters article
2 parents 38f03f0 + 3fd1265 commit 7ed9a6f

File tree

1 file changed

+83
-40
lines changed

1 file changed

+83
-40
lines changed

security/voters.rst

Lines changed: 83 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,6 @@ which makes creating a voter even easier::
5050

5151
.. _how-to-use-the-voter-in-a-controller:
5252

53-
.. tip::
54-
55-
Checking each voter several times can be time consuming for applications
56-
that perform a lot of permission checks. To improve performance in those cases,
57-
you can make your voters implement the :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\CacheableVoterInterface`.
58-
This allows the access decision manager to remember the attribute and type
59-
of subject supported by the voter, to only call the needed voters each time.
60-
6153
Setup: Checking for Access in a Controller
6254
------------------------------------------
6355

@@ -296,6 +288,89 @@ If you're using the :ref:`default services.yaml configuration <service-container
296288
you're done! Symfony will automatically pass the ``security.helper``
297289
service when instantiating your voter (thanks to autowiring).
298290

291+
Improving Voter Performance
292+
---------------------------
293+
294+
If your application defines many voters and checks permissions on many objects
295+
during a single request, this can impact performance. Most of the time, voters
296+
only care about specific permissions (attributes), such as ``EDIT_BLOG_POST``,
297+
or specific object types, such as ``User`` or ``Invoice``. That's why Symfony
298+
can cache the voter resolution (i.e. the decision to apply or skip a voter for
299+
a given attribute or object).
300+
301+
To enable this optimization, make your voter implement
302+
:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\CacheableVoterInterface`.
303+
This is already the case when extending the abstract ``Voter`` class shown above.
304+
Then, override one or both of the following methods::
305+
306+
use App\Entity\Post;
307+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
308+
// ...
309+
310+
class PostVoter extends Voter
311+
{
312+
const VIEW = 'view';
313+
const EDIT = 'edit';
314+
315+
protected function supports(string $attribute, mixed $subject): bool
316+
{
317+
// ...
318+
}
319+
320+
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
321+
{
322+
// ...
323+
}
324+
325+
// this method returns true if the voter applies to the given attribute;
326+
// if it returns false, Symfony won't call it again for this attribute
327+
public function supportsAttribute(string $attribute): bool
328+
{
329+
return in_array($attribute, [self::VIEW, self::EDIT], true);
330+
}
331+
332+
// this method returns true if the voter applies to the given object class/type;
333+
// if it returns false, Symfony won't call it again for that type of object
334+
public function supportsAttribute(string $attribute): bool
335+
{
336+
// you can't use a simple Post::class === $subjectType comparison
337+
// because the subject type might be a Doctrine proxy class
338+
return is_a($subjectType, Post::class, true);
339+
}
340+
}
341+
342+
.. _security-voters-change-message-and-status-code:
343+
344+
Changing the message and status code returned
345+
---------------------------------------------
346+
347+
By default, the ``#[IsGranted]`` attribute will throw a
348+
:class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException`
349+
and return an http **403** status code with **Access Denied** as message.
350+
351+
However, you can change this behavior by specifying the message and status code returned::
352+
353+
// src/Controller/PostController.php
354+
355+
// ...
356+
use Symfony\Component\Security\Http\Attribute\IsGranted;
357+
358+
class PostController extends AbstractController
359+
{
360+
#[Route('/posts/{id}', name: 'post_show')]
361+
#[IsGranted('show', 'post', 'Post not found', 404)]
362+
public function show(Post $post): Response
363+
{
364+
// ...
365+
}
366+
}
367+
368+
.. tip::
369+
370+
If the status code is different than 403, an
371+
:class:`Symfony\\Component\\HttpKernel\\Exception\\HttpException`
372+
will be thrown instead.
373+
299374
.. _security-voters-change-strategy:
300375

301376
Changing the Access Decision Strategy
@@ -467,35 +542,3 @@ must implement the :class:`Symfony\\Component\\Security\\Core\\Authorization\\Ac
467542
// ...
468543
;
469544
};
470-
471-
.. _security-voters-change-message-and-status-code:
472-
473-
Changing the message and status code returned
474-
---------------------------------------------
475-
476-
By default, the ``#[IsGranted]`` attribute will throw a
477-
:class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException`
478-
and return an http **403** status code with **Access Denied** as message.
479-
480-
However, you can change this behavior by specifying the message and status code returned::
481-
482-
// src/Controller/PostController.php
483-
484-
// ...
485-
use Symfony\Component\Security\Http\Attribute\IsGranted;
486-
487-
class PostController extends AbstractController
488-
{
489-
#[Route('/posts/{id}', name: 'post_show')]
490-
#[IsGranted('show', 'post', 'Post not found', 404)]
491-
public function show(Post $post): Response
492-
{
493-
// ...
494-
}
495-
}
496-
497-
.. tip::
498-
499-
If the status code is different than 403, an
500-
:class:`Symfony\\Component\\HttpKernel\\Exception\\HttpException`
501-
will be thrown instead.

0 commit comments

Comments
 (0)