Zum Hauptinhalt springen

Was ist Laravel Pennant

Laravel Pennant ist ein schlankes, einfaches Paket für Feature Flags. Mit Feature Flags können Sie neue Funktionen schrittweise ausrollen, A/B-Tests durchführen oder Trunk-Based Development unterstützen.

Was sind Feature Flags

Mit Feature Flags entkoppeln Sie Deployment und Release. Sie können den Code in Produktion deployen und das Feature konfigurativ ein- oder ausschalten.

Installation

1

Paket installieren

Installieren Sie Pennant mit Composer.
composer require laravel/pennant
2

Konfigurationsdatei und Migrationen veröffentlichen

Veröffentlichen Sie die Dateien mit dem Artisan-Befehl vendor:publish.
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
Dadurch entstehen config/pennant.php und Migrationen unter database/migrations.
3

Migrationen ausführen

Legt die Tabelle features an, in der Pennant die Werte der Feature Flags speichert.
php artisan migrate

Konfiguration

In config/pennant.php legen Sie den verwendeten Storage-Treiber fest. Pennant unterstützt zwei Treiber:
TreiberBeschreibung
databasePersistente Speicherung in einer relationalen Datenbank (Standard)
arrayIn-Memory-Speicherung (für Tests oder temporäre Nutzung)
// config/pennant.php
'default' => env('PENNANT_STORE', 'database'),

Features definieren

Definition per Closure

Features werden über die Methode define der Feature-Fassade definiert – üblicherweise in der boot-Methode eines Service-Providers. Die Closure erhält einen „Scope“ (meist den authentifizierten Benutzer).
<?php

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\Lottery;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Feature::define('new-api', fn (User $user) => match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        });
    }
}
Die Logik lautet:
  • Interne Teammitglieder erhalten das Feature immer.
  • Kunden mit hohem Traffic erhalten es nicht.
  • Für alle anderen wird es mit 1 % Wahrscheinlichkeit aktiviert.
Beim ersten Prüfen wird das Ergebnis der Closure im Storage-Treiber gespeichert. Danach wird der gespeicherte Wert verwendet.
Wenn die Definition nur eine Lottery zurückgibt, können Sie die Closure weglassen.
Feature::define('site-redesign', Lottery::odds(1, 1000));

Definition als Klasse

Pennant unterstützt auch klassenbasierte Feature-Definitionen. In diesem Fall ist keine Registrierung im Service-Provider erforderlich.
php artisan pennant:feature NewApi
Die generierte Klasse liegt in app/Features. Implementieren Sie die Methode resolve.
<?php

namespace App\Features;

use App\Models\User;
use Illuminate\Support\Lottery;

class NewApi
{
    /**
     * Löst den initialen Wert des Features auf.
     */
    public function resolve(User $user): mixed
    {
        return match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        };
    }
}

Speichernamen anpassen

Standardmäßig wird der voll qualifizierte Klassenname gespeichert. Mit dem Attribut Name können Sie den Namen anpassen.
use Laravel\Pennant\Attributes\Name;

#[Name('new-api')]
class NewApi
{
    // ...
}

Feature-Prüfung abfangen (Methode before)

In klassenbasierten Features können Sie eine before-Methode definieren. Sie wird vor dem Zugriff auf den Storage im Speicher ausgeführt; gibt sie einen anderen Wert als null zurück, wird dieser verwendet.
class NewApi
{
    public function before(User $user): mixed
    {
        if (Config::get('features.new-api.disabled')) {
            return $user->isInternalTeamMember();
        }
    }

    public function resolve(User $user): mixed
    {
        // ...
    }
}
Die before-Methode ist hilfreich, um bei Fehlern das Feature notfallmäßig zu deaktivieren oder einen Rollout auf einen bestimmten Zeitpunkt zu terminieren.

Features prüfen

Feature::active() / Feature::inactive()

Mit active prüfen Sie, ob ein Feature aktiv ist. Standardmäßig wird der aktuell authentifizierte Benutzer verwendet.
use Laravel\Pennant\Feature;

