ohne Third-Party implementieren
Feature Flags ermöglichen Continuous Deployment ohne Feature Branching: Code wird gemergt, aber Features bleiben deaktiviert, bis sie bereit sind. Eine eigene, auf Symfony-Bordmitteln aufgebaute Lösung ist schlanker, schneller und vollständig kontrollierbar — ohne externe Abhängigkeiten und ohne Vendor-Lock-in.
Inhaltsverzeichnis
- 1. Warum Feature Flags und warum selbst bauen
- 2. Die Feature-Flag-Registry: zentrales Flag-Management
- 3. Flags in symfony.yaml konfigurieren
- 4. Twig-Extension für Feature-Flag-Prüfungen in Templates
- 5. Voter-Integration: rollenbasierte Feature Flags
- 6. Dynamische Flags aus der Datenbank
- 7. Cache-Strategie für performante Flag-Prüfungen
- 8. A/B-Testing mit prozentualen Feature Flags
- 9. Eigene Lösung vs. Feature-Flag-Bibliotheken
- 10. Zusammenfassung
- 11. FAQ
1. Warum Feature Flags und warum selbst bauen
Feature Flags (auch Feature Toggles oder Feature Switches genannt) trennen das Deployment von neuem Code vom Release neuer Features an Nutzer. Ein Feature kann in den main-Branch gemergt, getestet und deployt werden, ohne für alle Nutzer sichtbar zu sein. Das ermöglicht Trunk-Based Development ohne langlebige Feature-Branches, reduziert Merge-Konflikte und macht Releases zu einer Entscheidung, nicht zu einem technischen Ereignis. Für interne Beta-Tests können Feature Flags so gesetzt werden, dass nur bestimmte Rollen oder Nutzergruppen neue Features sehen.
Warum nicht einfach ein fertiges Paket nutzen? Symfony-Projekte haben spezifische Anforderungen: Flags sollen in der bekannten services.yaml-Struktur konfigurierbar sein, in Twig-Templates nutzbar sein und mit dem Symfony-Voter-System für rollenbasierte Flags zusammenarbeiten. Eine eigene Lösung ist in der Regel 200–400 Zeilen PHP-Code — überschaubar, vollständig verständlich für das gesamte Team und ohne Abhängigkeit von einem Paket, das möglicherweise nicht gepflegt wird. Die eigene Implementierung kann exakt auf die Anforderungen zugeschnitten werden: statische Konfigurationsflags für Environments, datenbankgetriebene Flags für Laufzeit-Kontrolle und prozentuale Flags für A/B-Tests.
2. Die Feature-Flag-Registry: zentrales Flag-Management
Das Herzstück der eigenen Feature-Flag-Implementierung ist die Registry. Sie ist ein Symfony-Service, der alle konfigurierten Flags kennt und eine einheitliche isEnabled(string $flag): bool-Methode anbietet. Der Service wird über Constructor Injection mit dem Flag-Array aus der Konfiguration versorgt und registriert verschiedene Flag-Provider: statische Flags aus der Konfiguration, datenbankgetriebene Flags für Laufzeit-Kontrolle und nutzerabhängige Flags für rollenbasierte Aktivierung. Das Strategy-Muster macht es einfach, neue Provider hinzuzufügen, ohne die Registry-Klasse zu ändern — ein neuer Provider implementiert ein Interface und wird als getaggter Symfony-Service registriert.
Die Registry cacht Flag-Ergebnisse für die Dauer eines Requests. Das verhindert, dass bei einer Seite mit zehn Feature-Flag-Prüfungen zehn separate Datenbankabfragen ausgeführt werden. Für Flags, die sich nie zur Laufzeit ändern (statische Konfigurationsflags), wird das Ergebnis dauerhaft gecacht. Für datenbankgetriebene Flags wird ein konfigurierbarer TTL-Cache verwendet. Das gibt volle Kontrolle über das Caching-Verhalten ohne Overhead durch eine externe Caching-Bibliothek — Symfony's eigenes CacheInterface ist vollständig ausreichend.
<?php
declare(strict_types=1);
namespace App\FeatureFlag;
use Psr\Cache\CacheItemPoolInterface;
/**
* Central feature flag registry — single point of truth for all flag checks.
* Supports static config flags, database-driven flags and role-based flags.
*/
final class FeatureFlagRegistry
{
/** @var array<string, bool|null> Runtime cache for this request */
private array $cache = [];
/**
* @param array<string, bool> $staticFlags Flags from config (always fast)
* @param iterable<FeatureFlagProviderInterface> $providers Additional providers (DB, role-based)
*/
public function __construct(
private readonly array $staticFlags,
private readonly iterable $providers,
private readonly CacheItemPoolInterface $cachePool,
) {}
/**
* Check if a feature flag is enabled.
* Checks static config first (fast path), then registered providers.
*/
public function isEnabled(string $flag, mixed $context = null): bool
{
// Fast path: check request-level cache first
$cacheKey = $flag . ($context ? '_' . md5(serialize($context)) : '');
if (isset($this->cache[$cacheKey])) {
return $this->cache[$cacheKey];
}
// Static config flags — always override dynamic flags
if (array_key_exists($flag, $this->staticFlags)) {
return $this->cache[$cacheKey] = $this->staticFlags[$flag];
}
// Try each registered provider in priority order
foreach ($this->providers as $provider) {
if ($provider->supports($flag)) {
$result = $provider->isEnabled($flag, $context);
return $this->cache[$cacheKey] = $result;
}
}
// Default: disabled if not explicitly configured
return $this->cache[$cacheKey] = false;
}
/**
* Return all registered flags with their current state — useful for debug toolbar.
*
* @return array<string, bool>
*/
public function getAllFlags(mixed $context = null): array
{
$flags = [];
foreach (array_keys($this->staticFlags) as $flag) {
$flags[$flag] = $this->isEnabled($flag, $context);
}
return $flags;
}
}
3. Flags in symfony.yaml konfigurieren
Statische Feature Flags werden in der services.yaml oder einer eigenen config/packages/feature_flags.yaml als Parameter konfiguriert. Das ermöglicht umgebungsabhängige Konfiguration über Symfony's Environment-System: In dev können alle Flags aktiviert sein, in staging nur ausgewählte, und in prod nur freigegebene Features. Die Konfiguration folgt denselben Konventionen wie die restliche Symfony-Konfiguration — kein neues Konzept, keine eigene Konfigurationssprache.
Der Symfony Dependency-Injection-Container injiziert das Flag-Array als Service-Parameter in die Registry. Das bedeutet: Die Konfiguration ist vollständig statisch zur Build-Zeit des Containers und hat null Laufzeit-Overhead. Keine Datenbankabfrage, keine Redis-Abfrage für statische Flags — sie sind als PHP-Array im kompilierten Container hart verdrahtet. Für Flags, die ohne Cache-Clearing geändert werden müssen, braucht man die dynamischen Provider aus dem nächsten Abschnitt. Die saubere Trennung zwischen statischen (Environment-basierten) und dynamischen (Laufzeit-kontrollierten) Feature Flags ist der Schlüssel zu einem performanten und wartbaren System.
4. Twig-Extension für Feature-Flag-Prüfungen in Templates
In Twig-Templates sind Feature Flags über eine eigene Twig-Extension nutzbar. Die Extension registriert die feature_enabled()-Funktion und optional ein eigenes {% feature 'flag' %}...{% endfeature %}-Tag, das einen Template-Block nur rendert, wenn das Flag aktiv ist. Die Twig-Extension empfängt die FeatureFlagRegistry als Dependency und delegiert alle Prüfungen dorthin. Das bedeutet: Die Caching-Logik der Registry gilt auch für Template-Prüfungen — mehrere feature_enabled('new_checkout')-Aufrufe in einem Template führen nicht zu mehreren Prüfungen.
Das {% feature %}-Tag macht Templates lesbarer als eine verschachtelte {% if feature_enabled() %}-Kette. Es kommuniziert Intent: Dieser Block ist feature-geflaggert und kann deaktiviert werden. Das ist besonders wichtig für Code-Reviews und für die Wartung — nach dem vollständigen Rollout eines Features kann der Entwickler gezielt nach dem Tag suchen und den veralteten Code entfernen. Feature-Flag-Schulden, also Flags, die nie entfernt wurden, sind eines der häufigsten Probleme mit Feature Flags in der Praxis. Eine Namenskonvention und regelmäßiges Audit der aktiven Flags helfen, die technische Schuld unter Kontrolle zu halten.
<?php
declare(strict_types=1);
namespace App\FeatureFlag\Twig;
use App\FeatureFlag\FeatureFlagRegistry;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* Twig extension for feature flag checks in templates.
* Provides feature_enabled() function and optional flag context.
*/
final class FeatureFlagExtension extends AbstractExtension
{
public function __construct(
private readonly FeatureFlagRegistry $registry,
) {}
/** @return TwigFunction[] */
public function getFunctions(): array
{
return [
// Usage in Twig: {% if feature_enabled('new_checkout') %}
new TwigFunction('feature_enabled', $this->isFeatureEnabled(...)),
// Usage: { { feature_list() } } — for debug toolbar display
new TwigFunction('feature_list', $this->getFeatureList(...)),
];
}
/**
* Check if a feature flag is enabled — proxies to registry with request caching.
*/
public function isFeatureEnabled(string $flag, mixed $context = null): bool
{
return $this->registry->isEnabled($flag, $context);
}
/**
* Return all flags with state — useful for debug overlays in dev environment.
*
* @return array<string, bool>
*/
public function getFeatureList(): array
{
return $this->registry->getAllFlags();
}
}
// Usage in Twig template:
// {% if feature_enabled('new_checkout_flow') %}
// { { include('checkout/new-flow.html.twig') } }
// {% else %}
// { { include('checkout/legacy-flow.html.twig') } }
// {% endif %}
// services.yaml registration (auto-tagging via Twig bundle):
// App\FeatureFlag\Twig\FeatureFlagExtension:
// tags: ['twig.extension']
5. Voter-Integration: rollenbasierte Feature Flags
Rollenbasierte Feature Flags prüfen nicht nur, ob ein Flag aktiviert ist, sondern auch, ob der aktuelle Nutzer zur Gruppe gehört, für die das Flag gilt. Das klassische Szenario: Ein neues Admin-Dashboard soll nur für Nutzer mit der Rolle ROLE_BETA sichtbar sein. Der Symfony-Voter ist der natürliche Ort für diese Prüfung — er hat Zugriff auf den SecurityContext und kann Rollen, User-Attribute und jede andere Eigenschaft des eingeloggten Nutzers prüfen. Ein Feature-Flag-Provider, der intern den Voter aufruft, verbindet das Flag-System mit der Symfony-Sicherheitsarchitektur.
Der Vorteil dieser Integration: Rollenbasierte Feature Flags folgen exakt denselben Mustern wie reguläre Symfony-Security-Prüfungen. Das Team muss kein separates Konzept für nutzerabhängige Flags lernen. Änderungen an Nutzerrollen wirken sich automatisch auf die Feature-Flag-Auswertung aus — kein zusätzlicher Konfigurationsaufwand. Und die Voter-Logik ist testbar wie jeder andere Voter: mit einem VoterInterface-Test der einfach die vote()-Methode aufruft und das Ergebnis überprüft, ohne einen vollständigen HTTP-Request aufzubauen.
6. Dynamische Flags aus der Datenbank
Statische Konfigurationsflags reichen für viele Anwendungsfälle aus, aber manchmal müssen Feature Flags zur Laufzeit geändert werden, ohne dass ein Deployment ausgeführt wird. Datenbankgetriebene Flags lösen dieses Problem: Eine einfache Tabelle mit Flag-Name und Status kann über ein Admin-Interface oder per SQL direkt geändert werden, und die Änderung ist nach dem nächsten Cache-Ablauf aktiv. Der Datenbankprovider implementiert dasselbe FeatureFlagProviderInterface wie alle anderen Provider und wird als getaggter Service in die Registry injiziert.
Die Implementierung des Datenbankproviders nutzt Doctrine DBAL oder ein einfaches Doctrine-Repository. Wichtig ist eine Caching-Schicht: Der Provider ruft nicht bei jeder Flag-Prüfung die Datenbank ab. Stattdessen werden alle Flags aus der Datenbank beim ersten Aufruf in einem einzigen Query geladen und für einen konfigurierbaren TTL gecacht. Mit einem TTL von 60 Sekunden reagiert die Anwendung auf Flag-Änderungen innerhalb einer Minute, ohne die Datenbank mit Flag-Queries zu überlasten. In Kombination mit einem Admin-Interface, das nach dem Speichern explizit den Cache invalidiert, kann die Reaktionszeit auf Sekunden reduziert werden.
<?php
declare(strict_types=1);
namespace App\FeatureFlag\Provider;
use App\FeatureFlag\FeatureFlagProviderInterface;
use Doctrine\DBAL\Connection;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
/**
* Database-driven feature flag provider with cache layer.
* Loads all flags from DB in a single query, caches with configurable TTL.
*/
final class DatabaseFeatureFlagProvider implements FeatureFlagProviderInterface
{
/** @var array<string, bool>|null Loaded flags from database */
private ?array $loadedFlags = null;
public function __construct(
private readonly Connection $connection,
private readonly CacheInterface $cache,
private readonly int $cacheTtl = 60,
) {}
public function supports(string $flag): bool
{
// This provider handles any flag that exists in the database
return array_key_exists($flag, $this->getFlags());
}
public function isEnabled(string $flag, mixed $context = null): bool
{
return $this->getFlags()[$flag] ?? false;
}
/**
* Load all flags from database, cached for $cacheTtl seconds.
*
* @return array<string, bool>
*/
private function getFlags(): array
{
if ($this->loadedFlags !== null) {
return $this->loadedFlags; // Request-level cache
}
$this->loadedFlags = $this->cache->get(
'feature_flags.all',
function (ItemInterface $item): array {
$item->expiresAfter($this->cacheTtl);
// Single query loads all flags — no N+1 for flag checks
$rows = $this->connection->fetchAllKeyValue(
'SELECT flag_name, is_enabled FROM feature_flags',
);
// Convert tinyint to bool
return array_map(fn($v) => (bool) $v, $rows);
},
);
return $this->loadedFlags;
}
/**
* Invalidate cache after admin changes a flag — call from admin controller.
*/
public function invalidateCache(): void
{
$this->cache->delete('feature_flags.all');
$this->loadedFlags = null;
}
}
7. Cache-Strategie für performante Flag-Prüfungen
Die Cache-Strategie für Feature Flags in Symfony hat zwei Ebenen: den Request-Level-Cache in der Registry (ein PHP-Array, das für die Dauer eines Requests gepflegt wird) und den persistenten Cache für datenbankgetriebene Flags (Redis oder Memcached über Symfony's Cache-Komponente). Der Request-Level-Cache verhindert Duplicate-Work innerhalb eines Requests: Wenn eine Seite zwanzig Feature-Flag-Prüfungen für dasselbe Flag enthält, trifft nur die erste den Provider. Der persistente Cache verhindert Datenbankabfragen für Flags, die sich selten ändern.
Die Tag-basierte Cache-Invalidierung ist die sauberste Strategie für das Zusammenspiel der Ebenen: Alle datenbankgetriebenen Flags werden mit dem Tag feature_flags gecacht. Wenn ein Admin-Interface ein Flag ändert, wird der Tag invalidiert und alle gecachten Flags werden gleichzeitig ungültig. Das ist präziser als ein einfacher TTL-Cache und reagiert sofort auf Änderungen. In Kombination mit dem Request-Level-Cache ist das Ergebnis: Zero Overhead für statische Flags (im Container verdrahtet), eine Datenbankabfrage pro Cache-Miss für dynamische Flags und keine weitere Abfrage für die restlichen Requests innerhalb des TTL.
8. A/B-Testing mit prozentualen Feature Flags
Prozentuale Feature Flags aktivieren ein Feature nur für einen definierten Prozentsatz der Nutzer — das Grundwerkzeug für A/B-Testing und schrittweise Feature-Rollouts. Die Implementierung nutzt eine deterministische Hash-Funktion: Die User-ID wird mit dem Flag-Namen gehasht und der Hash-Wert modulo 100 verglichen mit dem konfigurierten Prozentsatz. Das Ergebnis ist stabil: Derselbe Nutzer sieht das Feature immer oder nie, bis der Prozentsatz geändert wird. Zufällige Aktivierung pro Request würde zu inkonsistenten Erfahrungen führen, bei denen ein Nutzer zwischen altem und neuem Feature hin- und herspringt.
Der prozentuale Provider in Symfony empfängt den aktuellen User aus dem SecurityContext. Für nicht-authentifizierte Nutzer kann eine Session-ID als Seed verwendet werden — das stellt konsistentes Verhalten auch für anonyme Nutzer sicher. Die Konfiguration des Prozentsatzes erfolgt per Datenbankflag mit einem zusätzlichen rollout_percentage-Feld. Das Admin-Interface ermöglicht es, den Prozentsatz schrittweise von 0 auf 100 zu erhöhen, während das Team das Verhalten der Anwendung beobachtet und beim Auftreten von Problemen sofort zurücksetzt.
9. Eigene Lösung vs. Feature-Flag-Bibliotheken
Auf dem PHP-Paket-Ökosystem gibt es mehrere Feature-Flag-Bibliotheken: unleash/client, flagsmith/flagsmith-php-client und verschiedene Symfony-Bundles. Der direkte Vergleich zeigt, wo eine eigene Lösung vorteilhaft ist und wann eine Bibliothek mehr Sinn ergibt.
| Kriterium | Eigene Lösung | Externe Bibliothek | SaaS-Lösung |
|---|---|---|---|
| Kontrolle | Vollständig | Begrenzt durch API | Vendor-Lock-in |
| Symfony-Integration | Nativ | Bundle nötig | HTTP-Overhead |
| Wartungsaufwand | Selbst verantwortlich | Community | Anbieter |
| A/B-Testing UI | Selbst bauen | Oft minimal | Vollständig inklusive |
| Kosten | Entwicklungszeit einmalig | Meist Open Source | Monatliche Gebühren |
Die Empfehlung: Für Projekte mit klaren, stabilen Anforderungen an Feature Flags ist die eigene Lösung die pragmatischste Wahl. 200–400 Zeilen PHP-Code, vollständig im Symfony-Ökosystem, kein Vendor-Overhead. Für Projekte, die fortgeschrittenes A/B-Testing mit statistischer Auswertung, Multi-Variate-Tests und einem vollständigen Admin-Dashboard brauchen, lohnt sich eine SaaS-Lösung wie LaunchDarkly oder Flagsmith — aber erst dann, wenn diese Anforderungen tatsächlich vorhanden sind und nicht als präventive Komplexität eingebaut werden.
Mironsoft
Symfony-Feature-Entwicklung, Continuous Deployment und Release-Engineering
Feature Flags in Symfony implementieren?
Wir bauen maßgeschneiderte Feature-Flag-Systeme für Symfony-Projekte — von der Flag-Registry über Twig-Integration bis zu dynamischen Datenbank-Flags und prozentualen A/B-Test-Rollouts.
Flag-System Design
Registry, Provider-Architektur und Cache-Strategie für performante Feature-Flag-Prüfungen
Admin-Interface
Symfony EasyAdmin-basiertes Flag-Management mit Cache-Invalidierung und Audit-Log
A/B-Testing
Prozentuale Rollouts und nutzerbasierte Flag-Aktivierung für schrittweise Feature-Releases
10. Zusammenfassung
Feature Flags in Symfony ohne Third-Party-Pakete zu implementieren ist in 200–400 Zeilen PHP-Code machbar und liefert eine Lösung, die vollständig auf die Symfony-Architektur abgestimmt ist. Die Flag-Registry als zentraler Service kombiniert statische Konfigurationsflags (null Laufzeit-Overhead, im Container verdrahtet) mit dynamischen Datenbankflags (gecacht mit konfigurierbarem TTL). Die Twig-Extension macht Flags in Templates ohne Umwege nutzbar. Voter-Integration bindet Flags an die Symfony-Sicherheitsarchitektur. Prozentuale Flags ermöglichen schrittweise Rollouts und A/B-Tests mit deterministischer Nutzer-Zuteilung.
Der entscheidende Vorteil gegenüber externen Lösungen ist die vollständige Kontrolle und Integration: Flag-Konfiguration in denselben YAML-Dateien wie die restliche Symfony-Konfiguration, Flag-Prüfungen in Twig mit bekannter Syntax, Debugging im Symfony-Profiler. Das Feature-Flag-System ist für das gesamte Team transparent und wartbar — kein Blackbox-Paket, das beim nächsten Symfony-Major-Update möglicherweise nicht mitgepflegt wird.
Symfony Feature Flags — Das Wichtigste auf einen Blick
Flag-Registry
Zentraler Service mit isEnabled(string $flag): bool. Request-Level-Cache verhindert Duplicate-Work. Statische und dynamische Provider über Interface entkoppelt.
Twig-Integration
feature_enabled('flag') in Twig via Extension. Dieselbe Registry, dieselbe Cache-Logik. Lesbar in Templates, suchbar bei Flag-Cleanup.
Dynamische Flags
Datenbankprovider lädt alle Flags in einem Query, gecacht per Symfony CacheInterface. Tag-basierte Invalidierung nach Admin-Änderungen.
A/B-Testing
Deterministischer Hash aus User-ID + Flag-Name. Derselbe Nutzer sieht dasselbe Feature immer. Prozentsatz schrittweise von 0 auf 100 erhöhen.