Pennant

Introduzione

Laravel Pennant è un pacchetto di feature flag semplice e leggero, senza ingombri. I feature flag ti permettono di distribuire progressivamente nuove funzionalità dell’applicazione con sicurezza, fare A/B testing di nuovi design dell’interfaccia, completare una strategia di sviluppo trunk-based e molto altro.

Installazione

Per prima cosa, installa Pennant nel tuo progetto usando il gestore di pacchetti Composer:

composer require laravel/pennant

Successivamente, pubblica i file di configurazione e migrazione di Pennant usando il comando Artisan vendor:publish:

php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"

Infine, esegui le migrazioni del database della tua applicazione. Questo creerà una tabella features che Pennant utilizza per il suo driver database:

php artisan migrate

Configurazione

Dopo aver pubblicato le risorse di Pennant, il file di configurazione si troverà in config/pennant.php. Questo file di configurazione ti permette di specificare il meccanismo di archiviazione predefinito che Pennant utilizzerà per memorizzare i valori risolti delle feature flag.

Pennant supporta la memorizzazione dei valori delle feature flag risolte in un array in memoria tramite il driver array. Oppure, Pennant può memorizzarli in modo persistente in un database relazionale tramite il driver database, che è il meccanismo di archiviazione predefinito utilizzato da Pennant.

Definizione delle Funzionalità

Per definire una funzionalità, puoi utilizzare il metodo define fornito dal facade Feature. Devi fornire un nome per la funzionalità e una closure che verrà invocata per determinare il valore iniziale della funzionalità.

Di solito, le funzionalità vengono definite in un service provider usando il facade Feature. La closure riceverà lo "scope" per il controllo della funzionalità. Nella maggior parte dei casi, lo scope è l’utente attualmente autenticato. In questo esempio, definiremo una funzionalità per lanciare gradualmente una nuova API agli utenti della nostra applicazione:

<?php

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\Lottery;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Avvia i servizi dell'applicazione.
     */
    public function boot(): void
    {
        Feature::define('new-api', fn (User $user) => match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        });
    }
}

Come puoi vedere, abbiamo le seguenti regole per la nostra funzionalità:

  • Tutti i membri del team interno dovrebbero usare la nuova API.
  • I clienti ad alto traffico non dovrebbero usare la nuova API.
  • Altrimenti, la funzionalità dovrebbe essere assegnata casualmente agli utenti con una probabilità di 1 su 100 di essere attiva.

La prima volta che la funzionalità new-api viene controllata per un determinato utente, il risultato della closure verrà memorizzato dal driver di storage. La volta successiva che la funzionalità viene controllata per lo stesso utente, il valore verrà recuperato dallo storage e la closure non verrà invocata.

Per comodità, se una definizione di funzionalità restituisce solo una lotteria, puoi omettere completamente la closure:

    Feature::define('site-redesign', Lottery::odds(1, 1000));

Caratteristiche Basate su Classi

Pennant ti permette anche di definire caratteristiche basate su classi. A differenza delle definizioni di caratteristiche basate su closure, non è necessario registrare una caratteristica basata su classe in un service provider. Per creare una caratteristica basata su classe, puoi eseguire il comando Artisan pennant:feature. Per default, la classe della caratteristica sarà collocata nella directory app/Features della tua applicazione:

php artisan pennant:feature NewApi

Quando scrivi una classe per una caratteristica, devi solo definire un metodo resolve, che verrà chiamato per determinare il valore iniziale della caratteristica per un determinato ambito. Di solito, l’ambito sarà l’utente attualmente autenticato:

<?php

namespace App\Features;

use App\Models\User;
use Illuminate\Support\Lottery;

class NewApi
{
    /**
     * Risolve il valore iniziale della caratteristica.
     */
    public function resolve(User $user): mixed
    {
        return match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        };
    }
}

Se desideri risolvere manualmente un’istanza di una caratteristica basata su classe, puoi chiamare il metodo instance sul facade Feature:

use Illuminate\Support\Facades\Feature;

$instance = Feature::instance(NewApi::class);

Le classi delle caratteristiche vengono risolte tramite il container, quindi puoi iniettare dipendenze nel costruttore della classe della caratteristica quando necessario.

Personalizzare il Nome della Funzionalità Memorizzata

Di default, Pennant memorizza il nome completo della classe della funzionalità. Se desideri separare il nome memorizzato della funzionalità dalla struttura interna dell’applicazione, puoi specificare una proprietà $name nella classe della funzionalità. Il valore di questa proprietà sarà memorizzato al posto del nome della classe:

