Presenta pattern pratici oltre le basi di Laravel Pennant. Introduce know-how che non emergono dalla sola documentazione ufficiale: scope a livello di team, kill switch di emergenza, dark launch, comandi di gestione e altro.
Per l’uso di base fai riferimento alla pagina della guida. In questa pagina presentiamo pattern pratici non presenti nella documentazione ufficiale.
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 specificiFeature::for($earlyAccessTeam)->activate('new-billing');
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.
<?phpnamespace 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.
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();}
# .envFEATURES_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.
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.
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 EventServiceProviderEvent::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'acquistoEvent::dispatch(new PurchaseCompleted($user, $product));
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.
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);
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.