Skip to content

How-to : API de drapeaux de fonctionnalités

Référence FT : FT313 (NENE2-FT/flaglog) — Gestion des drapeaux de fonctionnalités : drapeaux par environnement, déploiement progressif avec rollout_percent, remplacements par utilisateur, endpoint d'évaluation avec résolution des remplacements, validation des clés en snake_case, 18 tests / 29 assertions PASS.

Ce guide montre comment construire un système de drapeaux de fonctionnalités supportant la configuration par environnement, les déploiements progressifs par pourcentage, et les remplacements par utilisateur.

Schéma

sql
CREATE TABLE feature_flags (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    key             TEXT    NOT NULL,
    environment     TEXT    NOT NULL DEFAULT 'production',
    enabled         INTEGER NOT NULL DEFAULT 0,
    rollout_percent INTEGER NOT NULL DEFAULT 100,
    created_at      TEXT    NOT NULL,
    updated_at      TEXT    NOT NULL,
    UNIQUE (key, environment)
);

CREATE TABLE flag_overrides (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    flag_key   TEXT    NOT NULL,
    environment TEXT   NOT NULL DEFAULT 'production',
    user_id    TEXT    NOT NULL,
    enabled    INTEGER NOT NULL,
    created_at TEXT    NOT NULL,
    UNIQUE (flag_key, environment, user_id)
);

key doit correspondre à ^[a-z][a-z0-9_]*$ (snake_case). rollout_percent est de 0 à 100.

Endpoints

MéthodeCheminDescription
PUT/flags/{key}Créer ou mettre à jour un drapeau
GET/flagsLister tous les drapeaux (optionnel ?environment=)
GET/flags/{key}/evaluateÉvaluer un drapeau pour un utilisateur (?user_id=)
PUT/flags/{key}/overrides/{userId}Définir un remplacement par utilisateur
DELETE/flags/{key}/overrides/{userId}Supprimer un remplacement par utilisateur

Upsert de drapeau — PUT /flags/

php
// Corps de la requête
{
    "enabled": true,
    "rollout_percent": 50,   // optionnel, défaut 100
    "environment": "staging" // optionnel, défaut "production"
}

// Réponse 200
{
    "key": "dark_mode",
    "enabled": true,
    "rollout_percent": 50,
    "environment": "staging",
    "created_at": "...",
    "updated_at": "..."
}

Le même endpoint crée ou met à jour (UPSERT par key + environment). Envoyer PUT deux fois avec des valeurs différentes met à jour le drapeau.

Validation des clés

php
// Clés valides (snake_case : a-z, 0-9, underscore, commence par une lettre)
dark_mode, beta_ui, new_feature_v2

// Invalide — retourne 422
Dark-Mode   // majuscules + trait d'union
123flag     // commence par un chiffre
my flag     // espace
php
if (!preg_match('/^[a-z][a-z0-9_]*$/', $key)) {
    throw new ValidationException([
        ['field' => 'key', 'message' => 'Key must be snake_case.', 'code' => 'invalid-format'],
    ]);
}

Validation du pourcentage de déploiement

php
if ($rolloutPercent < 0 || $rolloutPercent > 100) {
    throw new ValidationException([
        ['field' => 'rollout_percent', 'message' => 'Must be 0–100.', 'code' => 'out-of-range'],
    ]);
}

Drapeaux par environnement

php
// Même clé, environnements différents
PUT /flags/beta_ui  {"enabled": true,  "environment": "staging"}
PUT /flags/beta_ui  {"enabled": false, "environment": "production"}

// Lister par environnement
GET /flags?environment=staging [{"key": "beta_ui", "enabled": true, ...}]
GET /flags?environment=production [{"key": "beta_ui", "enabled": false, ...}]

Évaluation — Déploiement + Remplacement

GET /flags/{key}/evaluate?user_id={userId}

Ordre de résolution :

  1. Le remplacement gagne : si une ligne flag_overrides existe pour (key, environment, user_id) → utiliser la valeur du remplacement
  2. Drapeau désactivé : si enabled = false → retourner false indépendamment du déploiement
  3. Vérification du déploiement : hacher user_id de manière déterministe → comparer à rollout_percent
php
// 1. Vérifier le remplacement
$override = $this->repo->findOverride($key, $environment, $userId);
if ($override !== null) {
    return new EvaluateResult(enabled: $override->enabled, override: $override->enabled);
}

// 2. Drapeau désactivé
if (!$flag->enabled) {
    return new EvaluateResult(enabled: false, override: null);
}

// 3. Pourcentage de déploiement
$hash = abs(crc32($userId)) % 100;
$enabled = $hash < $flag->rolloutPercent;
return new EvaluateResult(enabled: $enabled, override: null);

Réponse :

json
{"enabled": true, "override": null}   // décision de déploiement
{"enabled": true, "override": true}   // remplacement activé
{"enabled": false, "override": false} // remplacement désactivé

Remplacements par utilisateur

php
// Activer pour un utilisateur spécifique (même si le drapeau est désactivé / déploiement 0%)
PUT /flags/beta_feature/overrides/alice  {"enabled": true}

// Désactiver pour un utilisateur spécifique (même si le drapeau est activé / déploiement 100%)
PUT /flags/global_flag/overrides/bob  {"enabled": false}

// Supprimer le remplacement — revient à la logique de drapeau global + déploiement
DELETE /flags/my_flag/overrides/alice

Le remplacement nécessite le champ enabled (booléen). Champ manquant → 422. Remplacement sur un drapeau inexistant → 404. Supprimer un remplacement inexistant → 404.


À ne pas faire

Anti-patternRisque
Autoriser un format de clé arbitraire (ex. traits d'union, majuscules)Clés incohérentes entre équipes ; difficile à grep/référencer dans le code
Pourcentage de déploiement > 100Erreur logique ; un déploiement à 110% signifie toujours activé même quand prévu comme progressif
Pas de séparation d'environnementLes drapeaux staging se répercutent en production ; les déploiements canary se cassent
Évaluer sans vérification de user_idcrc32(null) ou chaîne vide donne un bucketing déterministe mais incorrect
Retourner 200 pour l'évaluation d'un drapeau manquantL'appelant suppose que le drapeau existe ; traite silencieusement comme désactivé plutôt que de déclencher une alerte
État global des drapeaux en mémoire/cache sans TTLDrapeaux périmés après changement du pourcentage de déploiement ; les changements ne se propagent pas

Publié sous licence MIT.