<?php

namespace App\Features;

class NewApi
{
    /**
     * Il nome memorizzato della funzionalità.
     *
     * @var string
     */
    public $name = 'new-api';

    // ...
}

Verifica delle Feature

Per determinare se una feature è attiva, puoi usare il metodo active sulla facade Feature. Per impostazione predefinita, le feature vengono verificate rispetto all’utente attualmente autenticato:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;

class PodcastController
{
    /**
     * Display a listing of the resource.
     */
    public function index(Request $request): Response
    {
        return Feature::active('new-api')
                ? $this->resolveNewApiResponse($request)
                : $this->resolveLegacyApiResponse($request);
    }

    // ...
}

Sebbene per impostazione predefinita le feature vengano verificate rispetto all’utente attualmente autenticato, puoi facilmente controllare la feature rispetto a un altro utente o scope. Per farlo, usa il metodo for offerto dalla facade Feature:

return Feature::for($user)->active('new-api')
        ? $this->resolveNewApiResponse($request)
        : $this->resolveLegacyApiResponse($request);

Pennant offre anche alcuni metodi di comodità aggiuntivi che possono essere utili per determinare se una feature è attiva o meno:

// Determina se tutte le feature date sono attive...
Feature::allAreActive(['new-api', 'site-redesign']);

// Determina se qualche feature data è attiva...
Feature::someAreActive(['new-api', 'site-redesign']);

// Determina se una feature è inattiva...
Feature::inactive('new-api');

// Determina se tutte le feature date sono inattive...
Feature::allAreInactive(['new-api', 'site-redesign']);

// Determina se qualche feature data è inattiva...
Feature::someAreInactive(['new-api', 'site-redesign']);

Quando si utilizza Pennant al di fuori di un contesto HTTP, come in un comando Artisan o in un job in coda, dovresti tipicamente specificare esplicitamente lo scope della feature. In alternativa, puoi definire uno scope predefinito che tenga conto sia dei contesti HTTP autenticati che di quelli non autenticati.

Controllo delle Funzionalità Basate su Classi

Per le funzionalità basate su classi, devi fornire il nome della classe quando verifichi la funzionalità:

<?php

namespace App\Http\Controllers;

use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;

class PodcastController
{
    /**
     * Visualizza un elenco delle risorse.
     */
    public function index(Request $request): Response
    {
        return Feature::active(NewApi::class)
                ? $this->resolveNewApiResponse($request)
                : $this->resolveLegacyApiResponse($request);
    }

    // ...
}

Esecuzione Condizionale

Il metodo when può essere utilizzato per eseguire fluentemente una determinata closure se una funzionalità è attiva. Inoltre, può essere fornita una seconda closure che verrà eseguita se la funzionalità è inattiva:

<?php

namespace App\Http\Controllers;

use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;

class PodcastController
{
    /**
     * Mostra un elenco delle risorse.
     */
    public function index(Request $request): Response
    {
        return Feature::when(NewApi::class,
            fn () => $this->resolveNewApiResponse($request),
            fn () => $this->resolveLegacyApiResponse($request),
        );
    }

    // ...
}

Il metodo unless funge da inverso del metodo when, eseguendo la prima closure se la funzionalità è inattiva:

return Feature::unless(NewApi::class,
    fn () => $this->resolveLegacyApiResponse($request),
    fn () => $this->resolveNewApiResponse($request),
);

Il Trait HasFeatures

Il trait HasFeatures di Pennant può essere aggiunto al modello User della tua applicazione (o a qualsiasi altro modello che possiede funzionalità) per offrire un modo fluido e comodo per controllare le funzionalità direttamente dal modello:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Pennant\Concerns\HasFeatures;

class User extends Authenticatable
{
    use HasFeatures;

    // ...
}

Una volta aggiunto il trait al tuo modello, puoi facilmente controllare le funzionalità invocando il metodo features:

if ($user->features()->active('new-api')) {
    // ...
}

Naturalmente, il metodo features fornisce accesso a molti altri metodi comodi per interagire con le funzionalità:

// Values...
$value = $user->features()->value('purchase-button')
$values = $user->features()->values(['new-api', 'purchase-button']);

// State...
$user->features()->active('new-api');
$user->features()->allAreActive(['new-api', 'server-api']);
$user->features()->someAreActive(['new-api', 'server-api']);

