Expressions et fermetures en guise d’attributs

Un attribut n’est pas obligé d’être une chaîne. Deux votants sont fournis pour les cas où la question est une condition plutôt qu’un nom : l’un prend une expression, l’autre une fermeture.

#[AccessPolicy(new Expression('is_granted("EDIT", subject) and subject.isPublished()'))]

Le votant d’expression traite tout attribut qui est une Expression, et exige symfony/expression-language. Son résultat porte l’expression dans la raison, accordée ou refusée, si bien que le profileur montre ce qui a été évalué plutôt qu’un verdict nu.

Les variables qu’il publie

  • subject et object, la même valeur sous les deux noms, Security ayant employé le second.
  • user, l’utilisateur derrière un jeton, ou le demandeur lui-même quand il n’y a pas de jeton.
  • token, le jeton Symfony quand le demandeur en est un, et null sinon.
  • role_names, les rôles que porte le demandeur, déjà développés par la hiérarchie de rôles.
  • auth_checker, un vérificateur lié à ce demandeur, par lequel passent les fonctions ci-dessous.
  • environment, les circonstances de la question.

Trois autres n’apparaissent que lorsqu’elles signifient quelque chose : actor quand c’est en réalité quelqu’un d’autre qui demande, trust_resolver là où il y en a un, et request quand le sujet est une requête HTTP. Une expression qui nomme actor dit donc ce qu’elle veut dire, et ne peut pas s’évaluer en douce contre une variable présente mais vide.

Les fonctions passent par le vérificateur

is_granted(), is_authenticated(), is_fully_authenticated() et is_remember_me() sont disponibles dans une expression, et chacune est implémentée par un appel sur auth_checker.

Cette indirection est la fonctionnalité : une expression et un votant répondent à la même question de la même façon, y compris dans une application qui a remplacé les votants fournis. Une question posée depuis une expression est elle-même une question, donc le profileur la montre imbriquée sous celle qui l’a déclenchée.

Une chaîne n’est pas une expression

Écrivez new Expression(...) et pensez-le. Une chaîne nue qui ressemble par hasard à une expression est un nom d’attribut comme un autre, et les votants ne la reconnaîtront simplement pas.

Le piège inverse est celui qui a dû être corrigé pendant le développement : une chaîne compilée en silence comme une expression. Une valeur qui a l’air d’une donnée et se comporte comme du code, sans rien sur le site d’appel pour les distinguer, c’est ainsi qu’une faute de frappe devient une règle. C’est aussi pourquoi la condition de When est typée Expression au lieu d’accepter une chaîne.

La moitié rassurante : une expression qui nomme une variable que rien n’a semée lève à la compilation, Variable "actor" is not valid, plutôt que de s’évaluer à rien à trois heures du matin.

Et la fermeture

Le votant de fermeture traite un attribut qui est une Closure, appelée avec la requête d’accès et un vérificateur lié à son demandeur, de sorte qu’elle peut poser une question de plus exactement comme le fait une expression.

$decision = $accessControlManager->decide(new AccessRequest(
    $requester,
    static fn (AccessRequest $r, RequesterBoundChecker $checker): bool
        => $checker->isGranted('EDIT', $r->subject) && $r->subject->isPublished(),
    $post,
));

PHP interdit une fermeture dans les arguments d’un attribut, donc ceci ne vient jamais d’un #[AccessPolicy] écrit dans le source : cela sert la voie programmatique, la même limite avec laquelle vit le votant de fermeture de Symfony Security. Sa raison nomme la fermeture, ce qui est une bonne raison de nommer les vôtres.

Entre les deux, préférez l’expression là où la règle est de la configuration et la fermeture là où la règle est du code. Et préférez un attribut nommé avec un votant derrière là où la règle n’est ni l’un ni l’autre, ce qui arrive plus souvent qu’il n’y paraît.