Trois réponses, pas deux

Un votant qui ne comprend pas la question doit le dire plutôt que refuser. Un refus est un avis, le silence n’en est pas un, et traiter les deux de la même façon, c’est ainsi qu’un modèle d’autorisation ferme en douce des portes que personne ne voulait fermer.

Dans Access Control, un votant répond par un AccessOutcome, et il y en a trois.

AccessOutcome::grant('The user owns the post.');
AccessOutcome::deny('Only the author may edit.');
AccessOutcome::abstain('This voter knows nothing about posts.');

L’abstention est un résultat à part entière

La réponse à trois valeurs n’est pas une coquetterie de l’API, c’est ce qui permet à plusieurs votants de partager une question sans se la disputer. On interroge un votant sur les documents à propos d’une transition de workflow, un votant sur les rôles à propos d’une vérification de propriété, et la réponse honnête dans les deux cas est qu’ils n’ont rien à dire.

Renvoyez un refus là et vous exprimez un avis qui n’est pas le vôtre. Sous deny_overrides, cet avis lie la décision pour tous les autres, et la porte se ferme pour une raison qui n’a jamais existé.

Quand on vient d’un booléen

Un votant Symfony Security qui étend Voter renvoie un booléen depuis voteOnAttribute(), et false y signifie refuser. L’abstention existe aussi dans ce modèle, mais elle s’exprime ailleurs, par un supports() qui répond false.

Cela marche jusqu’au jour où l’applicabilité dépend de quelque chose que supports() ne voit pas. Le cas classique : un votant qui ne connaît qu’un certain type de demandeur. supportsSubject() reçoit le sujet, pas la requête, donc le votant doit revendiquer le sujet puis s’abstenir dans vote(). Avec un booléen, il n’y a nulle part où le dire.

if (! $requester instanceof User) {
    return AccessOutcome::abstain('This voter only knows about users.');
}

Ce qui arrive quand tout le monde s’abstient

Le silence doit bien se résoudre en quelque chose au bout du compte, et ce quelque chose se tranche une fois, par configuration, plutôt que par chaque votant qui devine. allow_if_all_abstain vaut false par défaut, donc une question à laquelle personne n’a répondu est refusée, et une requête isolée peut passer outre quand elle en a l’intention.

Ce qui compte, c’est où vit ce réglage : sur le gestionnaire, pas sur chaque algorithme de combinaison. Un réglage obéi par tous les points d’entrée, plutôt que par ceux qui ont pensé à le transmettre.

Relire les trois valeurs

Une décision expose isGranted(), qui répond false pour une abstention exactement comme pour un refus : c’est ce que tout point d’entrée en fait déjà. Quand vous avez besoin de distinguer les deux, lisez decision, une énumération DecisionVote qui garde l’abstention intacte.

Dans les tests, affirmez l’abstention plutôt que de vous contenter de « pas accordé ». Le composant fournit assertAccessAbstained() pour exactement cela, parce que les deux échecs qu’il distingue sont ceux que vous voulez voir dans un diff.