$user->features()->inactive('new-api');
$user->features()->allAreInactive(['new-api', 'server-api']);
$user->features()->someAreInactive(['new-api', 'server-api']);

// Conditional execution...
$user->features()->when('new-api',
    fn () => /* ... */,
    fn () => /* ... */,
);

$user->features()->unless('new-api',
    fn () => /* ... */,
    fn () => /* ... */,
);

Direttiva Blade

Per rendere il controllo delle funzionalità in Blade un’esperienza senza intoppi, Pennant offre le direttive @feature e @featureany:

@feature('site-redesign')
    <!-- 'site-redesign' è attivo -->
@else
    <!-- 'site-redesign' è inattivo -->
@endfeature

@featureany(['site-redesign', 'beta'])
    <!-- 'site-redesign' o `beta` è attivo -->
@endfeatureany

Middleware

Pennant include anche un middleware che può essere utilizzato per verificare se l’utente autenticato ha accesso a una funzionalità prima che una route venga invocata. Puoi assegnare il middleware a una route e specificare le funzionalità necessarie per accedervi. Se una delle funzionalità specificate è inattiva per l’utente autenticato, la route restituirà una risposta HTTP 400 Bad Request. È possibile passare più funzionalità al metodo statico using.

use Illuminate\Support\Facades\Route;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;

Route::get('/api/servers', function () {
    // ...
})->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));

Personalizzare la Risposta

Se desideri personalizzare la risposta restituita dal middleware quando una delle funzionalità elencate è inattiva, puoi usare il metodo whenInactive fornito dal middleware EnsureFeaturesAreActive. Generalmente, questo metodo dovrebbe essere invocato all’interno del metodo boot di uno dei service provider della tua applicazione:

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    EnsureFeaturesAreActive::whenInactive(
        function (Request $request, array $features) {
            return new Response(status: 403);
        }
    );

    // ...
}

Intercettare le Verifiche delle Funzionalità

A volte può essere utile eseguire alcune verifiche in memoria prima di recuperare il valore memorizzato di una determinata funzionalità. Immagina di sviluppare una nuova API dietro una feature flag e di voler avere la possibilità di disabilitare la nuova API senza perdere i valori risolti delle funzionalità memorizzati. Se noti un bug nella nuova API, potresti facilmente disabilitarla per tutti tranne che per i membri del team interno, correggere il bug e poi riabilitare la nuova API per gli utenti che avevano già accesso alla funzionalità.

Puoi ottenere questo con il metodo before di una feature basata su classi. Quando presente, il metodo before viene sempre eseguito in memoria prima di recuperare il valore dalla memorizzazione. Se viene restituito un valore diverso da null dal metodo, verrà usato al posto del valore memorizzato della funzionalità per la durata della richiesta:

<?php

namespace App\Features;

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

class NewApi
{
    /**
     * Esegui una verifica sempre in memoria prima che il valore memorizzato venga recuperato.
     */
    public function before(User $user): mixed
    {
        if (Config::get('features.new-api.disabled')) {
            return $user->isInternalTeamMember();
        }
    }

    /**
     * Risolvi il valore iniziale della funzionalità.
     */
    public function resolve(User $user): mixed
    {
        return match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        };
    }
}

Puoi anche usare questa funzionalità per programmare il lancio globale di una funzionalità che era precedentemente dietro una feature flag:

<?php

namespace App\Features;

use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;

class NewApi
{
    /**
     * Esegui una verifica sempre in memoria prima che il valore memorizzato venga recuperato.
     */
    public function before(User $user): mixed
    {
        if (Config::get('features.new-api.disabled')) {
            return $user->isInternalTeamMember();
        }

        if (Carbon::parse(Config::get('features.new-api.rollout-date'))->isPast()) {
            return true;
        }
    }

    // ...
}

Cache In-Memory

Quando verifichi una feature, Pennant creerà una cache in-memory del risultato. Se utilizzi il driver database, questo significa che riconfermare lo stesso flag di feature all’interno di una singola richiesta non genererà query aggiuntive al database. Questo garantisce anche che la feature abbia un risultato coerente per tutta la durata della richiesta.

Se hai bisogno di svuotare manualmente la cache in-memory, puoi usare il metodo flushCache offerto dalla facade Feature:

Feature::flushCache();

Ambito

Specificare l’Ambito

