Vai al contenuto principale
Per l’uso di base fai riferimento alla pagina della guida. In questa pagina presentiamo pattern pratici non presenti nella documentazione ufficiale.

Scope di team in un SaaS multi-tenant

Se vuoi gestire i flag a livello di team (tenant) invece che di singolo utente, cambia lo scope predefinito in team.
// AppServiceProvider
Feature::resolveScopeUsing(fn () => Auth::user()?->currentTeam);
Basta questo perché Feature::active('billing-v2') si riferisca automaticamente al team corrente. Finché i membri appartengono allo stesso team ottengono lo stesso risultato, garantendo coerenza della UI. Esempio di rollout graduale in base alla data di iscrizione del team.
use App\Models\Team;
use Illuminate\Support\Carbon;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;

Feature::define('new-billing', function (Team $team) {
    // I team registrati dal 2024 in poi sono subito abilitati
    if ($team->created_at->isAfter(new Carbon('2024-01-01'))) {
        return true;
    }

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

    // Team più vecchi: cautela, si parte dall'1%
    return Lottery::odds(1 / 100);
});
Se vuoi abilitarlo solo per team specifici (per esempio anteprima per clienti enterprise):
// Dopo il deploy, abilita manualmente solo per team specifici
Feature::for($earlyAccessTeam)->activate('new-billing');

Kill switch di emergenza (uso del metodo before)

Quando in produzione emerge un bug, puoi disabilitare istantaneamente una funzionalità senza rollback del codice. Aggiungendo il metodo before a una feature basata su classe, il controllo viene eseguito prima del valore in storage.
<?php

namespace App\Features;

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

class NewCheckout
{
    /**
     * Interruttore di emergenza che disabilita istantaneamente tramite config in caso di bug in produzione
     */
    public function before(User $user): mixed
    {
        // Controllato dal valore in config/features.php o da env
        if (Config::boolean('features.new_checkout_disabled', false)) {
            return false; // Disabilita istantaneamente per tutti
        }

        // Gli amministratori sono sempre abilitati (per il debug)
        if ($user->isAdmin()) {
            return true;
        }

        return null; // Restituendo null si passa al normale resolve()
    }

    public function resolve(User $user): mixed
    {
        return $user->isPremium() || Lottery::odds(1 / 5);
    }
}
Impostando la variabile d’ambiente FEATURES_NEW_CHECKOUT_DISABLED=true puoi fermare la funzionalità senza toccare il DB. Un kill switch senza deploy.
Se before restituisce null, viene eseguito resolve(). Se restituisce false, viene trattato immediatamente come inattivo. Quando non è un’emergenza, restituisci null.

Rollout schedulato

Se vuoi pubblicare automaticamente a tutti gli utenti a una data e ora specifiche, puoi implementarlo con il metodo before.
public function before(User $user): mixed
{
    $rolloutDate = Config::get('features.new_api.rollout_date');

    if ($rolloutDate && Carbon::parse($rolloutDate)->isPast()) {
        return true; // Passata la data di rollout, pubblicato a tutti
    }

    return null; // Se non ancora, passa al normale resolve()
}

public function resolve(User $user): mixed
{
    // Prima del rollout, solo membri interni
    return $user->isInternalTeamMember();
}
# .env
FEATURES_NEW_API_ROLLOUT_DATE=2025-04-01
Così, superato il 2025-04-01, la funzionalità viene automaticamente pubblicata a tutti. Automatizzabile senza deploy, senza DB, senza comandi Artisan.

Dark launch (Shadow Mode)

