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

# Cas d'usage pratiques de Laravel Pennant

> Patrons pratiques allant au-delà des bases de Laravel Pennant : scope par équipe, kill switch d'urgence, dark launch, commandes de management, etc. — savoir-faire absent de la documentation officielle.

Pour les bases, reportez-vous à la [page de guide](/fr/pennant). Cette page présente des patrons pratiques que la documentation officielle ne couvre pas.

***

## Scope par équipe dans un SaaS multi-locataire

Si vous souhaitez gérer les flags à l'échelle de l'équipe (locataire) plutôt qu'à l'échelle de l'utilisateur, changez le scope par défaut vers l'équipe.

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

Cela suffit pour que `Feature::active('billing-v2')` cible automatiquement l'équipe courante. Comme tous les membres d'une même équipe obtiennent le même résultat, la cohérence de l'UI est préservée.

Exemple de déploiement progressif selon la date d'inscription de l'équipe.

```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) {
    // Les équipes inscrites depuis 2024 sont immédiatement actives
    if ($team->created_at->isAfter(new Carbon('2024-01-01'))) {
        return true;
    }

    // Inscriptions 2022-2023 : rollout progressif à 10 %
    if ($team->created_at->isAfter(new Carbon('2022-01-01'))) {
        return Lottery::odds(1 / 10);
    }

    // Anciennes équipes : rollout prudent à 1 %
    return Lottery::odds(1 / 100);
});
```

Pour n'activer que certaines équipes (mise à disposition anticipée aux comptes entreprise, par exemple) :

```php theme={null}
// Activer manuellement pour une équipe donnée après déploiement
Feature::for($earlyAccessTeam)->activate('new-billing');
```

***

## Kill switch d'urgence (méthode `before`)

Lorsqu'un bug se manifeste en production, vous pouvez désactiver immédiatement une fonctionnalité sans rollback du code. En ajoutant la méthode `before` à une feature basée sur classe, la vérification est effectuée avant les valeurs stockées.

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

namespace App\Features;

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

class NewCheckout
{
    /**
     * Kill switch d'urgence, activable en config lors d'un bug en production
     */
    public function before(User $user): mixed
    {
        // Contrôlé par la valeur dans config/features.php ou une variable d'env
        if (Config::boolean('features.new_checkout_disabled', false)) {
            return false; // Désactivation immédiate pour tous
        }

        // Toujours actif pour les administrateurs (débogage)
        if ($user->isAdmin()) {
            return true;
        }

        return null; // Renvoyer null poursuit vers resolve()
    }

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

Il suffit de définir `FEATURES_NEW_CHECKOUT_DISABLED=true` en variable d'environnement pour arrêter la fonctionnalité sans toucher à la base. C'est un kill switch qui ne demande aucun déploiement.

<Tip>
  Si `before` renvoie `null`, `resolve()` est exécuté. Si `before` renvoie `false`, la fonctionnalité est considérée immédiatement inactive. En dehors des urgences, renvoyez `null`.
</Tip>

***

## Rollout planifié

Vous voulez ouvrir automatiquement une fonctionnalité à tous les utilisateurs à une date donnée. Utilisez la méthode `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; // Une fois la date passée, ouvert à tous
    }

    return null; // Sinon, on continue vers resolve()
}

public function resolve(User $user): mixed
{
    // Avant le rollout : uniquement les membres internes
    return $user->isInternalTeamMember();
}
```

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

Passé le `2025-04-01`, la fonctionnalité s'ouvre automatiquement à tous. Pas de déploiement, pas de base, pas de commande Artisan.

***

## Dark launch (mode shadow)

Un patron où la nouvelle logique tourne sur les données de production sans être visible pour l'utilisateur, avec comparaison des résultats à l'ancienne. Si tout va bien, il suffit d'activer le flag pour finaliser la mise en production.

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

        // En mode shadow, on exécute le nouvel algo pour comparer, mais on renvoie l'ancien
        if (Feature::for($request->user())->active('recommendation-v2-shadow')) {
            try {
                $newResult = $this->getNewRecommendations($request->user());

                // On journalise la différence (invisible pour l'utilisateur)
                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()]);
            }
        }

        // La réponse renvoyée reste toujours celle de l'ancienne logique
        return response()->json($legacyResult);
    }
}
```

Quand les différences disparaissent des logs, il suffit de basculer le flag vers `recommendation-v2`.

***

## Collecte des résultats A/B via événements

La documentation officielle mentionne l'événement `FeatureRetrieved` mais ne montre pas de patron concret d'agrégation A/B.

L'événement `FeatureResolved` est déclenché **uniquement à la première** résolution d'une feature. Utilisez-le pour enregistrer l'attribution du variant à un utilisateur.

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

// À enregistrer dans AppServiceProvider ou EventServiceProvider
Event::listen(function (FeatureResolved $event) {
    if ($event->feature !== 'purchase-button') {
        return;
    }

    // Enregistrement au moment de l'attribution du variant
    analytics()->identify($event->scope?->id, [
        'ab_purchase_button' => $event->value,
    ]);
});
```