Come discusso, le funzionalità vengono solitamente verificate rispetto all’utente attualmente autenticato. Tuttavia, questo potrebbe non soddisfare sempre le tue esigenze. Pertanto, è possibile specificare l’ambito con cui verificare una determinata funzionalità tramite il metodo for della facciata Feature:

return Feature::for($user)->active('new-api')
        ? $this->resolveNewApiResponse($request)
        : $this->resolveLegacyApiResponse($request);

Naturalmente, gli ambiti delle funzionalità non sono limitati agli "utenti". Immagina di aver creato una nuova esperienza di fatturazione che stai distribuendo a interi team anziché a singoli utenti. Forse vorresti che i team più vecchi avessero una distribuzione più lenta rispetto ai team più nuovi. La tua closure per la risoluzione della funzionalità potrebbe apparire più o meno così:

use App\Models\Team;
use Carbon\Carbon;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;

Feature::define('billing-v2', function (Team $team) {
    if ($team->created_at->isAfter(new Carbon('1st Jan, 2023'))) {
        return true;
    }

    if ($team->created_at->isAfter(new Carbon('1st Jan, 2019'))) {
        return Lottery::odds(1 / 100);
    }

    return Lottery::odds(1 / 1000);
});

Noterai che la closure che abbiamo definito non si aspetta un User, ma un modello Team. Per determinare se questa funzionalità è attiva per il team di un utente, dovresti passare il team al metodo for offerto dalla facciata Feature:

if (Feature::for($user->team)->active('billing-v2')) {
    return redirect('/billing/v2');
}

// ...

Ambito Predefinito

È anche possibile personalizzare l’ambito predefinito che Pennant utilizza per verificare le funzionalità. Ad esempio, potresti voler controllare tutte le funzionalità in base al team dell’utente attualmente autenticato invece che all’utente stesso. Invece di dover chiamare Feature::for($user->team) ogni volta che controlli una funzionalità, puoi specificare il team come ambito predefinito. Tipicamente, questo dovrebbe essere fatto in uno dei provider di servizi della tua applicazione:

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);

        // ...
    }
}

Se non viene fornito esplicitamente un ambito tramite il metodo for, il controllo delle funzionalità utilizzerà ora il team dell’utente attualmente autenticato come ambito predefinito:

Feature::active('billing-v2');

// È ora equivalente a...

Feature::for($user->team)->active('billing-v2');

Ambito Nullable

Se l’ambito che fornisci quando verifichi una feature è null e la definizione della feature non supporta null tramite un tipo nullable o includendo null in un tipo unione, Pennant restituirà automaticamente false come valore del risultato della feature.

Pertanto, se l’ambito che passi a una feature può essere null e desideri che il resolver del valore della feature venga invocato, dovresti considerarlo nella definizione della tua feature. Un ambito null può verificarsi se controlli una feature all’interno di un comando Artisan, di un job in coda o di una rotta non autenticata. Poiché in questi contesti di solito non c’è un utente autenticato, l’ambito predefinito sarà null.

Se non specifichi sempre esplicitamente l’ambito della tua feature allora dovresti assicurarti che il tipo dell’ambito sia "nullable" e gestire il valore dell’ambito null nella logica della definizione della tua feature:

use App\Models\User;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;