Pattern che fa girare una nuova logica sui dati di produzione senza mostrarla agli utenti, confrontando i risultati con la logica precedente. Se non ci sono problemi, basta accendere il flag per completare il rilascio.
class RecommendationController
{
    public function index(Request $request)
    {
        $legacyResult = $this->getLegacyRecommendations($request->user());

        // In shadow mode esegui il nuovo algoritmo per confrontarlo, ma mostra la vecchia logica
        if (Feature::for($request->user())->active('recommendation-v2-shadow')) {
            try {
                $newResult = $this->getNewRecommendations($request->user());

                // Registra la differenza nei log (invisibile all'utente)
                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 risposta è sempre il risultato della vecchia logica
        return response()->json($legacyResult);
    }
}
Una volta verificato dai log che non ci sono più differenze, basta cambiare il flag in recommendation-v2.

Raccolta dei risultati A/B test tramite eventi

La documentazione ufficiale accenna all’evento FeatureRetrieved, ma non mostra un pattern reale di aggregazione A/B. L’evento FeatureResolved si attiva solo la prima volta che il valore della feature viene risolto. Usalo per registrare l’assegnazione della variante a un utente.
use Laravel\Pennant\Events\FeatureResolved;
use Illuminate\Support\Facades\Event;

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

    // Registra al momento dell'assegnazione della variante all'utente
    analytics()->identify($event->scope?->id, [
        'ab_purchase_button' => $event->value,
    ]);
});
Se registri separatamente il momento della conversione, potrai aggregare il tasso di conversione per ogni variante.
// Al completamento dell'acquisto
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'),
]);
Differenza tra FeatureResolved e FeatureRetrieved: FeatureResolved si attiva solo alla prima valutazione, FeatureRetrieved a ogni verifica. Per registrare l’assegnazione è più adatto FeatureResolved, per tracciare le page view FeatureRetrieved.

Scope nei job in coda

Nei job di coda non esiste un utente autenticato, quindi il controllo delle feature può comportarsi in modo inatteso. Passa esplicitamente lo scope al job.
class SendWeeklyDigest implements ShouldQueue
{
    public function __construct(
        private readonly User $user
    ) {}

    public function handle(): void
    {
        // NO: non c'è utente autenticato, quindi sarà sempre false
        // if (Feature::active('new-digest-layout')) { ... }

        // OK: passa esplicitamente l'utente
        if (Feature::for($this->user)->active('new-digest-layout')) {
            $this->sendNewLayout($this->user);
        } else {
            $this->sendLegacyLayout($this->user);
        }
    }
}
Puoi anche valutare il flag al momento del dispatch del job e passarlo al job stesso.
// Valuta nel controller e passa al job
$useNewLayout = Feature::for($user)->active('new-digest-layout');

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

UI di gestione tramite comando Artisan

Se crei un semplice comando Artisan per gestire i flag, puoi manipolare quelli di produzione senza deploy.
<?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 = 'Gestisce i 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('Azione sconosciuta'),
        };
    }

    private function activate(string $feature): void
    {
        Feature::activateForEveryone($feature);
        $this->info("✓ {$feature} attivato per tutti gli utenti");
    }

    private function deactivate(string $feature): void
    {
        Feature::deactivateForEveryone($feature);
        $this->info("✓ {$feature} disattivato per tutti gli utenti");
    }

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

        $this->line("{$feature}: attivo su {$count} scope");
    }
}
php artisan feature activate new-checkout
php artisan feature deactivate new-checkout
php artisan feature status new-checkout

Refactoring sicuro dei nomi delle feature (attributo Name)

Quando rinomini una feature class, se cambia il nome del flag salvato nel DB tutti i flag degli utenti vengono resettati. Con l’attributo Name fissa il nome usato per lo storage.
use Laravel\Pennant\Attributes\Name;

// Vecchio nome classe: CheckoutV2 → nuovo nome: NewCheckoutExperience
// Nel DB viene salvato sempre come 'checkout-v2'
#[Name('checkout-v2')]
class NewCheckoutExperience
{
    public function resolve(User $user): mixed
    {
        return $user->isPremium();
    }
}
In questo modo puoi rifattorizzare liberamente il nome della classe mantenendo i dati del DB.

Conclusioni

Una volta padroneggiate le basi della documentazione ufficiale, il vero valore di Pennant emerge con i pattern seguenti.
PatternContesto d’uso
Scope di teamSaaS multi-tenant
Kill switch beforeRisposta d’emergenza a bug in produzione
Rollout schedulatoAutomazione di rilasci a data e ora specifiche
Dark launchValidazione sicura di nuovi algoritmi in produzione
Evento FeatureResolvedRaccolta precisa dei risultati A/B test
Scope esplicito nei jobValutazione corretta dei flag in ambiente di coda
Comando Artisan di gestioneManipolazione dei flag di produzione senza deploy
Attributo NameRefactoring sicuro della classe

Guida Laravel Pennant

Per l’installazione e l’uso di base fai riferimento alla pagina della guida.
Ultima modifica il 13 luglio 2026