Wir erläutern praxisorientierte Muster jenseits der Pennant-Grundlagen. Teamscope, Notfall-Kill-Switch, Dark Launch, Verwaltungskommandos u. a. – Know-how, das die offizielle Dokumentation allein nicht vermittelt.
Die Grundlagen finden Sie auf der Guide-Seite. Auf dieser Seite stellen wir praxisnahe Muster vor, die in der offiziellen Dokumentation nicht enthalten sind.
Damit richtet sich Feature::active('billing-v2') automatisch auf das aktuelle Team. Solange die Mitglieder demselben Team angehören, erhalten sie unabhängig davon, wer aktiv ist, dasselbe Ergebnis – die UI-Konsistenz bleibt erhalten.Beispiel für einen stufenweisen Rollout anhand des Anmeldedatums des Teams:
use App\Models\Team;use Illuminate\Support\Carbon;use Illuminate\Support\Lottery;use Laravel\Pennant\Feature;Feature::define('new-billing', function (Team $team) { // Ab 2024 angemeldete Teams sind sofort aktiv if ($team->created_at->isAfter(new Carbon('2024-01-01'))) { return true; } // 2022–2023 angemeldete Teams in 10-%-Schritten ausrollen if ($team->created_at->isAfter(new Carbon('2022-01-01'))) { return Lottery::odds(1 / 10); } // Ältere Teams vorsichtig mit 1 % starten return Lottery::odds(1 / 100);});
Wenn Sie nur bestimmte Teams aktivieren möchten (z. B. Vorabzugang für Enterprise-Kunden):
// Nach dem Deployment gezielt für bestimmte Teams manuell aktivierenFeature::for($earlyAccessTeam)->activate('new-billing');
Wenn in der Produktion ein Bug entdeckt wird, lässt sich das Feature ohne Code-Rollback sofort deaktivieren. Ergänzen Sie eine before-Methode an einem klassenbasierten Feature; diese Prüfung läuft vor dem Speicherwert.
<?phpnamespace App\Features;use App\Models\User;use Illuminate\Support\Facades\Config;class NewCheckout{ /** * Notfallschalter, der bei einem Produktions-Bug per Config sofort deaktiviert */ public function before(User $user): mixed { // Steuerung per config/features.php oder env if (Config::boolean('features.new_checkout_disabled', false)) { return false; // sofort für alle deaktivieren } // Administratoren stets aktiv (zum Debuggen) if ($user->isAdmin()) { return true; } return null; // Rückgabe null: normales resolve() wird durchlaufen } public function resolve(User $user): mixed { return $user->isPremium() || Lottery::odds(1 / 5); }}
Ein Setzen der Umgebungsvariable FEATURES_NEW_CHECKOUT_DISABLED=true stoppt das Feature, ohne die DB anzufassen. Ein Kill-Switch ohne Deployment.
Gibt beforenull zurück, wird resolve() ausgeführt. Wird false zurückgegeben, wird das Feature sofort als inaktiv behandelt. Außerhalb von Notfällen sollten Sie null zurückgeben.
Ein Fall, in dem ein Feature zu einem festgelegten Zeitpunkt automatisch für alle veröffentlicht werden soll. Umsetzbar über die Methode before.
public function before(User $user): mixed{ $rolloutDate = Config::get('features.new_api.rollout_date'); if ($rolloutDate && Carbon::parse($rolloutDate)->isPast()) { return true; // Nach dem Rollout-Datum für alle freigeben } return null; // Andernfalls in das normale resolve() gehen}public function resolve(User $user): mixed{ // Vor dem Rollout nur für interne Team-Mitglieder return $user->isInternalTeamMember();}
# .envFEATURES_NEW_API_ROLLOUT_DATE=2025-04-01
Damit wird das Feature nach dem 2025-04-01 automatisch für alle Nutzer freigegeben. Kein Deployment, keine DB, kein Artisan-Kommando nötig.
Ein Muster, bei dem eine neue Logik im produktiven Datenbestand läuft, ohne den Nutzern angezeigt zu werden, und mit der alten Logik verglichen wird. Gibt es keine Probleme, reicht später das Aktivieren der Flag für den Release.
class RecommendationController{ public function index(Request $request) { $legacyResult = $this->getLegacyRecommendations($request->user()); // Im Shadow-Mode wird der neue Algorithmus ausgeführt und verglichen, angezeigt wird aber die alte Logik if (Feature::for($request->user())->active('recommendation-v2-shadow')) { try { $newResult = $this->getNewRecommendations($request->user()); // Differenzen loggen (nicht an den Nutzer zeigen) if ($legacyResult !== $newResult) { Log::channel('shadow_mode')->info('recommendation diff', [ 'user_id' => $request->user()->id, 'legacy' => $legacyResult, 'new' => $newResult, ]); } } catch (\Throwable $e) { Log::error('shadow mode error', ['error' => $e->getMessage()]); } } // Die Response liefert immer das Ergebnis der alten Logik return response()->json($legacyResult); }}
Prüfen Sie die Logs; sobald keine Differenzen mehr auftreten, genügt es, die Flag auf recommendation-v2 umzustellen.
In der offiziellen Dokumentation wird das Event FeatureRetrieved erwähnt, ein konkretes Muster zur Auswertung von A/B-Tests ist jedoch nicht ausgeführt.Das Event FeatureResolved wird nur beim erstmaligen Auflösen des Wertes ausgelöst. Nutzen Sie das, um die Variantenzuweisung des Nutzers festzuhalten.
use Laravel\Pennant\Events\FeatureResolved;use Illuminate\Support\Facades\Event;// Im AppServiceProvider oder EventServiceProvider registrierenEvent::listen(function (FeatureResolved $event) { if ($event->feature !== 'purchase-button') { return; } // Beim Zeitpunkt der Variantenzuweisung protokollieren analytics()->identify($event->scope?->id, [ 'ab_purchase_button' => $event->value, ]);});
Wenn Sie zusätzlich die Conversions gesondert festhalten, können Sie die Conversion-Rate pro Variante auswerten.
// Bei KaufabschlussEvent::dispatch(new PurchaseCompleted($user, $product));
Unterschied zwischen FeatureResolved und FeatureRetrieved: FeatureResolved wird nur beim ersten Auswerten ausgelöst, FeatureRetrieved bei jeder Prüfung. Für Zuweisungs-Records eignet sich FeatureResolved, für Page-View-Tracking FeatureRetrieved.
In Queue-Jobs existiert kein authentifizierter Nutzer, sodass Feature-Checks unerwartetes Verhalten zeigen können. Übergeben Sie den Scope daher explizit an den Job.
class SendWeeklyDigest implements ShouldQueue{ public function __construct( private readonly User $user ) {} public function handle(): void { // NG: Da kein authentifizierter Nutzer existiert, ist das Ergebnis immer false // if (Feature::active('new-digest-layout')) { ... } // OK: Nutzer explizit übergeben if (Feature::for($this->user)->active('new-digest-layout')) { $this->sendNewLayout($this->user); } else { $this->sendLegacyLayout($this->user); } }}
Alternativ können Sie die Flag beim Dispatch des Jobs evaluieren und dem Job mitgeben.
// Im Controller auswerten und an den Job übergeben$useNewLayout = Feature::for($user)->active('new-digest-layout');SendWeeklyDigest::dispatch($user, $useNewLayout);
Sicheres Umbenennen von Feature-Namen (Attribut Name)
Wenn Sie ein klassenbasiertes Feature umbenennen und sich damit der in der DB gespeicherte Flag-Name ändert, werden die Flags aller Nutzer zurückgesetzt. Fixieren Sie den Speichernamen mit dem Attribut Name.
use Laravel\Pennant\Attributes\Name;// Alter Klassenname: CheckoutV2 → Neuer Klassenname: NewCheckoutExperience// In der DB stets als 'checkout-v2' abgelegt#[Name('checkout-v2')]class NewCheckoutExperience{ public function resolve(User $user): mixed { return $user->isPremium(); }}
Damit können Sie den Klassennamen beliebig refactoren, ohne dass sich die DB-Daten ändern.