Feature::define('new-api', fn (User $user) => match (true) {// [tl! remove]
Feature::define('new-api', fn (User|null $user) => match (true) {// [tl! add]
    $user === null => true,// [tl! add]
    $user->isInternalTeamMember() => true,
    $user->isHighTrafficCustomer() => false,
    default => Lottery::odds(1 / 100),
});

Identificazione dello Scope

I driver di storage integrati di Pennant, array e database, sanno come memorizzare correttamente gli identificatori di scope per tutti i tipi di dati PHP e per i modelli Eloquent. Tuttavia, se la tua applicazione utilizza un driver Pennant di terze parti, potrebbe non sapere come memorizzare correttamente un identificatore per un modello Eloquent o altri tipi personalizzati nella tua applicazione.

Per questo motivo, Pennant ti permette di formattare i valori dello scope per la memorizzazione implementando il contratto FeatureScopeable sugli oggetti della tua applicazione che vengono utilizzati come scope di Pennant.

Ad esempio, immagina di usare due driver di feature diversi in un’unica applicazione: il driver integrato database e un driver di terze parti "Flag Rocket". Il driver "Flag Rocket" non sa come memorizzare correttamente un modello Eloquent. Invece, richiede un’istanza di FlagRocketUser. Implementando il metodo toFeatureIdentifier definito dal contratto FeatureScopeable, possiamo personalizzare il valore dello scope memorizzabile fornito a ciascun driver usato dalla nostra applicazione:

<?php

namespace App\Models;

use FlagRocket\FlagRocketUser;
use Illuminate\Database\Eloquent\Model;
use Laravel\Pennant\Contracts\FeatureScopeable;

class User extends Model implements FeatureScopeable
{
    /**
     * Cast the object to a feature scope identifier for the given driver.
     */
    public function toFeatureIdentifier(string $driver): mixed
    {
        return match($driver) {
            'database' => $this,
            'flag-rocket' => FlagRocketUser::fromId($this->flag_rocket_id),
        };
    }
}

Serializzazione dello Scope

Per impostazione predefinita, Pennant utilizza il nome completamente qualificato della classe quando memorizza una feature associata a un modello Eloquent. Se già usi una Eloquent morph map, puoi scegliere di far sì che anche Pennant utilizzi la morph map per separare la feature memorizzata dalla struttura della tua applicazione.

Per ottenere ciò, dopo aver definito la tua morph map di Eloquent in un service provider, puoi invocare il metodo useMorphMap della facciata Feature:

use Illuminate\Database\Eloquent\Relations\Relation;
use Laravel\Pennant\Feature;

Relation::enforceMorphMap([
    'post' => 'App\Models\Post',
    'video' => 'App\Models\Video',
]);

Feature::useMorphMap();

Valori "Ricchi" delle Funzionalità

Fino ad ora, abbiamo principalmente mostrato le funzionalità come se fossero in uno stato binario, cioè "attive" o "inattive", ma Pennant permette anche di memorizzare valori più complessi, o "ricchi".

Ad esempio, immagina di testare tre nuovi colori per il pulsante "Acquista ora" della tua applicazione. Invece di restituire true o false dalla definizione della funzionalità, puoi restituire una stringa:

use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;

Feature::define('purchase-button', fn (User $user) => Arr::random([
    'blue-sapphire',
    'seafoam-green',
    'tart-orange',
]));

Puoi ottenere il valore della funzionalità purchase-button utilizzando il metodo value:

$color = Feature::value('purchase-button');

La direttiva Blade inclusa in Pennant rende anche facile mostrare condizionalmente contenuti in base al valore attuale della funzionalità:

@feature('purchase-button', 'blue-sapphire')
    <!-- 'blue-sapphire' è attivo -->
@elsefeature('purchase-button', 'seafoam-green')
    <!-- 'seafoam-green' è attivo -->
@elsefeature('purchase-button', 'tart-orange')
    <!-- 'tart-orange' è attivo -->
@endfeature

Quando si utilizzano valori ricchi, è importante sapere che una funzionalità è considerata "attiva" quando ha qualsiasi valore diverso da false.

Quando si chiama il metodo condizionale when, il valore ricco della funzionalità sarà fornito alla prima closure:

    Feature::when('purchase-button',
        fn ($color) => /* ... */,
        fn () => /* ... */,
    );

Allo stesso modo, quando si chiama il metodo condizionale unless, il valore ricco della funzionalità sarà fornito alla closure opzionale secondaria:

    Feature::unless('purchase-button',
        fn () => /* ... */,
        fn ($color) => /* ... */,
    );

Recuperare più funzionalità

Il metodo values consente di recuperare più funzionalità per un determinato ambito:

Feature::values(['billing-v2', 'purchase-button']);

// [
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
// ]

Oppure, puoi utilizzare il metodo all per recuperare i valori di tutte le funzionalità definite per un determinato ambito:

Feature::all();

// [
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
//     'site-redesign' => true,
// ]

Tuttavia, le funzionalità basate su classe sono registrate dinamicamente e non sono conosciute da Pennant fino a quando non vengono controllate esplicitamente. Ciò significa che le funzionalità basate su classe della tua applicazione potrebbero non apparire nei risultati restituiti dal metodo all se non sono già state verificate durante la richiesta corrente.

Se desideri assicurarti che le classi delle funzionalità siano sempre incluse quando utilizzi il metodo all, puoi utilizzare le capacità di scoperta delle funzionalità di Pennant. Per iniziare, invoca il metodo discover in uno dei provider di servizi della tua applicazione:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Feature::discover();

        // ...
    }
}

Il metodo discover registrerà tutte le classi delle funzionalità nella directory app/Features della tua applicazione. Il metodo all includerà ora queste classi nei suoi risultati, indipendentemente dal fatto che siano state verificate durante la richiesta corrente:

Feature::all();

// [
//     'App\Features\NewApi' => true,
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
//     'site-redesign' => true,
// ]

Eager Loading

Anche se Pennant mantiene una cache in memoria di tutte le feature risolte per una singola richiesta, potrebbero comunque sorgere problemi di prestazioni. Per alleviare questo, Pennant offre la possibilità di caricare preventivamente i valori delle feature.

Per illustrare questo, immagina di verificare se una feature è attiva all’interno di un ciclo:

use Laravel\Pennant\Feature;

foreach ($users as $user) {
    if (Feature::for($user)->active('notifications-beta')) {
        $user->notify(new RegistrationSuccess);
    }
}

Supponendo di utilizzare il driver del database, questo codice eseguirà una query al database per ogni utente nel ciclo, potenzialmente eseguendo centinaia di query. Tuttavia, utilizzando il metodo load di Pennant, possiamo eliminare questo possibile collo di bottiglia nelle prestazioni caricando preventivamente i valori delle feature per una collezione di utenti o ambiti:

Feature::for($users)->load(['notifications-beta']);

foreach ($users as $user) {
    if (Feature::for($user)->active('notifications-beta')) {
        $user->notify(new RegistrationSuccess);
    }
}

Per caricare i valori delle feature solo quando non sono già stati caricati, puoi utilizzare il metodo loadMissing:

Feature::for($users)->loadMissing([
    'new-api',
    'purchase-button',
    'notifications-beta',
]);

Puoi caricare tutte le feature definite usando il metodo loadAll:

Feature::for($users)->loadAll();

Aggiornamento dei Valori

Quando il valore di una feature viene risolto per la prima volta, il driver sottostante memorizzerà il risultato nello storage. Questo è spesso necessario per garantire un’esperienza coerente per gli utenti tra diverse richieste. Tuttavia, a volte potresti voler aggiornare manualmente il valore memorizzato della feature.

Per fare ciò, puoi usare i metodi activate e deactivate per attivare o disattivare una feature "on" o "off":

use Laravel\Pennant\Feature;

// Activate the feature for the default scope...
Feature::activate('new-api');

// Deactivate the feature for the given scope...
Feature::for($user->team)->deactivate('billing-v2');

È anche possibile impostare manualmente un valore complesso per una feature fornendo un secondo argomento al metodo activate:

Feature::activate('purchase-button', 'seafoam-green');

Per istruire Pennant a dimenticare il valore memorizzato per una feature, puoi usare il metodo forget. Quando la feature viene controllata di nuovo, Pennant risolverà il valore della feature dalla sua definizione:

Feature::forget('purchase-button');

Aggiornamenti di massa

Per aggiornare i valori delle feature memorizzati in blocco, puoi utilizzare i metodi activateForEveryone e deactivateForEveryone.

Ad esempio, immagina di essere sicuro della stabilità della feature new-api e di aver scelto il miglior colore 'purchase-button' per il tuo flusso di checkout – puoi aggiornare di conseguenza il valore memorizzato per tutti gli utenti:

use Laravel\Pennant\Feature;

Feature::activateForEveryone('new-api');

Feature::activateForEveryone('purchase-button', 'seafoam-green');

In alternativa, puoi disattivare la feature per tutti gli utenti:

Feature::deactivateForEveryone('new-api');

Questo aggiornerà solo i valori delle feature risolte che sono stati memorizzati dal driver di storage di Pennant. È inoltre necessario aggiornare la definizione della feature nella tua applicazione.

Rimozione delle Funzionalità

A volte può essere utile rimuovere completamente una funzionalità dall’archiviazione. Questo è tipicamente necessario se hai rimosso la funzionalità dalla tua applicazione oppure hai apportato modifiche alla definizione della funzionalità che desideri distribuire a tutti gli utenti.

Puoi rimuovere tutti i valori memorizzati per una funzionalità usando il metodo purge:

// Rimozione di una singola funzionalità...
Feature::purge('new-api');

// Rimozione di più funzionalità...
Feature::purge(['new-api', 'purchase-button']);

Se desideri rimuovere tutte le funzionalità dall’archiviazione, puoi chiamare il metodo purge senza argomenti:

Feature::purge();

Poiché può essere utile rimuovere le funzionalità come parte della pipeline di distribuzione della tua applicazione, Pennant include il comando Artisan pennant:purge che eliminerà le funzionalità fornite dall’archiviazione:

