Aller au contenu

Placer un indicateur

phil/feature-flags-bundle

Un indicateur se lit à quatre endroits, tous au moyen du même FlagEvaluator : dans une requête, le contrôleur, le gabarit et le cœur lisent donc la même évaluation mémoïsée pour le même contexte.

En bordure : l'attribut de contrôleur

use Phil\FeatureFlagsBundle\Infrastructure\Http\IsFeatureEnabled;

#[IsFeatureEnabled('new-checkout')]                                   // 404 while the flag is off
final class CheckoutController
{
    #[IsFeatureEnabled('checkout-layout', variant: 'two-columns')]    // 404 outside that arm
    public function twoColumns(): Response { /* … */ }
}

IsFeatureEnabled(string $flag, ?string $variant = null) cible une classe ou une méthode et peut être répété. FeatureGateListener écoute kernel.controller et, pour chaque IsFeatureEnabled sur la classe et la méthode du contrôleur, évalue l'indicateur :

  • sans variant, le contrôleur ne s'exécute que tant que l'indicateur est activé ;
  • avec variant, seulement pour les contextes que l'expérience place dans cette variante : chaque branche peut donc avoir ses propres contrôleurs.

Sinon, il lance NotFoundHttpException. Un indicateur désactivé répond 404, pas 403 : une fonctionnalité qui n'est pas sortie n'existe pas encore, et dire qu'elle est interdite reviendrait à dire qu'elle existe.

Un nom d'indicateur invalide dans l'attribut lance InvalidValue à l'appel du contrôleur (une erreur 500), pas à la construction.

Dans le routage : la condition de route

FlagRoutingCondition est enregistré comme service de condition de routage sous l'alias feature_flags, avec deux méthodes, enabled(string $flag): bool et variant(string $flag): ?string :

# A route that matches only while its flag is on; the next route with the same path answers otherwise.
checkout_v2:
    path: /checkout
    controller: App\Controller\CheckoutV2Controller
    condition: "service('feature_flags').enabled('new-checkout')"

checkout:
    path: /checkout
    controller: App\Controller\CheckoutController
#[Route('/checkout', condition: "service('feature_flags').variant('checkout-layout') == 'two-columns'")]

Déclarez la route protégée par un indicateur avant la route actuelle. Les conditions de route exigent symfony/expression-language, et elles sont évaluées au moment de la correspondance, à chaque requête : le routeur appelle donc l'évaluateur.

Une condition de route ne lit qu'un indicateur qui ne cible ni sujets ni locataires. Le routeur fait correspondre la requête dans RouterListener (kernel.request, priorité 32), avant que le pare-feu (priorité 8) charge l'utilisateur et avant les écouteurs qui fixent habituellement le locataire : le contexte selon lequel il évaluerait ne sait pas qui fait la demande. Un indicateur avec une règle d'appartenance ou de pourcentage, ou une expérience, sur le sujet ou le locataire est donc refusé. La condition lance FlagNotRoutable la première fois qu'elle évalue un tel indicateur, avec un message qui nomme l'indicateur, et la requête répond 500. La vérification s'exécute à chaque évaluation : un indicateur modifié dans l'administration pour cibler des locataires est donc refusé dès sa requête suivante. Un indicateur activé ou désactivé pour tout le monde convient, tout comme un indicateur qui cible des projets, pourvu que le résolveur puisse déterminer le projet à partir de la seule requête (son hôte, par exemple) avant le routage.

Un indicateur qui cible des sujets ou des locataires protège un contrôleur avec #[IsFeatureEnabled], qui s'exécute sur kernel.controller, après le pare-feu, et voit donc l'utilisateur authentifié. Pour servir un nouveau contrôleur sur le chemin de l'actuel pour certains utilisateurs seulement, acheminez le chemin vers un seul contrôleur et faites bifurquer le code à l'intérieur, ou protégez le nouveau contrôleur avec l'attribut sur un chemin qui lui est propre.

Dans les gabarits : les fonctions Twig

FeatureFlagsExtension déclare deux fonctions avec #[AsTwigFunction] :

FonctionRetourne
feature_enabled(flag)bool
feature_variant(flag)le nom de la variante sous forme de chaîne, ou null
{% if feature_enabled('new-checkout') %}…{% endif %}
{% if feature_variant('checkout-layout') == 'two-columns' %}…{% endif %}

Elles exigent TwigBundle (l'extension est enregistrée au moyen des étiquettes twig.attribute_extension et twig.runtime).

Dans le cœur

Un service interroge FlagEvaluator::isEnabled() ou variant() une seule fois, au point où il choisit entre deux implémentations, et une méthode nommée de l'application garde le nom de l'indicateur à un seul endroit :

final readonly class OrderMailer
{
    public function __construct(private FlagEvaluator $flags) {}

    private function includeOrderCancellationInEmail(): bool
    {
        return $this->flags->isEnabled(FlagName::of('order-cancellation-in-email'));
    }
}

Lorsque l'indicateur est retiré, seuls cette méthode et ses appelants changent. Évitez de disperser des appels à isEnabled() sur le même indicateur à travers les couches : chacun est une branche à retirer plus tard.

Un indicateur peut aussi choisir une implémentation au niveau des services, avec un petit répartiteur qui contient les deux implémentations et interroge l'évaluateur à chaque appel : la décision reste prise par requête, contrairement à une fabrique résolue une seule fois lorsque le conteneur construit le service.

Choisir l'endroit

EndroitÀ utiliser pour
Attribut de contrôleurUne page ou un point de terminaison entièrement nouveau qui ne doit pas exister tant que l'indicateur est désactivé.
Condition de routeUn nouveau contrôleur qui remplace l'actuel sur le même chemin, pour un indicateur activé ou désactivé pour tout le monde (ou par projet).
Attribut de contrôleur, pour un indicateur cibléUne fonctionnalité pour certains sujets ou locataires : l'attribut s'exécute après le pare-feu.
TwigUn bloc de page affiché ou masqué, une variante de mise en page.
CœurUn changement de comportement dans un service (un nouveau calcul, une nouvelle section de courriel).