Zum Hauptinhalt springen
Die Grundlagen finden Sie auf der Guide-Seite. Auf dieser Seite stellen wir praxisnahe Muster vor, die in der offiziellen Dokumentation nicht enthalten sind.

Team-Scope in Multi-Tenant-SaaS

Wenn Sie Flags nicht pro Nutzer, sondern pro Team (Tenant) verwalten möchten, ändern Sie den Default-Scope auf das Team.
// AppServiceProvider
Feature::resolveScopeUsing(fn () => Auth::user()?->currentTeam);
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 aktivieren
Feature::for($earlyAccessTeam)->activate('new-billing');

Notfall-Kill-Switch (Nutzung der Methode before)

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.
<?php

namespace 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 before null 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.

Geplanter Rollout

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();
}
# .env
FEATURES_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.

Dark Launch (Shadow Mode)

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.

Ergebnisse aus A/B-Tests per Event einsammeln

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 registrieren
Event::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 Kaufabschluss
Event::dispatch(new PurchaseCompleted($user, $product));
// Listener für PurchaseCompleted
analytics()->track('purchase_completed', [
    'user_id'              => $user->id,
    'ab_purchase_button'   => Feature::for($user)->value('purchase-button'),
]);
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.

Scope in gequeueten Jobs

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);

Management-UI per Artisan-Kommando

Ein simples Artisan-Kommando zur Flag-Verwaltung erlaubt das Steuern von Produktions-Flags ohne Deployment.
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Laravel\Pennant\Feature;

class ManageFeatureFlag extends Command
{
    protected $signature = 'feature {action : activate|deactivate|status} {feature : Feature name}';
    protected $description = 'Feature-Flags verwalten';

    public function handle(): void
    {
        $feature = $this->argument('feature');

        match ($this->argument('action')) {
            'activate'   => $this->activate($feature),
            'deactivate' => $this->deactivate($feature),
            'status'     => $this->status($feature),
            default      => $this->error('Unbekannte Aktion'),
        };
    }

    private function activate(string $feature): void
    {
        Feature::activateForEveryone($feature);
        $this->info("✓ {$feature} wurde für alle Nutzer aktiviert");
    }

    private function deactivate(string $feature): void
    {
        Feature::deactivateForEveryone($feature);
        $this->info("✓ {$feature} wurde für alle Nutzer deaktiviert");
    }

    private function status(string $feature): void
    {
        $count = \DB::table('features')
            ->where('name', $feature)
            ->where('value', json_encode(true))
            ->count();

        $this->line("{$feature}: in {$count} Scopes aktiv");
    }
}
php artisan feature activate new-checkout
php artisan feature deactivate new-checkout
php artisan feature status new-checkout

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.

Fazit

Wenn Sie die Grundlagen aus der offiziellen Dokumentation beherrschen, entfaltet Pennant mit folgenden Mustern sein volles Potenzial.
MusterEinsatzgebiet
Team-ScopeMulti-Tenant-SaaS
before-Kill-SwitchNotfallreaktion auf Produktions-Bugs
Geplanter RolloutZeitgesteuerte Release-Automation
Dark Launchsichere Validierung neuer Algorithmen in Produktion
Event FeatureResolvedpräzise Erfassung von A/B-Test-Ergebnissen
Expliziter Scope in Jobspräzise Flag-Auswertung im Queue-Kontext
Verwaltungs-Artisan-KommandosSteuerung von Produktions-Flags ohne Deployment
Attribut Namesicheres Refactoring von Klassen

Laravel-Pennant-Leitfaden

Installation und grundlegende Verwendung finden Sie auf der Guide-Seite.
Zuletzt geändert am 13. Juli 2026