php artisan pennant:purge new-api

php artisan pennant:purge new-api purchase-button

È anche possibile rimuovere tutte le funzionalità tranne quelle in una lista specifica. Ad esempio, immagina di voler eliminare tutte le funzionalità ma mantenere i valori per le funzionalità "new-api" e "purchase-button" nell’archiviazione. Per fare ciò, puoi passare questi nomi di funzionalità all’opzione --except:

php artisan pennant:purge --except=new-api --except=purchase-button

Per comodità, il comando pennant:purge supporta anche l’opzione --except-registered. Questa opzione indica che tutte le funzionalità, tranne quelle registrate esplicitamente in un provider di servizi, devono essere rimosse:

php artisan pennant:purge --except-registered

Testing

Quando si testa il codice che interagisce con i feature flag, il modo più semplice per controllare il valore restituito dal feature flag nei tuoi test è ridefinire semplicemente la feature. Ad esempio, immagina di avere la seguente feature definita in uno dei service provider della tua applicazione:

use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;

Feature::define('purchase-button', fn () => Arr::random([
    'blue-sapphire',
    'seafoam-green',
    'tart-orange',
]));

Per modificare il valore restituito dalla feature nei tuoi test, puoi ridefinire la feature all’inizio del test. Il seguente test passerà sempre, anche se l’implementazione di Arr::random() è ancora presente nel service provider:

use Laravel\Pennant\Feature;

test('it can control feature values', function () {
    Feature::define('purchase-button', 'seafoam-green');

    expect(Feature::value('purchase-button'))->toBe('seafoam-green');
});
use Laravel\Pennant\Feature;

public function test_it_can_control_feature_values()
{
    Feature::define('purchase-button', 'seafoam-green');

    $this->assertSame('seafoam-green', Feature::value('purchase-button'));
}

Lo stesso approccio può essere utilizzato per le feature basate su classi:

use Laravel\Pennant\Feature;

test('it can control feature values', function () {
    Feature::define(NewApi::class, true);

    expect(Feature::value(NewApi::class))->toBeTrue();
});
use App\Features\NewApi;
use Laravel\Pennant\Feature;

public function test_it_can_control_feature_values()
{
    Feature::define(NewApi::class, true);

    $this->assertTrue(Feature::value(NewApi::class));
}

Se la tua feature restituisce un’istanza di Lottery, ci sono diversi helper per i test disponibili.

Configurazione dello Store

Puoi configurare lo store che Pennant utilizzerà durante i test definendo la variabile d’ambiente PENNANT_STORE nel file phpunit.xml della tua applicazione:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
    <!-- ... -->
    <php>
        <env name="PENNANT_STORE" value="array"/>
        <!-- ... -->
    </php>
</phpunit>

Aggiungere Driver Pennant Personalizzati

Implementare il Driver

Se nessuno degli storage drivers esistenti di Pennant soddisfa le esigenze della tua applicazione, puoi scrivere il tuo storage driver. Il tuo driver personalizzato dovrebbe implementare l’interfaccia Laravel\Pennant\Contracts\Driver:

<?php

namespace App\Extensions;

use Laravel\Pennant\Contracts\Driver;

class RedisFeatureDriver implements Driver
{
    public function define(string $feature, callable $resolver): void {}
    public function defined(): array {}
    public function getAll(array $features): array {}
    public function get(string $feature, mixed $scope): mixed {}
    public function set(string $feature, mixed $scope, mixed $value): void {}
    public function setForAllScopes(string $feature, mixed $value): void {}
    public function delete(string $feature, mixed $scope): void {}
    public function purge(array|null $features): void {}
}

Ora, dobbiamo solo implementare ciascuno di questi metodi utilizzando una connessione Redis. Per un esempio su come implementare ognuno di questi metodi, dai un’occhiata a Laravel\Pennant\Drivers\DatabaseDriver nel codice sorgente di Pennant

Laravel non fornisce una directory per contenere le tue estensioni. Sei libero di posizionarle dove preferisci. In questo esempio, abbiamo creato una directory Extensions per ospitare il RedisFeatureDriver.

Registrare il Driver

Una volta implementato il tuo driver, sei pronto per registrarlo con Laravel. Per aggiungere driver aggiuntivi a Pennant, puoi utilizzare il metodo extend fornito dalla facade Feature. Dovresti chiamare il metodo extend dal metodo boot di uno dei provider di servizi della tua applicazione:

<?php