if (Feature::active('new-api')) {
    // Die neue API verwenden
}
Für klassenbasierte Features übergeben Sie den Klassennamen.
use App\Features\NewApi;

if (Feature::active(NewApi::class)) {
    // ...
}
Weitere praktische Methoden:
// Sind alle Features aktiv?
Feature::allAreActive(['new-api', 'site-redesign']);

// Ist eines der Features aktiv?
Feature::someAreActive(['new-api', 'site-redesign']);

// Ist das Feature inaktiv?
Feature::inactive('new-api');

// Sind alle Features inaktiv?
Feature::allAreInactive(['new-api', 'site-redesign']);

// Ist eines der Features inaktiv?
Feature::someAreInactive(['new-api', 'site-redesign']);

Bedingte Ausführung (when / unless)

Mit when führen Sie eine Closure nur aus, wenn das Feature aktiv ist.
return Feature::when(NewApi::class,
    fn () => $this->resolveNewApiResponse($request),
    fn () => $this->resolveLegacyApiResponse($request),
);
unless ist die Umkehrung von when; die erste Closure läuft, wenn das Feature inaktiv ist.
return Feature::unless(NewApi::class,
    fn () => $this->resolveLegacyApiResponse($request),
    fn () => $this->resolveNewApiResponse($request),
);

Trait HasFeatures

Fügen Sie dem User-Modell das Trait HasFeatures hinzu, können Sie Features direkt am Modell prüfen.
use Laravel\Pennant\Concerns\HasFeatures;

class User extends Authenticatable
{
    use HasFeatures;
}
if ($user->features()->active('new-api')) {
    // ...
}

// Wert abrufen
$value = $user->features()->value('purchase-button');

// Bedingte Ausführung
$user->features()->when('new-api',
    fn () => /* ... */,
    fn () => /* ... */,
);

Blade-Direktiven

In Blade können Sie die Direktive @feature verwenden.
@feature('site-redesign')
    {{-- Wenn 'site-redesign' aktiv ist --}}
@else
    {{-- Wenn 'site-redesign' inaktiv ist --}}
@endfeature

@featureany(['site-redesign', 'beta'])
    {{-- Wenn eines der beiden aktiv ist --}}
@endfeatureany

Middleware

Mit der Middleware EnsureFeaturesAreActive geben Sie an, dass eine Route ein bestimmtes Feature benötigt. Ist das Feature inaktiv, wird 400 Bad Request zurückgegeben.
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;

Route::get('/api/servers', function () {
    // ...
})->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));
Über whenInactive passen Sie die Antwort an.
EnsureFeaturesAreActive::whenInactive(
    function (Request $request, array $features) {
        return new Response(status: 403);
    }
);

In-Memory-Cache

Innerhalb eines Requests cached Pennant die Ergebnisse im Speicher. Mehrfache Prüfungen desselben Feature Flags erzeugen keine zusätzlichen DB-Queries. Zum manuellen Leeren des Caches verwenden Sie flushCache.
Feature::flushCache();

Scopes

Scope festlegen

Standardmäßig ist der authentifizierte Benutzer der Scope. Mit for können Sie einen beliebigen Scope angeben.
// Für einen bestimmten Benutzer prüfen
Feature::for($user)->active('new-api');

// Für ein Team prüfen
Feature::for($user->team)->active('billing-v2');
Ein Beispiel, das Features je Team steuert:
Feature::define('billing-v2', function (Team $team) {
    if ($team->created_at->isAfter(new Carbon('1st Jan, 2023'))) {
        return true;
    }

    if ($team->created_at->isAfter(new Carbon('1st Jan, 2019'))) {
        return Lottery::odds(1 / 100);
    }

    return Lottery::odds(1 / 1000);
});

Standard-Scope anpassen

