selbst implementieren, ohne Drittanbieter-Dienst
Ein externer Feature-Flag-Dienst bringt eine komfortable Oberfläche mit, aber auch eine zusätzliche Abhängigkeit, laufende Kosten und in vielen Fällen Nutzerdaten, die einen dritten Server erreichen, obwohl das gar nicht nötig wäre. Für einen großen Teil der üblichen Anwendungsfälle, von einfachen Ein-Aus-Schaltern bis zu prozentualem Rollout mit Segment-Targeting, reicht ein kompaktes, selbst gebautes Feature-Flag-System in PHP vollkommen aus.
Inhaltsverzeichnis
- 1. Warum ein eigenes Feature-Flag-System ohne Drittanbieter-Dienst
- 2. Grundstruktur der Flag-Konfiguration
- 3. Rollout-Prozentsatz deterministisch implementieren
- 4. Nutzer-Segment-Targeting über einen Kontext
- 5. Saubere Integration in den Anwendungscode
- 6. Testbarkeit von Code hinter Feature Flags
- 7. Flag-Debt vermeiden: alte Flags konsequent aufräumen
- 8. Persistenz und Caching der Flag-Werte
- 9. Häufige Fehler bei der praktischen Umsetzung
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein eigenes Feature-Flag-System ohne Drittanbieter-Dienst
Gründe für eine Eigenentwicklung gibt es mehrere: laufende Kosten bei wachsender Nutzerzahl, Datenschutzbedenken, wenn bei jeder Flag-Auswertung Nutzerkontext an einen dritten Server übertragen wird, und schlicht die Beobachtung, dass die meisten Projekte nur Ein-Aus-Schalter, prozentualen Rollout und einfaches Segment-Targeting brauchen, nicht den vollen Funktionsumfang eines Enterprise-SaaS-Produkts. Hinzu kommt die Kontrolle über die Latenz: Ein selbst gehostetes System muss bei der Auswertung eines Flags nicht erst einen externen Dienst über das Netzwerk erreichen, sondern kann direkt auf lokal vorgehaltene Daten zugreifen, was gerade bei häufig geprüften Flags auf stark frequentierten Seiten spürbar schneller ist.
Ein minimales System deckt drei Bausteine ab: eine verlässliche Quelle für die Flag-Definitionen, eine Entscheidungsfunktion, die ein Flag für einen gegebenen Kontext auswertet, und eine saubere Integrationsstelle im Anwendungscode. Keiner dieser drei Bausteine erfordert zwingend einen externen SaaS-Dienst.
2. Grundstruktur der Flag-Konfiguration
Eine einfache Struktur pro Flag reicht meist völlig aus: ein eindeutiger Schlüssel, ein globaler Ein-Aus-Schalter, ein Rollout-Prozentsatz, eine Liste erlaubter Segmente, eine kurze Beschreibung, ein Erstellungsdatum und ein verantwortlicher Owner. Diese Struktur lässt sich entweder als einfaches PHP-Konfigurationsarray oder als Datenbanktabelle abbilden.
Eine Datenbanktabelle mit einer schlanken Admin-Oberfläche lohnt sich, sobald auch Nicht-Entwickler Flags ohne Deploy umschalten müssen. Reicht es dagegen aus, dass Flags nur über einen Code-Review-Prozess geändert werden, genügt ein einfaches PHP-Konfigurationsarray völlig, ganz ohne zusätzliche Infrastruktur. Ein Mittelweg, der sich in der Praxis bewährt hat, ist ein Konfigurationsarray, das per Deploy gepflegt wird, ergänzt um eine Umgebungsvariable oder einen schnellen Cache-Eintrag für einzelne, dringende Notabschaltungen, ohne dafür gleich eine vollständige Admin-Oberfläche aufbauen zu müssen.
3. Rollout-Prozentsatz deterministisch implementieren
Der naheliegende, aber falsche Ansatz ist ein zufälliger Wurf per random_int() bei jeder einzelnen Auswertung. Das Problem dabei: Derselbe Nutzer landet bei jedem Request erneut in einem anderen Zufallsergebnis, sodass eine Funktion bei einem Seitenaufruf sichtbar ist und beim nächsten Aufruf plötzlich wieder verschwindet, was wie ein Bug wirkt.
Die Lösung ist deterministisches Hashing: Eine stabile Kennung wie die Nutzer-ID wird zusammen mit dem Flag-Schlüssel gehasht und auf einen festen Wertebereich zwischen 0 und 99 abgebildet. Derselbe Nutzer landet für dasselbe Flag dadurch bei jeder Auswertung im exakt gleichen Bucket und bleibt somit konsistent innerhalb oder außerhalb des Rollouts, unabhängig davon, wie oft er die Seite aufruft.
<?php
declare(strict_types=1);
namespace App\FeatureFlags;
final class FeatureFlagService
{
public function __construct(private readonly FlagRepository $repository)
{
}
public function isEnabledForUser(string $flagKey, string $userId): bool
{
$flag = $this->repository->find($flagKey);
if ($flag === null || !$flag->enabled) {
return false;
}
if ($flag->rolloutPercentage >= 100) {
return true;
}
// Deterministic bucketing: the same user and flag key always hash
// to the same bucket, so a user never flips in and out of a
// rollout between two requests for the same page.
$bucket = crc32($flagKey . ':' . $userId) % 100;
return $bucket < $flag->rolloutPercentage;
}
}
4. Nutzer-Segment-Targeting über einen Kontext
Über den reinen Rollout-Prozentsatz hinaus lässt sich gezieltes Targeting umsetzen, indem ein FeatureFlagContext-Objekt die relevanten Merkmale eines Nutzers bündelt: Kundengruppe, Land, Beta-Tester-Status oder beliebige andere fachliche Segmentmerkmale.
Bei der Auswertung haben explizite Segment-Regeln Vorrang vor dem prozentualen Rollout: Ein als Beta-Tester markierter Nutzer sieht eine neue Funktion beispielsweise unabhängig vom aktuellen Rollout-Prozentsatz, während alle übrigen Nutzer weiterhin über das deterministische Hashing in den Rollout gelangen oder eben nicht.
<?php
declare(strict_types=1);
namespace App\FeatureFlags;
final readonly class FeatureFlagContext
{
public function __construct(
public string $userId,
public string $customerGroup = 'default',
public string $countryCode = 'DE',
public bool $isBetaTester = false,
) {
}
}
final class TargetingFeatureFlagService
{
public function __construct(private readonly FlagRepository $repository)
{
}
public function isEnabled(string $flagKey, FeatureFlagContext $context): bool
{
$flag = $this->repository->find($flagKey);
if ($flag === null || !$flag->enabled) {
return false;
}
// Explicit segment rules take priority over the percentage rollout.
if ($context->isBetaTester && $flag->allowBetaTesters) {
return true;
}
if (in_array($context->customerGroup, $flag->allowedCustomerGroups, true)) {
return true;
}
$bucket = crc32($flagKey . ':' . $context->userId) % 100;
return $bucket < $flag->rolloutPercentage;
}
}
5. Saubere Integration in den Anwendungscode
Der Zugriff auf Flags sollte hinter einem Interface oder einem injizierbaren Service gekapselt werden, statt rohe Konfigurationsarrays über den gesamten Code verstreut direkt auszulesen. Ein Aufruf wie $flags->isEnabled('new_checkout', $context) liest sich an jeder Stelle im Code sofort verständlich und verrät nichts über die dahinterliegende Speicherform.
Tief verschachtelte, kombinierte Flag-Bedingungen über mehrere Aufrufstellen hinweg sollten vermieden werden. Sinnvoller ist es, die Entscheidung einmal, möglichst nah am Einstiegspunkt eines Requests, zu treffen und anschließend einen einfachen booleschen Wert oder ein passendes Strategie-Objekt weiterzureichen, statt dieselbe Flag-Prüfung an vielen Stellen zu wiederholen.
6. Testbarkeit von Code hinter Feature Flags
Wird der Feature-Flag-Service als austauschbare Abhängigkeit injiziert statt als globaler Zustand direkt abgefragt, können Tests ihn durch eine feste, vorhersagbare Implementierung ersetzen, statt sich auf echten Konfigurations- oder Datenbankzustand zu verlassen. Damit werden beide Zweige, aktiviertes und deaktiviertes Flag, unabhängig voneinander explizit testbar.
Für die Rollout-Prozentsatz- und Segment-Logik selbst lohnen sich zusätzlich dedizierte Unit-Tests mit festen Kontextobjekten, die Randfälle abdecken: 0 Prozent Rollout, 100 Prozent Rollout, ein exakter Segment-Treffer sowie ein Nutzer außerhalb jedes Segments.
7. Flag-Debt vermeiden: alte Flags konsequent aufräumen
Flag-Debt entsteht, wenn Feature Flags lange nach abgeschlossenem Rollout oder einer endgültigen Entscheidung weiter im Code verbleiben. Mit der Zeit sammeln sich so tote Codezweige an, die kognitive Last beim Lesen des Codes erhöhen und im schlimmsten Fall zu widersprüchlichen, sich gegenseitig überlagernden Flags führen.
Wirksam dagegen ist ein fester Owner und ein erwartetes Entfernungsdatum, die jedem Flag bereits bei seiner Erstellung mitgegeben werden, kombiniert mit einem regelmäßigen Review, etwa vierteljährlich, das alle Flags mit 0 oder 100 Prozent Rollout auflistet und aktiv zur Entscheidung stellt, ob Flag und toter Codezweig endgültig entfernt werden können.
8. Persistenz und Caching der Flag-Werte
Werden Flag-Definitionen in einer Datenbank gespeichert, entsteht ohne Caching pro Request und pro geprüftem Flag eine zusätzliche Datenbankabfrage, was bei mehreren Flags und hoher Last spürbar Last erzeugt. Ein kurzes Caching der Flag-Definitionen selbst, etwa in APCu oder Redis mit einer TTL von wenigen Sekunden bis Minuten, reduziert diese Last deutlich, ohne die Aktualität nennenswert zu beeinträchtigen.
Sinnvoller als reines Warten auf den Ablauf der TTL ist eine explizite Invalidierung des Caches, sobald ein Flag über die Admin-Oberfläche geändert wird. So wirkt eine Umschaltung nahezu sofort, ohne dass bei jedem einzelnen Request unnötig die Datenbank belastet werden muss.
9. Häufige Fehler bei der praktischen Umsetzung
Ein häufiger Fehler ist ein nicht-deterministischer Rollout, bei dem Nutzer zwischen zwei Requests scheinbar zufällig zwischen aktiviertem und deaktiviertem Zustand wechseln, was wie ein Bug wirkt und das Vertrauen in das gesamte System untergräbt. Fast ebenso verbreitet ist die fehlende Aufräumung alter Flags, wodurch der Code über die Zeit mit totem Gewicht angereichert wird.
Weitere häufige Fehler sind fehlende Owner- und Ablaufdatum-Metadaten pro Flag, tief verschachtelte Flag-Prüfungen über viele Dateien hinweg statt einer zentralen Entscheidung, fehlende Tests für den deaktivierten Zweig eines Flags, der dadurch unbemerkt verrotten kann, sowie Entscheidungsfunktionen, die neben der reinen Auswertung zusätzlich Seiteneffekte wie Logging auslösen und damit ihre eigentlich einfache Aufgabe unnötig verkomplizieren. Auch verschachtelte Abhängigkeiten zwischen mehreren Flags, bei denen ein Flag nur unter der Bedingung eines anderen Flags wirken soll, führen in der Praxis schnell zu schwer nachvollziehbaren Kombinationen, die sich kaum noch vollständig überblicken lassen und deshalb möglichst vermieden werden sollten.
| Aspekt | Naive if-Konstante im Code | Eigenes Feature-Flag-System |
|---|---|---|
| Änderung ohne Deploy | Nicht möglich | Möglich über Konfiguration oder Admin-Oberfläche |
| Rollout-Prozentsatz | Nicht vorgesehen | Deterministisch pro Nutzer umsetzbar |
| Segment-Targeting | Manuell im Code verstreut | Zentral über ein Kontext-Objekt geregelt |
| Testbarkeit beider Zustände | Oft vergessen | Über injizierbaren Service explizit erzwingbar |
| Aufräumen nach Rollout | Bleibt häufig für immer im Code | Mit Owner und Ablaufdatum aktiv gesteuert |
Mironsoft
PHP-Modernisierung, Code-Qualität und Legacy-Refactoring
Gewachsener PHP-Code, der niemand mehr gern anfasst?
Wir modernisieren PHP-Codebasen auf aktuelle Sprachstandards, führen statische Analyse und Coding Standards ein und refactorn Legacy-Code Schritt für Schritt, ohne den laufenden Betrieb zu gefährden.
Legacy-Refactoring
Gewachsenen PHP-Code strukturiert und risikoarm modernisieren.
Code-Qualität etablieren
PHPStan, Coding Standards und CI-Checks nachhaltig im Team verankern.
Versions-Upgrade
PHP-Major-Version-Upgrades sicher planen und ohne Ausfallzeit umsetzen.
10. Zusammenfassung
Feature Flags in PHP: Das Wichtigste auf einen Blick
Kernidee
Ein kompaktes System aus Konfiguration, Entscheidungsfunktion und sauberer Integration ersetzt viele SaaS-Anwendungsfälle.
Rollout
Deterministisches Hashing von Nutzer-ID und Flag-Schlüssel sorgt für konsistente Ergebnisse pro Nutzer.
Targeting
Ein Kontext-Objekt bündelt Segmentmerkmale und macht Targeting-Regeln zentral änderbar.
Hygiene
Owner, Ablaufdatum und regelmäßige Reviews verhindern, dass alte Flags dauerhaft liegen bleiben.