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

# Praktische use-cases voor Laravel Pennant

> Praktische patronen voor Laravel Pennant die verder gaan dan de basis. We behandelen kennis die je niet uit de officiële documentatie haalt, zoals scoping per team, noodkillswitches, dark launches en beheercommando's.

Zie de [gidspagina](/nl/pennant) voor het basisgebruik. Op deze pagina behandelen we praktische patronen die niet in de officiële documentatie staan.

***

## Teamscope in een multi-tenant SaaS

Wil je flags beheren per team (tenant) in plaats van per individuele gebruiker, verander dan de standaardscope naar het team.

```php theme={null}
// AppServiceProvider
Feature::resolveScopeUsing(fn () => Auth::user()?->currentTeam);
```

Alleen hiermee richt `Feature::active('billing-v2')` zich automatisch op het huidige team. Welk teamlid het ook is, zolang die tot hetzelfde team behoort komt hetzelfde resultaat terug, waardoor de UI consistent blijft.

Een voorbeeld van een gefaseerde rollout op basis van de aanmelddatum van het team.

```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) {
    // Teams die zich vanaf 2024 hebben geregistreerd: direct actief
    if ($team->created_at->isAfter(new Carbon('2024-01-01'))) {
        return true;
    }

    // Registraties uit 2022–2023: rollout in stappen van 10%
    if ($team->created_at->isAfter(new Carbon('2022-01-01'))) {
        return Lottery::odds(1 / 10);
    }

    // Oudere teams voorzichtig vanaf 1%
    return Lottery::odds(1 / 100);
});
```

Wil je alleen specifieke teams activeren (bijvoorbeeld vroege toegang voor enterprise-klanten):

```php theme={null}
// Na de deploy alleen een specifiek team handmatig activeren
Feature::for($earlyAccessTeam)->activate('new-billing');
```

***

## Noodkillswitch (de `before`-methode benutten)

Wordt er in productie een bug ontdekt, dan kun je de functie direct uitschakelen zonder code terug te draaien. Voeg aan een klassegebaseerde feature een `before`-methode toe, en die check draait vóór de waarde uit de storage.

```php theme={null}
<?php

namespace App\Features;

use App\Models\User;
use Illuminate\Support\Facades\Config;

class NewCheckout
{
    /**
     * Noodschakelaar waarmee je bij een productiebug direct kunt uitschakelen via config
     */
    public function before(User $user): mixed
    {
        // Aansturen via de waarde in config/features.php of via env
        if (Config::boolean('features.new_checkout_disabled', false)) {
            return false; // Direct uitschakelen voor iedereen
        }

        // Beheerders altijd actief (voor debugging)
        if ($user->isAdmin()) {
            return true;
        }

        return null; // Retourneer null om door te gaan naar de normale resolve()
    }

    public function resolve(User $user): mixed
    {
        return $user->isPremium() || Lottery::odds(1 / 5);
    }
}
```

Alleen de omgevingsvariabele `FEATURES_NEW_CHECKOUT_DISABLED=true` instellen is genoeg om de functie te stoppen zonder de database aan te raken. Een killswitch zonder deploy.

<Tip>
  Retourneert `before` `null`, dan wordt `resolve()` uitgevoerd. Retourneert hij `false`, dan wordt de feature direct als inactief behandeld. Retourneer buiten noodgevallen dus `null`.
</Tip>

***

## Geplande rollouts

Het scenario waarin je op een specifieke datum en tijd automatisch aan alle gebruikers wilt uitrollen. Dit implementeer je met de `before`-methode.

```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; // Na de rolloutdatum voor iedereen beschikbaar
    }

    return null; // Zo niet, dan door naar de normale resolve()
}

public function resolve(User $user): mixed
{
    // Vóór de rollout alleen interne teamleden
    return $user->isInternalTeamMember();
}
```

```ini theme={null}
# .env
FEATURES_NEW_API_ROLLOUT_DATE=2025-04-01
```

Hiermee wordt de feature na `2025-04-01` automatisch voor alle gebruikers beschikbaar. Automatisering zonder deploy, zonder database en zonder Artisan-commando.

***

## Dark launch (shadow mode)

Het patroon waarbij je nieuwe logica op productiedata laat draaien zonder die aan gebruikers te tonen, en het resultaat vergelijkt met de oude logica. Zijn er geen problemen, dan is de release compleet door alleen de flag aan te zetten.

```php theme={null}
class RecommendationController
{
    public function index(Request $request)
    {
        $legacyResult = $this->getLegacyRecommendations($request->user());

        // In shadow mode draait het nieuwe algoritme ter vergelijking, maar wordt de oude logica getoond
        if (Feature::for($request->user())->active('recommendation-v2-shadow')) {
            try {
                $newResult = $this->getNewRecommendations($request->user());

                // Verschillen loggen (niet aan de gebruiker tonen)
                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()]);
            }
        }

        // De response is altijd het resultaat van de oude logica
        return response()->json($legacyResult);
    }
}
```

Controleer de logs, en zodra er geen verschillen meer zijn, hoef je alleen de flag om te zetten naar `recommendation-v2`.

***

## A/B-testresultaten verzamelen met events

De officiële documentatie noemt het `FeatureRetrieved`-event, maar toont geen echte patronen voor A/B-testaggregatie.

Het `FeatureResolved`-event wordt alleen afgevuurd wanneer de waarde van een feature voor het **eerst** wordt geresolved. Hiermee registreer je de varianttoewijzing van een gebruiker.

