Feature Flags in PHP selbst implementieren
AI generated
8.4
PHP, Resilienz, Architektur-Patterns
Feature Flags in PHP
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.

15 Min. Lesezeit Rollout-Prozentsatz, Targeting Testbarkeit, Flag-Debt

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.

11. FAQ: Feature Flags in PHP: Das Wichtigste auf einen Blick

1Brauche ich für Feature Flags immer einen externen Dienst?
Nein. Für die üblichen Anforderungen wie Ein-Aus-Schalter, prozentualen Rollout und einfaches Segment-Targeting reicht ein kompaktes, selbst gebautes System in PHP meist vollkommen aus.
2Wie stelle ich sicher, dass ein Nutzer bei jedem Aufruf im selben Rollout-Zustand landet?
Durch deterministisches Hashing einer stabilen Kennung wie der Nutzer-ID zusammen mit dem Flag-Schlüssel in einen festen Wertebereich, statt bei jeder Auswertung neu zu würfeln.
3Was ist ein Kontext-Objekt beim Segment-Targeting?
Ein Value Object, das die für Targeting-Entscheidungen relevanten Merkmale eines Nutzers bündelt, etwa Kundengruppe, Land oder Beta-Tester-Status, und der Auswertungsfunktion als Eingabe übergeben wird.
4Wie teste ich Code, der hinter einem Feature Flag liegt?
Indem der Feature-Flag-Service als austauschbare Abhängigkeit injiziert wird, sodass Tests ihn durch eine feste Implementierung ersetzen und beide Zweige, aktiviert und deaktiviert, unabhängig voneinander prüfen können.
5Was ist Flag-Debt?
Die Ansammlung alter Feature Flags, die nach abgeschlossenem Rollout oder endgültiger Entscheidung nie aus dem Code entfernt wurden, wodurch tote Zweige und unnötige Komplexität im Code verbleiben.
6Wie verhindere ich Flag-Debt?
Durch einen festen Owner und ein erwartetes Entfernungsdatum pro Flag ab dem Zeitpunkt der Erstellung, kombiniert mit einem regelmäßigen Review, das alte Flags bei 0 oder 100 Prozent Rollout aktiv zur Entfernung vorschlägt.
7Wo sollten Flag-Definitionen gespeichert werden?
Bei häufigen Änderungen durch Nicht-Entwickler eignet sich eine Datenbanktabelle mit Admin-Oberfläche, bei seltenen, code-review-pflichtigen Änderungen reicht ein einfaches PHP-Konfigurationsarray.
8Warum sollte ich Flag-Definitionen cachen?
Weil ein Datenbank-Lookup pro Flag-Prüfung und Request bei mehreren Flags und hoher Last spürbar Last erzeugt. Ein kurzes Caching der Flag-Definitionen in einem schnellen Speicher wie APCu oder Redis reduziert diese Last deutlich.
9Welche Reihenfolge sollte die Auswertung eines Flags haben?
Üblich ist zunächst eine Prüfung auf explizite Segment-Regeln, danach der prozentuale Rollout anhand der deterministischen Nutzer-Zuordnung, und erst zuletzt der allgemeine Ein-Aus-Standardwert des Flags.
10Ist ein Feature-Flag-System nur für Frontend-Funktionen sinnvoll?
Nein, dasselbe Muster eignet sich ebenso für Backend-Verhalten, etwa das schrittweise Aktivieren eines neuen Zahlungsanbieters, einer neuen Preisberechnung oder eines geänderten Datenbank-Zugriffspfads.