Mit Feature::resolveScopeUsing passen Sie den Standard-Scope an.
Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);
Danach wird ohne for der Standard-Scope verwendet.
Feature::active('billing-v2');
// entspricht:
Feature::for($user->team)->active('billing-v2');

Nullable Scope

Ist der Scope null (z. B. bei nicht authentifizierten Routen oder Artisan-Befehlen) und die Feature-Definition kann kein null verarbeiten, wird automatisch false zurückgegeben. Wenn Sie null behandeln möchten, deklarieren Sie den Parameter als nullable.
Feature::define('new-api', fn (User|null $user) => match (true) {
    $user === null => true,
    $user->isInternalTeamMember() => true,
    $user->isHighTrafficCustomer() => false,
    default => Lottery::odds(1 / 100),
});

Rich Feature Values

Features können auch andere Werte als Booleans zurückgeben – etwa um in einem A/B-Test die Buttonfarbe zu steuern.
Feature::define('purchase-button', fn (User $user) => Arr::random([
    'blue-sapphire',
    'seafoam-green',
    'tart-orange',
]));
Den Wert erhalten Sie mit value.
$color = Feature::value('purchase-button');
In Blade sind auch werteabhängige Verzweigungen möglich.
@feature('purchase-button', 'blue-sapphire')
    {{-- blue-sapphire aktiv --}}
@elsefeature('purchase-button', 'seafoam-green')
    {{-- seafoam-green aktiv --}}
@elsefeature('purchase-button', 'tart-orange')
    {{-- tart-orange aktiv --}}
@endfeature
Bei Rich Values gilt jeder Wert außer false als aktiv.
Übergibt when einen Rich Value, erhält die erste Closure diesen Wert.
Feature::when('purchase-button',
    fn ($color) => /* $color enthält den Wert */,
    fn () => /* wenn inaktiv */,
);

Mehrere Features gleichzeitig abrufen

Mit values holen Sie mehrere Feature-Werte auf einmal.
Feature::values(['billing-v2', 'purchase-button']);

// [
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
// ]
Mit all erhalten Sie alle definierten Feature-Werte.
Feature::all();
Um klassenbasierte Features in all einzubeziehen, rufen Sie im Service-Provider discover auf.
Feature::discover();
Damit werden alle Feature-Klassen im Verzeichnis app/Features registriert.

Eager Loading

Prüfungen in Schleifen können zu Performance-Problemen führen. Mit load laden Sie die Werte vorab.
// Schlecht: pro Schleifendurchlauf eine DB-Query
foreach ($users as $user) {
    if (Feature::for($user)->active('notifications-beta')) {
        $user->notify(new RegistrationSuccess);
    }
}

// Gut: vorab in einem Rutsch laden
Feature::for($users)->load(['notifications-beta']);

foreach ($users as $user) {
    if (Feature::for($user)->active('notifications-beta')) {
        $user->notify(new RegistrationSuccess);
    }
}
Um nur noch nicht geladene Werte zu laden, verwenden Sie loadMissing.
Feature::for($users)->loadMissing([
    'new-api',
    'purchase-button',
    'notifications-beta',
]);

Werte aktualisieren

Manuell

Mit activate und deactivate schalten Sie Features ein bzw. aus.
// Im Standard-Scope aktivieren
Feature::activate('new-api');

// In einem bestimmten Scope deaktivieren
Feature::for($user->team)->deactivate('billing-v2');

// Rich Value setzen
Feature::activate('purchase-button', 'seafoam-green');
Um den gespeicherten Wert zu verwerfen, verwenden Sie forget. Bei der nächsten Prüfung wird die Definition neu ausgewertet.
Feature::forget('purchase-button');

Sammel-Update

Mit activateForEveryone und deactivateForEveryone wenden Sie eine Änderung auf alle Scopes im Storage an.
Feature::activateForEveryone('new-api');
Feature::activateForEveryone('purchase-button', 'seafoam-green');
Feature::deactivateForEveryone('new-api');

Features purgen

