Juste Doctrine et du cache
Shizuku stocke tes feature flags en base de données via l'ORM DoctrineM. Pas de service externe, pas de fichier YAML.
Dans mon entreprise, chaque feature vivait sur sa propre longue branche. Le jour du merge, c'était la guerre — conflits partout, déploiements bloqués, tout le monde attendait tout le monde. Le vrai problème n'était pas le code, c'était le couplage entre livrer et activer. On avais besoin de merger tôt, déployer souvent, et décider quand activer une feature sans dépendre d'un dashboard externe, d'un service tiers, ni d'un fichier YAML à redéployer.
Alors j'ai construit Shizuku. Un flag en base, une commande en console, et rien d'autre.
feature(), cache TTL, caller-chain tracking.Dépendances
| Package | Version | Rôle |
|---|---|---|
| php | ≥ 8.2 | Requis par Symfony 7.4+ |
| symfony/http-kernel | ^7.4 · ^8.0 | AbstractBundle, DataCollector |
| symfony/console | ^7.4 · ^8.0 | Commandes CLI |
| doctrine/orm | ^3.0 | Persistance de l'entité |
| doctrine/doctrine-bundle | ^2.0 · ^3.0 | ServiceEntityRepository |
| twig/twig | ^3.0 | Fonction feature() |
| symfony/cache-contracts | ^3.0 | Interface de cache (optionnel) |
symfony/cache pour le cache des flags et
symfony/web-profiler-bundle pour le panel Profiler —
branchés automatiquement si présents.
Mise en place
01 — Require
02 — Enregistrer le bundle
// config/bundles.php
return [
Devexploris\ShizukuFeatureFlags\ShizukuFeatureFlagsBundle::class => ['all' => true],
];
03 — Créer la table
Le bundle ne livre pas de migrations. Génère-en une depuis ton application :
Utilisation
PHP
Injecte FeatureFlagService et appelle isEnabled(). Un flag inconnu retourne toujours false.
use Devexploris\ShizukuFeatureFlags\Service\FeatureFlagService;
class MyService
{
public function __construct(private FeatureFlagService $flags) {}
public function doSomething(): void
{
if ($this->flags->isEnabled('my_feature')) {
// do something
}
}
}
Twig
Le bundle enregistre la fonction globale feature(). Les flags inconnus retournent silencieusement false — aucune exception.
{% if feature('my_feature') %}
{# nouveau comportement #}
{% endif %}
{# inline — ex. bascule de classe CSS #}
isEnabled() et feature() retournent false.
Le Profiler Symfony le signale dans son panel pour éviter les typos silencieuses.
Commandes console
Les noms de flags doivent être en snake_case : lettres minuscules, chiffres et underscores, commençant par une lettre.
List
filter est optionnel. All, Enabled, Disabled, Locked. Sans argument, un prompt interactif s'affiche.
Create
Les flags sont créés désactivés par défaut. Passe --enable pour les démarrer actifs.
Enable / Disable
Les flags verrouillés ne peuvent être ni activés ni désactivés.
Lock
Le verrouillage gèle l'état du flag définitivement. Il ne peut plus être activé ni désactivé.
C'est une opération à sens unique: il n'existe pas de commande de déverrouillage.
Un flag verrouillé signale que sa feature a atteint son état final et que les appels
isEnabled() qui l'entourent sont devenus du code mort à supprimer.
Delete
Seuls les flags verrouillés peuvent être supprimés par défaut. --force contourne la restriction (avec confirmation).
Supprimer un flag ne supprime pas ses appels dans votre code.
Tout isEnabled('my_feature') ou feature('my_feature') encore présent retournera silencieusement false
une fois le flag supprimé, votre feature semblera cassée sans erreur, sans exception, sans log.
Un flag verrouillé et activé n'a pas vocation à rester dans le code indéfiniment.
Supprimez chaque morceau if (isEnabled(...)), puis supprimez le flag.
Le panel Profiler liste les flags verrouillés comme rappel jusqu'à leur suppression.
Cycle de vie d'un flag
| 01 | Flag créé désactivé par défaut (ou activé avec --enable). |
| 02 | Peut être togglé librement entre enabled et disabled jusqu'au verrouillage. |
| 03 | Une fois verrouillé, l'état est gelé et ne peut plus être modifié. Il n'existe pas de déverrouillage. La feature est considérée comme livrée ou abandonnée — le flag est désormais de la dette technique. |
| 04 | Nettoyer le code — supprimer chaque appel isEnabled() et feature(). Conserver le chemin gagnant, supprimer l'autre. |
| 05 | Seulement ensuite, supprimer le flag. Un flag supprimé sans nettoyage du code retournera silencieusement false — la feature se casse sans avertissement. |
Mise en cache
Quand cache.app est disponible dans le container, le bundle le branche automatiquement.
L'état des flags est mis en cache 300 secondes et invalidé immédiatement
à chaque commande (enable, disable, lock, delete, create).
Pool personnalisé
# config/services.yaml
Devexploris\ShizukuFeatureFlags\Service\FeatureFlagService:
arguments:
$cache: '@cache.my_custom_pool'
Désactiver le cache
# config/services.yaml
Devexploris\ShizukuFeatureFlags\Service\FeatureFlagService:
arguments:
$cache: ~
Symfony Profiler
Quand symfony/web-profiler-bundle est installé, un panel
Feature Flags est ajouté à la toolbar automatiquement.
| Élément | Description |
|---|---|
| Checked / enabled | Nombre d'appels isEnabled() et combien ont retourné true. |
| Locked flags | Flags gelés en attente de nettoyage, affichés comme "Must be cleaned before removal". |
| Unknown flags | Flags appelés dans le code mais absents de la base. La toolbar se surligne en orange. |
| Caller chain | Chaîne complète Class::method ayant déclenché chaque isEnabled(), frames vendor exclues. |
Référence de l'entité
| Propriété | Type | Description |
|---|---|---|
| name | string | Identifiant unique snake_case, validé à la création. |
| description | string | Label lisible par un humain. |
| isEnabled | bool | Si le flag est actuellement actif. |
| isLocked | bool | Si le flag est gelé en attente de nettoyage. |
| createdAt | DateTimeImmutable | Défini automatiquement à la persistance. |
| enabledAt | DateTimeImmutable|null | Défini lors de la première activation, remis à null à la désactivation. |
| lockedAt | DateTimeImmutable|null | Défini lors du verrouillage. |