> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Laravel Pennant – Praxis-Anwendungsfälle

> 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](/de/pennant). 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.

```php theme={null}
// 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:

```php theme={null}
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):

```php theme={null}
// 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 theme={null}
<?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.

<Tip>
  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.
</Tip>

***

## 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`.

```php theme={null}
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();
}
```

```ini theme={null}
# .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.

```php theme={null}
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.

```php theme={null}
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.

```php theme={null}
// Bei Kaufabschluss
Event::dispatch(new PurchaseCompleted($user, $product));
```

```php theme={null}
// Listener für PurchaseCompleted
analytics()->track('purchase_completed', [
    'user_id'              => $user->id,
    'ab_purchase_button'   => Feature::for($user)->value('purchase-button'),
]);
```

<Info>
  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`.
</Info>

***

## 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.

```php theme={null}
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.

```php theme={null}
// 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 theme={null}
<?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");
    }
}
```

```shell theme={null}
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`.

```php theme={null}
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.

| Muster                        | Einsatzgebiet                                       |
| ----------------------------- | --------------------------------------------------- |
| Team-Scope                    | Multi-Tenant-SaaS                                   |
| `before`-Kill-Switch          | Notfallreaktion auf Produktions-Bugs                |
| Geplanter Rollout             | Zeitgesteuerte Release-Automation                   |
| Dark Launch                   | sichere Validierung neuer Algorithmen in Produktion |
| Event `FeatureResolved`       | präzise Erfassung von A/B-Test-Ergebnissen          |
| Expliziter Scope in Jobs      | präzise Flag-Auswertung im Queue-Kontext            |
| Verwaltungs-Artisan-Kommandos | Steuerung von Produktions-Flags ohne Deployment     |
| Attribut `Name`               | sicheres Refactoring von Klassen                    |

<Card title="Laravel-Pennant-Leitfaden" icon="flag" href="/de/pennant">
  Installation und grundlegende Verwendung finden Sie auf der Guide-Seite.
</Card>


## Related topics

- [Laravel Agent Detector – Paket zur Erkennung von KI-Agenten](/de/blog/agent-detector-introduction.md)
- [Laravel Pennant](/de/pennant.md)
- [OAuth-2.0-Authentifizierung - Google Sheets API for Laravel](/de/packages/laravel-google-sheets/oauth.md)
- [Service-Account-Authentifizierung - Google Sheets API für Laravel](/de/packages/laravel-google-sheets/service-account.md)
- [Google Sheets API for Laravel](/de/packages/laravel-google-sheets/index.md)