Wenn ein Feature aus der Anwendung entfernt oder seine Definition geändert wurde, können Sie die Werte aus dem Storage entfernen (purgen).
// Einzelnes Feature purgen
Feature::purge('new-api');

// Mehrere Features purgen
Feature::purge(['new-api', 'purchase-button']);

// Alle Features purgen
Feature::purge();
Es geht auch per Artisan-Befehl – ideal für die Deploy-Pipeline.
php artisan pennant:purge new-api

# Mehrere angeben
php artisan pennant:purge new-api purchase-button

# Alle außer den angegebenen Features purgen
php artisan pennant:purge --except=new-api --except=purchase-button

# Alle purgen, die nicht im Service-Provider registriert sind
php artisan pennant:purge --except-registered

Tests

Features neu definieren

In Tests steuern Sie die Rückgabewerte, indem Sie das Feature per Feature::define neu definieren.
tab=Pest
use Laravel\Pennant\Feature;

test('it can control feature values', function () {
    Feature::define('purchase-button', 'seafoam-green');

    expect(Feature::value('purchase-button'))->toBe('seafoam-green');
});
tab=PHPUnit
use Laravel\Pennant\Feature;

public function test_it_can_control_feature_values(): void
{
    Feature::define('purchase-button', 'seafoam-green');

    $this->assertSame('seafoam-green', Feature::value('purchase-button'));
}
Klassenbasierte Features funktionieren genauso.
tab=Pest
test('it can control feature values', function () {
    Feature::define(NewApi::class, true);

    expect(Feature::value(NewApi::class))->toBeTrue();
});
tab=PHPUnit
use App\Features\NewApi;

public function test_it_can_control_feature_values(): void
{
    Feature::define(NewApi::class, true);

    $this->assertTrue(Feature::value(NewApi::class));
}

Test-Store konfigurieren

Über eine Umgebungsvariable in phpunit.xml legen Sie den während Tests verwendeten Store fest.
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
    <php>
        <env name="PENNANT_STORE" value="array"/>
    </php>
</phpunit>

Eigene Treiber

Wenn die vorhandenen Treiber nicht ausreichen, können Sie einen eigenen Treiber erstellen und dabei das Interface Laravel\Pennant\Contracts\Driver implementieren.
<?php

namespace App\Extensions;

use Laravel\Pennant\Contracts\Driver;

class RedisFeatureDriver implements Driver
{
    public function define(string $feature, callable $resolver): void {}
    public function defined(): array {}
    public function getAll(array $features): array {}
    public function get(string $feature, mixed $scope): mixed {}
    public function set(string $feature, mixed $scope, mixed $value): void {}
    public function setForAllScopes(string $feature, mixed $value): void {}
    public function delete(string $feature, mixed $scope): void {}
    public function purge(array|null $features): void {}
}
Registrieren Sie den Treiber in der boot-Methode eines Service-Providers per extend.
Feature::extend('redis', function (Application $app) {
    return new RedisFeatureDriver($app->make('redis'), $app->make('events'), []);
});
Anschließend können Sie den Treiber in config/pennant.php konfigurieren.
'stores' => [
    'redis' => [
        'driver' => 'redis',
        'connection' => null,
    ],
],

Zusammenfassung

ZielVorgehen
Feature Flags installierencomposer require laravel/pennant
Feature definierenFeature::define('name', fn ($user) => ...)
Feature prüfenFeature::active('name')
In Blade prüfen@feature('name') ... @endfeature
Wert aktualisierenFeature::activate('name') / deactivate
Auf alle Scopes anwendenFeature::activateForEveryone('name')
In Tests steuernMit Feature::define('name', true) neu definieren
Aus dem Storage entfernenFeature::purge('name')

Nächste Schritte

Debugging und Error Handling

Lernen Sie, wie Ihre Anwendung Ausnahmen behandelt und meldet.

Laravel Pulse

Führen Sie ein Performance-Monitoring-Dashboard für Ihre Anwendung ein.
Zuletzt geändert am 13. Juli 2026