Déclarer la question là où elle s’applique

Une politique d’accès est une question déclarée là où elle s’applique, en attribut, plutôt que posée dans le corps d’une méthode. Le point d’entrée la lit, interroge, et refuse avant que votre code ne tourne.

use AccessControl\Attribute\AccessPolicy;
use AccessControl\Attribute\Argument;

#[AccessPolicy('EDIT', new Argument('post'))]
public function edit(Post $post): Response
{
    // ...
}

Dans une application Symfony, cette lecture est faite pour vous sur les contrôleurs et sur les commandes de console. En PHP nu, elle est faite par le point d’entrée que vous écrivez, à travers le même évaluateur.

Les six paramètres

new AccessPolicy(
    attribute: 'EDIT',                  // what is being asked
    subject: new Argument('post'),      // what it is being asked about
    strategy: 'deny_overrides',         // the combining algorithm, optional
    environment: ['ip' => '10.0.0.1'],  // extra circumstances, optional
    allowIfAllAbstain: null,            // null defers to the manager
    message: 'Only the author may edit this post.',
);

L’attribut est répétable, et il se pose sur une classe, une méthode ou une fonction. Sur une classe, il s’applique à toutes les méthodes. L’attribute est tout ce qu’un votant comprend : une chaîne, une Closure, une Expression.

Argument : les valeurs connues seulement à l’exécution

Un attribut est statique ; le sujet, en général, ne l’est pas. Argument nomme une valeur que le point d’entrée résout au moment de l’appel, et le nom est celui de l’argument.

En nommer un qui n’existe pas lève une UnknownArgumentException plutôt que de passer null aux votants. Cette distinction compte davantage qu’il n’y paraît : un votant interrogé sur null répond à une autre question, en général en s’abstenant, et une abstention qui atteint une salle vide est exactement la façon dont une garde cesse de s’appliquer sans que personne ne le remarque. Renommer un argument de contrôleur est un remaniement ; ici, c’est un échec de compilation, et c’est bien le but.

message : la seule chose que le visiteur lit

La raison que donne un votant est un diagnostic et n’atteint jamais la réponse. Le message d’une politique est l’inverse : c’est le texte porté par le refus, écrit à côté de la règle, dans les mots que vous voulez faire lire à un humain.

#[AccessPolicy('EDIT', new Argument('post'), message: 'Only the author may edit this post.')]

Deux publics, deux champs, et aucun des deux emprunté à l’autre.

Ce que l’interface promet, et ce qu’elle ne promet pas

Une politique d’accès est n’importe quel objet qui implémente AccessPolicyInterface, et cette interface déclare exactement une chose : message.

Pas de code de statut, pas de code de sortie. Un code de statut est une propriété du point d’entrée, pas de la politique qui a refusé : la même politique vaut un 403 sur le web et un état de sortie 113 sur une console, et chaque point d’entrée tranche le sien. Une politique qui porterait un code HTTP serait une politique inutilisable hors de HTTP.

C’est aussi pourquoi une politique qu’aucun gestionnaire ne prend en charge lève au lieu d’être ignorée : un attribut que personne ne lit est une garde absente. La page des politiques d’accès donne le reste, et les combinateurs qui les composent font l’article de demain.