Ce qui se passe
Un client demande pourquoi son remboursement de mars vaut ce qu'il vaut. Depuis, la
politique a été réécrite — en place, dans le même fichier. La seule trace de ce que
le code faisait à l'époque est dans l'historique Git, et on ne colle pas un diff
dans une réponse au support.
Laravel Rulebook, de Mathias Onea, s'attaque à ce problème précis : les décisions qu'il faut pouvoir expliquer après coup.
composer require mathiasonea/laravel-rulebookIl demande PHP 8.3 et Laravel 12 ou 13. Pas de façade, pas de registre, pas de fichier de configuration, pas de migration : les règles sortent du conteneur de services, et un rulebook est générique sur son sujet, son contexte et son résultat.
Le mécanisme
Chaque règle est une classe PHP ordinaire qui déclare la fenêtre pendant laquelle
elle a fait foi, avec always(), from(), until() ou between(). Les fenêtres
sont semi-ouvertes, si bien que deux années consécutives ne se chevauchent
jamais.
final class FlexibleFareRefund2025 extends FlexibleFareRefund
{
public function validity(): ValidityPeriod
{
return ValidityPeriod::between(
from: new DateTimeImmutable('2025-01-01T00:00:00-05:00'),
until: new DateTimeImmutable('2026-01-01T00:00:00-05:00'),
);
}
protected function noticeInDays(): int { return 7; }
protected function handlingFeeInCents(): int { return 0; }
}La règle de 2026 ouvre au même instant que celle de 2025 se ferme, avec ses propres chiffres : préavis porté à 14 jours et frais de dossier de 3,50 $. La logique d'éligibilité commune reste dans la classe parente ; chaque année ne fournit que ses valeurs.
La résolution rend un gagnant et un seul, désigné par priority() — jamais par
la position dans le tableau des règles. On interroge à une date :
$decision = $rulebook->resolveAt(
subject: new Ticket(reference: 'TCK-4193', priceInCents: 89_00),
at: new DateTimeImmutable('2025-11-02T09:00:00-05:00'),
context: new Cancellation(fare: 'flexible', daysBeforeEvent: 10),
);
$decision->outcome()->formatted(); // $89.00
$decision->winningResult()->reason(); // Refunded under the 2025 policy.Le même appel daté de 2026 rend 0,00 $ : dix jours de préavis ne suffisent plus aux quatorze exigés par la politique en vigueur.
Ce qui fait la valeur
Ce n'est pas la fenêtre de dates, qu'on peut bricoler avec deux if. C'est la
traçabilité de la décision :
- Chaque règle revient avec un statut et une raison, qu'elle ait gagné, perdu, ou qu'elle soit tombée hors de sa fenêtre.
- Cette raison peut porter un
reasonCodesur lequel on filtre — de quoi compter les refus par motif sans analyser du texte libre. snapshot()fige la décision dans un enregistrement JSON, à ranger à côté de l'objet qu'elle concerne. C'est ce qui rend la justification indépendante du code déployé aujourd'hui.- L'échec est explicite :
NoMatchingRulequand rien ne s'applique,AmbiguousRuleMatchquand deux règles s'égalisent. Deux cas qu'unif/elserésout silencieusement, et mal.
À retenir
Le domaine visé est étroit mais fréquent : remboursements, barèmes de commission, taux de TVA, seuils tarifaires — tout ce qui est recalculé plus tard, sous des conditions qui n'existent plus. Si vos règles ne changent jamais, ce paquet n'apporte rien. Si vous avez déjà fouillé Git pour reconstituer un calcul, il apporte exactement ce qui manquait.
Source : Laravel News