```php theme={null}
use Laravel\Pennant\Events\FeatureResolved;
use Illuminate\Support\Facades\Event;

// Registreren in de AppServiceProvider of EventServiceProvider
Event::listen(function (FeatureResolved $event) {
    if ($event->feature !== 'purchase-button') {
        return;
    }

    // Registreren op het moment dat de variant aan de gebruiker wordt toegewezen
    analytics()->identify($event->scope?->id, [
        'ab_purchase_button' => $event->value,
    ]);
});
```

Registreer je daarnaast apart wanneer een conversie plaatsvindt, dan kun je de conversieratio per variant aggregeren.

```php theme={null}
// Bij het afronden van een aankoop
Event::dispatch(new PurchaseCompleted($user, $product));
```

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

<Info>
  Het verschil tussen `FeatureResolved` en `FeatureRetrieved`: `FeatureResolved` vuurt alleen bij de eerste evaluatie, `FeatureRetrieved` vuurt bij elke check. Voor het vastleggen van toewijzingen is `FeatureResolved` geschikt, voor het volgen van paginaweergaven `FeatureRetrieved`.
</Info>

***

## Scope in queued jobs

In queue-jobs bestaat er geen geauthenticeerde gebruiker, waardoor featurechecks zich onverwacht kunnen gedragen. Geef de job expliciet een scope mee.

```php theme={null}
class SendWeeklyDigest implements ShouldQueue
{
    public function __construct(
        private readonly User $user
    ) {}

    public function handle(): void
    {
        // FOUT: er is geen geauthenticeerde gebruiker, dus dit is altijd false
        // if (Feature::active('new-digest-layout')) { ... }

        // GOED: geef de gebruiker expliciet mee
        if (Feature::for($this->user)->active('new-digest-layout')) {
            $this->sendNewLayout($this->user);
        } else {
            $this->sendLegacyLayout($this->user);
        }
    }
}
```

Je kunt de flag ook bij het dispatchen van de job evalueren en aan de job doorgeven.

```php theme={null}
// In de controller evalueren en aan de job doorgeven
$useNewLayout = Feature::for($user)->active('new-digest-layout');

SendWeeklyDigest::dispatch($user, $useNewLayout);
```

***

## Een beheer-UI met Artisan-commando's

Maak je een eenvoudig Artisan-commando om flags te beheren, dan kun je productieflags bedienen zonder deploy.

```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 = 'Featureflags beheren';

    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('Onbekende actie'),
        };
    }

    private function activate(string $feature): void
    {
        Feature::activateForEveryone($feature);
        $this->info("✓ {$feature} is geactiveerd voor alle gebruikers");
    }

    private function deactivate(string $feature): void
    {
        Feature::deactivateForEveryone($feature);
        $this->info("✓ {$feature} is gedeactiveerd voor alle gebruikers");
    }

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

        $this->line("{$feature}: actief in {$count} scopes");
    }
}
```

```shell theme={null}
php artisan feature activate new-checkout
php artisan feature deactivate new-checkout
php artisan feature status new-checkout
```

***

## Featurenamen veilig refactoren (het `Name`-attribute)

Wanneer je een klassegebaseerde feature hernoemt en de opgeslagen flagnaam in de database verandert, worden de flags van alle gebruikers gereset. Leg de opslagnaam vast met het `Name`-attribute.

```php theme={null}
use Laravel\Pennant\Attributes\Name;

// Oude klassenaam: CheckoutV2 → nieuwe klassenaam: NewCheckoutExperience
// Wordt in de database altijd opgeslagen als 'checkout-v2'
#[Name('checkout-v2')]
class NewCheckoutExperience
{
    public function resolve(User $user): mixed
    {
        return $user->isPremium();
    }
}
```

Zo blijven je databasegegevens intact, hoe je de klassenaam ook refactort.

***

## Conclusie

Heb je de basis uit de officiële documentatie onder de knie, dan komt Pennant pas echt tot zijn recht met de volgende patronen.

| Patroon                       | Wanneer te gebruiken                             |
| ----------------------------- | ------------------------------------------------ |
| Teamscope                     | Multi-tenant SaaS                                |
| Killswitch met `before`       | Noodrespons bij productiebugs                    |
| Geplande rollout              | Releaseautomatisering op datum en tijd           |
| Dark launch                   | Veilige productievalidatie van nieuwe algoritmes |
| Het `FeatureResolved`-event   | Nauwkeurige verzameling van A/B-testresultaten   |
| Expliciete scope in jobs      | Correcte flag-evaluatie in queue-omgevingen      |
| Beheer met Artisan-commando's | Productieflags bedienen zonder deploy            |
| Het `Name`-attribute          | Veilige klasse-refactoring                       |

<Card title="Laravel Pennant-gids" icon="flag" href="/nl/pennant">
  Zie de gidspagina voor de installatie en het basisgebruik.
</Card>


## Related topics

- [InteractsWithData-trait](/nl/advanced/interacts-with-data.md)
- [Praktische technieken voor Laravel Telescope](/nl/blog/telescope-introduction.md)
- [Introductie React — de basis voor Inertia × Laravel](/nl/blog/react-introduction.md)
- [Laravel Pennant](/nl/pennant.md)
- [Scopes in Eloquent](/nl/advanced/eloquent-scopes.md)