namespace App\Providers;

use App\Extensions\RedisFeatureDriver;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Registra i servizi dell'applicazione.
     */
    public function register(): void
    {
        // ...
    }

    /**
     * Avvia i servizi dell'applicazione.
     */
    public function boot(): void
    {
        Feature::extend('redis', function (Application $app) {
            return new RedisFeatureDriver($app->make('redis'), $app->make('events'), []);
        });
    }
}

Una volta registrato il driver, puoi utilizzare il driver redis nel file di configurazione config/pennant.php della tua applicazione:

    'stores' => [

        'redis' => [
            'driver' => 'redis',
            'connection' => null,
        ],

        // ...
    ],

Definizione delle funzionalità esternamente

Se il tuo driver è un wrapper attorno a una piattaforma di feature flag di terze parti, probabilmente definirai le funzionalità sulla piattaforma anziché usare il metodo Feature::define di Pennant. In tal caso, il tuo driver personalizzato dovrebbe anche implementare l’interfaccia Laravel\Pennant\Contracts\DefinesFeaturesExternally:

<?php

namespace App\Extensions;

use Laravel\Pennant\Contracts\Driver;
use Laravel\Pennant\Contracts\DefinesFeaturesExternally;

class FeatureFlagServiceDriver implements Driver, DefinesFeaturesExternally
{
    /**
     * Ottieni le funzionalità definite per lo scope dato.
     */
    public function definedFeaturesForScope(mixed $scope): array {}

    /* ... */
}

Il metodo definedFeaturesForScope dovrebbe restituire un elenco di nomi delle funzionalità definite per lo scope fornito.

Events

Pennant invia diversi eventi che possono essere utili per tracciare le feature flags all’interno della tua applicazione.

Laravel\Pennant\Events\FeatureRetrieved

Questo evento viene emesso ogni volta che una feature viene controllata. Può essere utile per creare e monitorare metriche sull’uso di un flag di funzionalità nella tua applicazione.

Laravel\Pennant\Events\FeatureResolved

Questo evento viene inviato la prima volta che il valore di una feature viene risolto per uno scope specifico.

Laravel\Pennant\Events\UnknownFeatureResolved

Questo evento viene emesso la prima volta che una funzionalità sconosciuta viene risolta per uno specifico ambito. Ascoltare questo evento può essere utile se avevi intenzione di rimuovere una feature flag ma hai accidentalmente lasciato riferimenti sparsi nella tua applicazione:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Laravel\Pennant\Events\UnknownFeatureResolved;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Avvia i servizi dell'applicazione.
     */
    public function boot(): void
    {
        Event::listen(function (UnknownFeatureResolved $event) {
            Log::error("Resolving unknown feature [{$event->feature}].");
        });
    }
}

Laravel\Pennant\Events\DynamicallyRegisteringFeatureClass

Questo evento viene emesso quando una class based feature viene verificata dinamicamente per la prima volta durante una richiesta.

Laravel\Pennant\Events\UnexpectedNullScopeEncountered

Questo evento viene emesso quando viene passato uno scope null a una definizione di funzionalità che non supporta null.

Questa situazione viene gestita correttamente e la funzionalità restituirà false. Tuttavia, se desideri disattivare il comportamento predefinito, puoi registrare un listener per questo evento nel metodo boot del AppServiceProvider della tua applicazione:

use Illuminate\Support\Facades\Log;
use Laravel\Pennant\Events\UnexpectedNullScopeEncountered;

/**
 * Avvia i servizi dell'applicazione.
 */
public function boot(): void
{
    Event::listen(UnexpectedNullScopeEncountered::class, fn () => abort(500));
}

Laravel\Pennant\Events\FeatureUpdated

Questo evento viene emesso quando si aggiorna una feature per uno scope, solitamente chiamando activate o deactivate.

Laravel\Pennant\Events\FeatureUpdatedForAllScopes

Questo evento viene emesso quando si aggiorna una feature per tutti gli scope, di solito chiamando activateForEveryone o deactivateForEveryone.

Laravel\Pennant\Events\FeatureDeleted

Questo evento viene emesso quando si elimina una feature per uno scope, di solito chiamando forget.

Laravel\Pennant\Events\FeaturesPurged

Questo evento viene emesso quando si purgano funzionalità specifiche.

Laravel\Pennant\Events\AllFeaturesPurged

Questo evento viene emesso quando vengono eliminate tutte le funzionalità.

Lascia un commento

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *