Une politique que le composant ne fournit pas

All, AtLeastOneOf et When couvrent l’essentiel de ce qu’une application a à dire. Quand ils n’y suffisent pas, le point d’extension tient en deux classes dans votre propre src, et le profileur continue de dessiner l’arbre sans qu’on lui dise rien.

Une politique est un objet qui implémente AccessPolicyInterface. Un gestionnaire est ce qui sait l’évaluer.

interface AccessPolicyHandlerInterface
{
    public function supports(AccessPolicyInterface $accessPolicy): bool;

    public function handle(
        AccessPolicyInterface $accessPolicy,
        AccessPolicyContext $context,
        AccessPolicyEvaluator $evaluator,
    ): AccessOutcome;
}

Dans une application Symfony, implémenter l’interface constitue tout l’enregistrement : l’autoconfiguration étiquette le service access_control.policy_handler.

Un exemple travaillé : une plage d’heures ouvrables

L’application de démonstration en porte un, #[DuringBusinessHours] : ce qu’il enveloppe s’applique dans la plage, et hors de la plage plus rien ne s’applique.

#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
final readonly class DuringBusinessHours implements CompositeAccessPolicyInterface
{
    public function __construct(
        public array $accessPolicies,
        public int $from = 9,
        public int $to = 18,
        public ?string $message = null,
    ) {
    }
}

Implémenter CompositeAccessPolicyInterface plutôt que l’interface simple est ce qui rend les politiques imbriquées visibles à l’évaluateur, et donc au panneau.

Pourquoi ce n’est pas un simple When

When est le plus proche, et il se comporte à l’envers. L’échec de sa condition le fait s’abstenir : la politique s’efface, et autre chose peut encore accorder.

Une plage hors de laquelle personne ne passe doit refuser à la place. La fermer en s’effaçant serait une façon de l’ouvrir, et cette seule phrase justifie à elle seule un composite maison.

if ($hour < $accessPolicy->from || $hour >= $accessPolicy->to) {
    return AccessOutcome::deny(sprintf('It is %d:00, outside the %d:00-%d:00 window.', $hour, ...));
}

Dans la plage, le gestionnaire évalue chaque politique imbriquée par l’évaluateur qu’on lui a remis, renvoie le premier refus rencontré, et s’abstient si rien n’a accordé : la plage est une condition sur les politiques imbriquées, pas une politique en soi.

Deux détails bons à voler

  • Évaluez les enfants par l’évaluateur, jamais en appelant un autre gestionnaire vous-même. C’est ce qui garde justes les événements, l’arbre du profileur et l’imbrication, quelle que soit la forme que vous inventez.
  • Injectez l’horloge, ne lisez pas l’heure dans l’environnement. Un point d’entrée remplit l’environnement de ce qu’il est seul à connaître, la requête sur le web et la commande sur la console, et l’heure de la journée n’en fait pas partie. Une politique qui s’abstiendrait sur une clé manquante tomberait en position ouverte, et aucun appelant ne devrait pouvoir désactiver une plage par omission.

supports revendique la vôtre et rien d’autre

L’évaluateur s’arrête au premier gestionnaire qui revendique une politique, donc supports() doit reconnaître votre attribut et aucun autre : en revendiquer un étranger, c’est le retirer au gestionnaire qui aurait su le lire.

Et une politique qu’aucun gestionnaire ne prend en charge lève une UnsupportedAccessPolicyException plutôt que d’être sautée. Un attribut que personne ne lit serait une garde absente, la seule panne dont ce composant ne veut pas.