Garder un contrôleur, de trois façons

Une action de contrôleur se garde par un attribut du composant, par l’attribut que Symfony vous a déjà donné, ou par un service que vous injectez. Les trois atteignent les mêmes votants.

L’attribut

final class PostController extends AbstractController
{
    #[AccessPolicy('EDIT', new Argument('post'), message: 'Only the author may edit this post.')]
    public function edit(Post $post): Response
    {
        // ...
    }
}

L’écouteur lit l’attribut, résout new Argument('post') contre les arguments du contrôleur, pose la question, et lève avant que votre code ne tourne. Un refus sort en 403, l’exception portant #[WithHttpStatus(403)], si bien que c’est un 403 même dans une application sans pare-feu pour le transformer en tel.

Sur la classe plutôt que sur la méthode, il s’applique à toutes les actions. Il est répétable, et il se compose avec All, AtLeastOneOf et When.

IsGranted continue de fonctionner

Dès l’instant où le bundle est enregistré, l’attribut de Symfony est lu par ce composant. Rien ne change dans votre code.

#[IsGranted('EDIT', 'post')]
public function edit(Post $post): Response

L’écouteur de la passerelle reproduit le comportement de Security, y compris les parties faciles à rater : un sujet donné sous forme d’Expression est évalué au lieu d’être pris pour un nom d’argument, et methods est comparé sans tenir compte de la casse.

L’écouteur de Security est retiré plutôt que laissé à tourner à côté. Deux écouteurs qui traitent le même attribut poseraient la question deux fois, et le profileur montrerait deux décisions là où le développeur en a écrit une.

Ce qui se perd est circonscrit et bon à savoir. L’écouteur de Security remplit setAttributes(), setSubject() et setAccessDecision() sur l’exception qu’il lève ; l’exception de ce composant n’en porte aucun des trois. Rien dans Symfony ne les lit, donc la perte n’atteint que du code applicatif qui les lit, dans un gestionnaire d’accès refusé ou un gabarit d’erreur.

Les aides, avec ou sans Security

AbstractController::isGranted() et denyAccessUnlessGranted() lèvent une LogicException qui vous dit d’installer SecurityBundle quand Security est absent, quelle que soit l’autre pile enregistrée par l’application. Ce contrôle vit dans FrameworkBundle, que ce projet ne corrige pas. Ajoutez le trait, et les sites d’appel restent tels quels :

use AccessControl\Bundle\Controller\AccessControlTrait;

final class PostController extends AbstractController
{
    use AccessControlTrait;

    public function edit(Post $post): Response
    {
        $this->denyAccessUnlessGranted('EDIT', $post);
        // ...
    }
}

Une méthode de trait l’emporte sur celle héritée de la classe parente, et c’est ce qui fait marcher cela sans toucher un seul appel. Avec Security présent, le trait s’en remet à lui.

Ou injectez le vérificateur

Pour un contrôleur qui n’est pas un AbstractController, ou pour n’importe où ailleurs dans l’application, injectez RequesterBoundChecker. isGranted() répond par un booléen ; decide() rend la décision entière, avec sa raison et ses votes, pour le moment où un booléen cesse de suffire.

Lequel des trois choisir : l’attribut quand la règle appartient à l’action, le vérificateur quand la règle est une branche parmi d’autres à l’intérieur, et #[IsGranted] quand il est déjà écrit et qu’il n’y a rien à gagner à y toucher.