Passer au contenu principal
Pour les bases, reportez-vous à la page de guide. 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.
// 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.
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) :
// 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

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

Rollout planifié

Vous voulez ouvrir automatiquement une fonctionnalité à tous les utilisateurs à une date donnée. Utilisez la méthode before.
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();
}
# .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.
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.
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.
// À la fin d'un achat
Event::dispatch(new PurchaseCompleted($user, $product));
// Listener PurchaseCompleted
analytics()->track('purchase_completed', [
    'user_id'              => $user->id,
    'ab_purchase_button'   => Feature::for($user)->value('purchase-button'),
]);
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.

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

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");
    }
}
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.
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.
PatronQuand l’utiliser
Scope équipeSaaS multi-locataire
Kill switch beforeUrgence bug en production
Rollout planifiéAutomatisation de release à date donnée
Dark launchValidation en prod sûre d’un nouvel algorithme
Événement FeatureResolvedTrace précise pour A/B testing
Scope explicite dans les jobsÉvaluation fiable dans les queues
Commande Artisan de managementManipuler les flags de prod sans déploiement
Attribut NameRefactor sûr des classes

Guide Laravel Pennant

Consultez la page de guide pour l’installation et les bases.
Dernière modification le 13 juillet 2026