Enregistrez la conversion séparément pour calculer le taux de conversion par variant.

```php theme={null}
// À la fin d'un achat
Event::dispatch(new PurchaseCompleted($user, $product));
```

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

<Info>
  Différence entre `FeatureResolved` et `FeatureRetrieved` : `FeatureResolved` se déclenche uniquement à la première évaluation ; `FeatureRetrieved` à chaque vérification. Utilisez `FeatureResolved` pour la trace d'attribution, `FeatureRetrieved` pour le suivi des pages vues.
</Info>

***

## Scope dans les jobs en file d'attente

Dans un job de queue, il n'y a pas d'utilisateur authentifié : les vérifications de feature peuvent produire des comportements inattendus. Passez explicitement un scope au job.

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

    public function handle(): void
    {
        // À éviter : sans utilisateur authentifié, ce sera toujours false
        // if (Feature::active('new-digest-layout')) { ... }

        // Correct : on passe explicitement l'utilisateur
        if (Feature::for($this->user)->active('new-digest-layout')) {
            $this->sendNewLayout($this->user);
        } else {
            $this->sendLegacyLayout($this->user);
        }
    }
}
```

Autre approche : évaluer le flag au dispatch et le passer au job.

```php theme={null}
// Évaluation en contrôleur puis passage au job
$useNewLayout = Feature::for($user)->active('new-digest-layout');

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

***

## Interface de gestion via commande Artisan

Une simple commande Artisan permet de manipuler les flags en production sans déploiement.

```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 = 'Gérer un feature flag';

    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('Action inconnue'),
        };
    }

    private function activate(string $feature): void
    {
        Feature::activateForEveryone($feature);
        $this->info("✓ {$feature} activé pour tous les utilisateurs");
    }

    private function deactivate(string $feature): void
    {
        Feature::deactivateForEveryone($feature);
        $this->info("✓ {$feature} désactivé pour tous les utilisateurs");
    }

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

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

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

***

## Refactor sûr d'un nom de feature (attribut `Name`)

Lorsque vous renommez une feature basée sur classe, le nom stocké en base change et remet à zéro les flags de tous les utilisateurs. Fixez le nom stocké avec l'attribut `Name`.

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

// Ancienne classe CheckoutV2 → nouvelle classe NewCheckoutExperience
// La base stocke toujours 'checkout-v2'
#[Name('checkout-v2')]
class NewCheckoutExperience
{
    public function resolve(User $user): mixed
    {
        return $user->isPremium();
    }
}
```

Vous pouvez ainsi refactorer librement le nom de la classe tout en préservant les données en base.

***

## Conclusion

Une fois les bases de la documentation officielle maîtrisées, ces patrons révèlent tout le potentiel de Pennant.

| Patron                         | Quand l'utiliser                               |
| ------------------------------ | ---------------------------------------------- |
| Scope équipe                   | SaaS multi-locataire                           |
| Kill switch `before`           | Urgence bug en production                      |
| Rollout planifié               | Automatisation de release à date donnée        |
| Dark launch                    | Validation en prod sûre d'un nouvel algorithme |
| Événement `FeatureResolved`    | Trace précise pour A/B testing                 |
| Scope explicite dans les jobs  | Évaluation fiable dans les queues              |
| Commande Artisan de management | Manipuler les flags de prod sans déploiement   |
| Attribut `Name`                | Refactor sûr des classes                       |

<Card title="Guide Laravel Pennant" icon="flag" href="/fr/pennant">
  Consultez la page de guide pour l'installation et les bases.
</Card>


## Related topics

- [Laravel Pennant](/fr/pennant.md)
- [Scopes Eloquent](/fr/advanced/eloquent-scopes.md)
- [Seeding de base de données](/fr/seeding.md)
- [Laravel Cloud Hibernation (mise en veille automatique)](/fr/blog/laravel-cloud-hibernation.md)
- [Contrats](/fr/contracts.md)
