Riepilogo post nella categoria Laravel Pennant

Nello sviluppo di applicazioni aziendali complesse, la capacità di adattare il comportamento del software a contesti diversi o di introdurre gradualmente nuove implementazioni di funzionalità senza stravolgere il codice esistente è un enorme vantaggio competitivo. Questa flessibilità architetturale è particolarmente cruciale in un framework come Laravel, dove il Service Container e la dependency injection giocano un ruolo centrale. Tuttavia, in applicazioni Laravel 9 o Laravel 10 più datate, o in quelle che sono cresciute rapidamente, la selezione dinamica di quale implementazione concreta di un'interfaccia utilizzare può essere gestita con logiche condizionali che, nel tempo, diventano macchinose, difficili da mantenere e da testare.

Questo articolo tecnico si propone come una guida al refactoring di tali meccanismi. Esploreremo come passare da approcci di selezione dei servizi più statici o basati su semplice configurazione, tipici di Laravel 9/10, a pattern più sofisticati e programmatici che sfruttano appieno le potenzialità del Service Container di Laravel 12, anche in sinergia con strumenti come Laravel Pennant per una gestione basata su feature flag. L'obiettivo è rendere le applicazioni della tua impresa più modulari, adattive e pronte per future evoluzioni.

Se vuoi approfondire, continua a leggere. Se hai una domanda specifica a riguardo di questo articolo, contattami per una consulenza dedicata. Dai anche un'occhiata al mio profilo per capire come posso aiutare concretamente la tua azienda o startup a crescere e a modernizzarsi.

Scenario pre-refactoring: selezione "statica" o condizionale semplice in Laravel 9/10

Vediamo alcuni approcci comuni (e i loro limiti) per la selezione di implementazioni di servizi in applicazioni Laravel 9/10. Supponiamo di avere un'interfaccia PaymentGatewayInterface e due implementazioni concrete: StripePaymentGateway e PayPalPaymentGateway.

Approccio 1: Binding fisso nel Service Provider

Il caso più semplice e rigido è un binding fisso, solitamente definito nel metodo register() di un Service Provider (es. AppServiceProvider).

// app/Providers/AppServiceProvider.php (Laravel 9/10 style - binding fisso)
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Contracts\PaymentGatewayInterface;
use App\Services\StripePaymentGateway; // Assumiamo questa sia l'implementazione di default

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(PaymentGatewayInterface::class, StripePaymentGateway::class);
    }
    // ...
}
  • Limiti: Nessuna flessibilità a runtime. Per cambiare implementazione, bisogna modificare il codice e rieseguire il deployment.

Approccio 2: Binding condizionale basato su config() o .env

Un passo avanti è rendere il binding condizionale in base a un valore di configurazione, che può essere a sua volta influenzato da una variabile d'ambiente.

// config/services.php
return [
    // ...
    'payment_gateway' => [
        'driver' => env('PAYMENT_GATEWAY_DRIVER', 'stripe'), // 'stripe' o 'paypal'
    ],
];

// app/Providers/AppServiceProvider.php (Laravel 9/10 style - binding condizionale)
use App\Contracts\PaymentGatewayInterface;
use App\Services\StripePaymentGateway;
use App\Services\PayPalPaymentGateway;

public function register(): void
{
    $driver = config('services.payment_gateway.driver');

    if ($driver === 'paypal') {
        $this->app->bind(PaymentGatewayInterface::class, PayPalPaymentGateway::class);
    } else {
        // Default a Stripe
        $this->app->bind(PaymentGatewayInterface::class, StripePaymentGateway::class);
    }
}
  • Vantaggi: Si può cambiare l'implementazione modificando il file .env (e svuotando la cache di configurazione php artisan config:clear).
  • Svantaggi: La logica di selezione è ancora abbastanza statica per la durata di una configurazione deployata; se le condizioni diventano numerose o complesse (es. selezione basata su utente, tenant, o altri fattori dinamici), il Service Provider può diventare disordinato.

Approccio 3: Factory Method manuale

A volte, la logica di selezione viene incapsulata in una classe Factory dedicata.

// app/Factories/PaymentGatewayFactory.php
namespace App\Factories;

use App\Contracts\PaymentGatewayInterface;
use App\Services\StripePaymentGateway;
use App\Services\PayPalPaymentGateway;
use Illuminate\Support\Facades\Config; // Per accedere alla configurazione

class PaymentGatewayFactory
{
    public function make(): PaymentGatewayInterface
    {
        $driver = Config::get('services.payment_gateway.driver', 'stripe');

        if ($driver === 'paypal') {
            // Potrebbe anche risolvere dipendenze specifiche per PayPalPaymentGateway qui
            return app(PayPalPaymentGateway::class);
        }
        return app(StripePaymentGateway::class);
    }
}

// Il Service Provider potrebbe poi fare il binding dell'interfaccia a questa factory,
// o la factory potrebbe essere iniettata dove serve.
// Esempio di binding a una factory nel Service Provider:
// $this->app->singleton(PaymentGatewayInterface::class, function ($app) {
//     return (new PaymentGatewayFactory())->make();
// });
  • Vantaggi: La logica di selezione è centralizzata nella factory.
  • Svantaggi: Aggiunge un altro livello di astrazione; la factory stessa deve essere gestita e iniettata, o il Service Provider la usa internamente, il che può portare a una complessità simile all'approccio 2 se la logica nella factory diventa troppo articolata.

Questi approcci, sebbene funzionali, possono mancare della flessibilità necessaria per applicazioni aziendali che richiedono adattamenti dinamici o strategie di rollout graduale di nuove implementazioni.

Modernizzare la selezione dei servizi in Laravel 12: tecniche avanzate

Laravel 12 (ereditando e consolidando le potenzialità delle versioni precedenti, specialmente L11) offre modi più eleganti e potenti per gestire la selezione dinamica delle implementazioni di servizi, sfruttando appieno il Service Container.

1. Binding con Closure per logica dinamica al momento della risoluzione

Il metodo bind (o singleton) del Service Container può accettare una closure. Questa closure viene eseguita solo quando l'interfaccia viene effettivamente richiesta (risolta) dal container, permettendo di inserire logica dinamica al momento della risoluzione.

// app/Providers/AppServiceProvider.php (Laravel 11/12 style)
use App\Contracts\NotificationSenderInterface;
use App\Services\EmailNotificationSender;
use App\Services\SmsNotificationSender;
use App\Services\PushNotificationSender; // Nuova implementazione
use Illuminate\Support\Facades\Auth;     // Per logica basata sull'utente
use Illuminate\Support\Facades\Config;   // Per fallback a configurazione

public function register(): void
{
    $this->app->singleton(NotificationSenderInterface::class, function ($app) {
        $user = Auth::user(); // Potrebbe essere null (es. in comandi Artisan)

        // Logica di selezione dinamica
        // Esempio 1: Preferenza utente
        if ($user && $user->notification_preference === 'sms') {
            return $app->make(SmsNotificationSender::class);
        }

        // Esempio 2: Feature specifica per un gruppo di utenti (potrebbe usare Pennant)
        if ($user && $user->can_receive_push_notifications) { // Assumendo un permesso o attributo
            // if (Feature::for($user)->active('use-push-notifications')) { // Ancora meglio con Pennant
            //     return $app->make(PushNotificationSender::class);
            // }
        }
        
        // Esempio 3: Fallback a configurazione globale
        $defaultChannel = Config::get('services.notification_channel.default', 'email');
        if ($defaultChannel === 'sms') {
            return $app->make(SmsNotificationSender::class);
        } elseif ($defaultChannel === 'push') {
            return $app->make(PushNotificationSender::class);
        }

        return $app->make(EmailNotificationSender::class); // Default
    });
}
  • Vantaggi: La logica di selezione è eseguita a runtime quando il servizio è necessario. Permette una flessibilità notevole.
  • Svantaggi: Se la logica nella closure diventa molto complessa, il Service Provider può ancora appesantirsi. La testabilità di questa logica interna alla closure richiede di mockare le condizioni (es. Auth::setUser(), Config::set()).

2. Refactoring chiave: integrazione con Laravel Pennant per selezione guidata da Feature Flag

Per una gestione veramente dinamica e disaccoppiata, specialmente per il rollout graduale di nuove implementazioni o per A/B testing di backend, l'integrazione con Laravel Pennant (introdotto in L10, maturo in L11/L12) è la soluzione più elegante. Una delle modalità più efficaci per guidare questa selezione dinamica è, appunto, l'utilizzo di feature flag, un argomento che abbiamo approfondito parlando di Laravel Pennant e la modernizzazione della gestione delle feature.

Scenario: Vogliamo introdurre una nuova AdvancedPricingEngine per il nostro e-commerce, ma solo per un gruppo selezionato di utenti o quando una feature flag è attiva.

1. Definire le Implementazioni e l'Interfaccia:

// app/Contracts/PricingEngineInterface.php
namespace App\Contracts;
interface PricingEngineInterface { public function calculatePrice(Product $product, User $user = null): float; }

// app/Services/StandardPricingEngine.php
namespace App\Services;
use App\Contracts\PricingEngineInterface; use App\Models\Product; use App\Models\User;
class StandardPricingEngine implements PricingEngineInterface {
    public function calculatePrice(Product $product, User $user = null): float { /_... logica standard ..._/ return $product->base_price; }
}

// app/Services/AdvancedPricingEngine.php
namespace App\Services;
use App\Contracts\PricingEngineInterface; use App\Models\Product; use App\Models\User;
class AdvancedPricingEngine implements PricingEngineInterface {
    public function calculatePrice(Product $product, User $user = null): float { /_... logica avanzata, sconti personalizzati, etc. ..._/ return $product->base_price * 0.9; }
}

2. Definire la Feature Flag con Pennant:

// In un ServiceProvider (es. PennantServiceProvider o AppServiceProvider)
use Laravel\Pennant\Feature;
use App\Models\User; // Per lo scope utente

public function boot(): void // O register, a seconda di quando serve la definizione
{
    Feature::define('use-advanced-pricing', function (User $user = null) {
        // Logica per determinare se l'utente deve usare il motore avanzato
        // Potrebbe essere basata su un attributo dell'utente, un segmento, ecc.
        // O potrebbe essere una flag globale attivata/disattivata dal database.
        if ($user) {
            return $user->is_beta_tester || $user->hasSubscribedToPremiumPlan();
        }
        // Fallback per contesti senza utente (es. prezzi base per ospiti)
        // o per una flag globale attivabile via DB.
        // In questo caso, se non c'è utente, diciamo di non usare l'avanzato.
        return false;
        // Se la flag 'use-advanced-pricing' è attivata nel DB per lo scope nullo (globale),
        // quella definizione prevarrebbe su questa closure per le chiamate senza scope.
    });
}

3. Binding Dinamico nel Service Provider usando la Feature Flag:

// app/Providers/AppServiceProvider.php
use App\Contracts\PricingEngineInterface;
use App\Services\StandardPricingEngine;
use App\Services\AdvancedPricingEngine;
use Laravel\Pennant\Feature;
use Illuminate\Support\Facades\Auth; // O un altro modo per ottenere lo scope

public function register(): void
{
    $this->app->singleton(PricingEngineInterface::class, function ($app) {
        // Lo scope per Feature::active() di default è l'utente autenticato.
        // Se sei in un contesto CLI o non-utente, dovrai passare esplicitamente lo scope
        // o definire la feature in modo che Pennant possa risolverla senza scope.
        $user = Auth::user(); // Ottieni l'utente corrente, se presente

        if (Feature::for($user)->active('use-advanced-pricing')) {
        // Oppure, se la feature è globale e non dipende dall'utente:
        // if (Feature::active('use-advanced-pricing-globally')) {
            return $app->make(AdvancedPricingEngine::class);
        }
        return $app->make(StandardPricingEngine::class);
    });
}

Con questo setup, il Service Container risolverà automaticamente l'implementazione corretta di PricingEngineInterface in base allo stato della feature flag use-advanced-pricing per l'utente corrente (o per lo scope fornito).

4. Binding Contestuali Avanzati (già presenti, ma potenti in combinazione)

Il metodo when()->needs()->give() può essere usato per fornire implementazioni diverse della stessa interfaccia a classi consumer differenti, indipendentemente dalle feature flag. Questo è utile se, ad esempio, AdminController necessita sempre di DetailedLogger, mentre UserController necessita di SimpleLogger.

// app/Providers/AppServiceProvider.php
use App\Contracts\LoggerInterface;
use App\Services\Logging\DetailedLogger;
use App\Services\Logging\SimpleLogger;
use App\Http\Controllers\AdminController;
use App\Http\Controllers\UserController;

public function register(): void
{
    $this->app->when(AdminController::class)
              ->needs(LoggerInterface::class)
              ->give(DetailedLogger::class);

    $this->app->when(UserController::class)
              ->needs(LoggerInterface::class)
              ->give(SimpleLogger::class);
    
    // Potresti avere un binding di default per altri contesti
    // $this->app->bind(LoggerInterface::class, SimpleLogger::class);
}

Testare la Selezione Dinamica dei Servizi

Testare che il Service Container risolva l'implementazione corretta è cruciale.

  • Per la selezione basata su config() (approccio L9/L10):

      // tests/Feature/DynamicServiceViaConfigTest.php
      public function test_paypal_gateway_is_resolved_when_config_is_paypal(): void
      {
          config(['services.payment_gateway.driver' => 'paypal']);
          // Potrebbe essere necessario forzare il re-register del provider o pulire le istanze
          // $this->app->forgetInstance(PaymentGatewayInterface::class);
          // $this->app->register(AppServiceProvider::class, true); // Forza re-registrazione
    
          $gateway = $this->app->make(PaymentGatewayInterface::class);
          $this->assertInstanceOf(PayPalPaymentGateway::class, $gateway);
      }
  • Per la selezione basata su Laravel Pennant:

      // tests/Feature/DynamicPricingEngineTest.php
      use App\Contracts\PricingEngineInterface;
      use App\Services\StandardPricingEngine;
      use App\Services\AdvancedPricingEngine;
      use App\Models\User;
      use Laravel\Pennant\Feature;
      use Illuminate\Foundation\Testing\RefreshDatabase; // Se necessario
      use Tests\TestCase;
    
      class DynamicPricingEngineTest extends TestCase
      {
          use RefreshDatabase;
    
          protected function setUp(): void
          {
              parent::setUp();
              // Assicurati che le feature siano definite per i test,
              // o usa Feature::fake() per controllarle esplicitamente.
              // Qui assumiamo che siano definite nel PennantServiceProvider
              // e che la logica di `use-advanced-pricing` dipenda dall'ID utente 1.
              Feature::define('use-advanced-pricing', fn (User $user = null) => $user && $user->id === 1);
          }
    
          public function test_standard_pricing_engine_for_normal_user(): void
          {
              $user = User::factory()->create(['id' => 2]);
              $this->actingAs($user);
    
              // Feature::fake(); // Inizia con tutto disattivato
              // Feature::for($user)->deactivate('use-advanced-pricing'); // Assicura sia disattiva
    
              $engine = $this->app->make(PricingEngineInterface::class);
              $this->assertInstanceOf(StandardPricingEngine::class, $engine);
          }
    
          public function test_advanced_pricing_engine_for_beta_user(): void
          {
              $user = User::factory()->create(['id' => 1]); // Utente che dovrebbe avere la feature
              $this->actingAs($user);
    
              // Feature::fake();
              // Feature::for($user)->activate('use-advanced-pricing'); // Attiva specificamente
    
              $engine = $this->app->make(PricingEngineInterface::class);
              $this->assertInstanceOf(AdvancedPricingEngine::class, $engine);
          }
    
          public function test_global_feature_flag_selects_paypal_gateway(): void
          {
              // Definisci o simula la feature flag globale
              Feature::define('paypal-checkout-active'); // La closure di default è `false`
                                                         // a meno che non sia attivata da DB
              Feature::activate('paypal-checkout-active'); // Attiva globalmente per il test
    
              $gateway = $this->app->make(PaymentGatewayInterface::class); // Assumendo il binding come nell'esempio precedente
              $this->assertInstanceOf(PayPalPaymentGateway::class, $gateway);
    
              Feature::deactivate('paypal-checkout-active'); // Pulisci per altri test
              // Forza la re-registrazione o pulisci l'istanza se il binding è singleton e già risolto
              $this->app->forgetInstance(PaymentGatewayInterface::class);
              // $this->app->register(AppServiceProvider::class, true); // se il binding è in AppServiceProvider
    
              $gatewayDefault = $this->app->make(PaymentGatewayInterface::class);
              $this->assertInstanceOf(StripePaymentGateway::class, $gatewayDefault); // Verifica fallback
          }
      }

Benefici del Refactoring per la tua Impresa

Modernizzare la selezione dinamica dei servizi porta a:

  • Maggiore Flessibilità Architetturale: capacità di adattare l'applicazione a diversi scenari senza modifiche invasive.
  • Facilità di A/B Testing: testa diverse implementazioni di una funzionalità su segmenti di utenti.
  • Rollout Graduale di Nuove Funzionalità: introduci nuove versioni di servizi in modo controllato.
  • Codice più Pulito e Aderente ai Principi SOLID: la logica di selezione è ben definita e il Service Container gestisce la creazione delle istanze.
  • Manutenibilità e Testabilità Migliorate: ogni implementazione e la logica di selezione possono essere testate in isolamento.

Il ruolo del programmatore Laravel esperto

Progettare un'architettura che supporti efficacemente la selezione dinamica dei servizi, specialmente in un'applicazione Laravel esistente di una certa complessità, richiede una profonda comprensione del Service Container, dei design pattern e delle strategie di refactoring. Come sviluppatore Laravel con esperienza ventennale, posso aiutare la tua impresa a:

  • Analizzare l'architettura attuale e identificare le aree che beneficerebbero di una maggiore flessibilità.
  • Progettare e implementare pattern di selezione dinamica robusti e testabili.
  • Integrare Laravel Pennant o altre strategie per la gestione dinamica.
  • Guidare il refactoring del codice esistente.

La mia filosofia è creare soluzioni che non solo risolvano i problemi odierni, ma che preparino la tua applicazione per le sfide future. Scopri di più sul mio approccio nella pagina Chi Sono.

La flessibilità architetturale non è un lusso, ma una necessità per le applicazioni aziendali che devono evolvere rapidamente. Un refactoring mirato della selezione dei servizi in Laravel 12 può fare una grande differenza.

Se la tua impresa ha bisogno di rendere le proprie applicazioni Laravel più adattive e manutenibili, contattami per una consulenza approfondita e valutiamo insieme la strategia migliore.

Ultima modifica: Lunedì 24 Febbraio 2025, alle 08:13

Nello sviluppo di applicazioni aziendali complesse e in continua evoluzione, la capacità di rilasciare nuove funzionalità in modo controllato e graduale è fondamentale. Le feature flag (o feature toggle) sono uno strumento potente che permette agli team di sviluppo di abilitare o disabilitare specifiche funzionalità di un'applicazione dinamicamente, senza dover effettuare un nuovo deployment del codice. Questo approccio facilita strategie come i rilasci canary, l'A/B testing, l'accesso anticipato per gruppi di utenti selezionati (beta tester) e la gestione del rollout progressivo.

Tuttavia, se la gestione di queste feature flag non è ben strutturata, può rapidamente trasformarsi in un incubo di conditional logic sparsa per il codice, rendendo l'applicazione difficile da mantenere e testare. Molte applicazioni Laravel sviluppate con versioni come Laravel 9 o le prime iterazioni di Laravel 10 (prima della piena adozione di soluzioni dedicate) potrebbero utilizzare approcci custom o molto basilari per le feature flag. Con l'introduzione di Laravel Pennant in Laravel 10 (e la sua piena compatibilità e raccomandazione per l'uso in un contesto Laravel 12), il framework offre ora una soluzione first-party elegante, potente e ben integrata.

In questo articolo tecnico, esploreremo come effettuare un refactoring della gestione delle feature flag, passando da approcci custom più datati a Laravel Pennant, con esempi di codice concreti per illustrare il processo e i benefici per le applicazioni della tua impresa.

Se vuoi approfondire, continua a leggere. Se hai una domanda specifica a riguardo di questo articolo, contattami per una consulenza dedicata. Dai anche un'occhiata al mio profilo per capire come posso aiutare concretamente la tua azienda o startup a crescere e a modernizzarsi.

Scenario pre-Pennant: gestione "artigianale" delle feature flag in Laravel 9/10

Prima di Laravel Pennant, o in progetti che non ne hanno ancora adottato l'uso, la gestione delle feature flag poteva avvenire in diversi modi, spesso con soluzioni "fatte in casa".

Approccio 1: Variabili d'ambiente e config()

Un metodo semplice e comune era quello di utilizzare variabili d'ambiente e file di configurazione.

1. Definizione nel file .env:

ENABLE_NEW_USER_DASHBOARD=true
ENABLE_ADVANCED_REPORTING=false

2. Creazione di un file di configurazione config/features.php:

// config/features.php
return [
    'new_user_dashboard' => env('ENABLE_NEW_USER_DASHBOARD', false),
    'advanced_reporting' => env('ENABLE_ADVANCED_REPORTING', false),
    // ... altre feature
];

3. Verifica nel codice (Controller, Service, Blade): In un Controller:

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;

class DashboardController extends Controller
{
    public function index()
    {
        if (Config::get('features.new_user_dashboard')) {
            return view('dashboards.new');
        }
        return view('dashboards.old');
    }
}

In una Blade view:

{{-- resources/views/components/layout.blade.php --}}
@if(config('features.advanced_reporting'))
    <li class="nav-item">
        <a href="{{ route('reports.advanced') }}" class="nav-link">Report Avanzati</a>
    </li>
@endif

Limiti di questo approccio:

  • Poco dinamico: per modificare lo stato di una flag, è necessario cambiare il file .env (o il file di config se non si usa env() direttamente) e, in molti casi, effettuare un nuovo deployment o svuotare la cache di configurazione (php artisan config:clear).
  • Difficoltà di segmentazione: è difficile abilitare una feature solo per specifici utenti, gruppi di utenti, o per una percentuale del traffico senza aggiungere logica custom complessa.
  • Gestione centralizzata limitata: con molte flag, i file di configurazione possono diventare disordinati.

Approccio 2: Tabella Database Custom

Un approccio più dinamico prevede l'uso di una tabella nel database per memorizzare lo stato delle feature flag.

1. Migrazione per la tabella feature_flags:

// database/migrations/xxxx_xx_xx_xxxxxx_create_feature_flags_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('feature_flags', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique(); // es. 'new-user-dashboard'
            $table->text('description')->nullable();
            $table->boolean('is_active')->default(false);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('feature_flags');
    }
};

2. Model FeatureFlag:

// app/Models/FeatureFlag.php
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class FeatureFlag extends Model
{
    use HasFactory;
    protected $fillable = ['name', 'description', 'is_active'];
    protected $casts = ['is_active' => 'boolean'];
}

3. Un FeatureService custom (esempio basilare):

// app/Services/FeatureService.php
namespace App\Services;

use App\Models\FeatureFlag;
use Illuminate\Support\Facades\Cache; // Per un caching semplice

class FeatureService
{
    /**
     * Controlla se una feature flag è attiva.
     *
     * @param string $featureName
     * @param \App\Models\User|null $user // Per future estensioni (segmentazione)
     * @return bool
     */
    public function isActive(string $featureName, $user = null): bool
    {
        // Tenta di recuperare dalla cache per N minuti
        return Cache::remember("feature_flag_{$featureName}", now()->addMinutes(5), function () use ($featureName) {
            $flag = FeatureFlag::where('name', $featureName)->first();
            return $flag ? $flag->is_active : false;
        });
        // Nota: una logica più complessa potrebbe considerare l'utente per la segmentazione
    }
}

4. Utilizzo nel codice:

// In un Controller, iniettando il servizio
use App\Services\FeatureService;

protected FeatureService $featureService;

public function __construct(FeatureService $featureService)
{
    $this->featureService = $featureService;
}

public function showUserProfile()
{
    if ($this->featureService->isActive('user-profile-v2', auth()->user())) {
        // ... mostra il nuovo profilo
    }
    // ... mostra il vecchio profilo
}

Limiti di questo approccio custom:

  • Boilerplate: bisogna scrivere e mantenere il codice per la migrazione, il model, il servizio, e un'eventuale interfaccia di gestione (un CRUD per le flag).
  • Gestione del caching: implementare una strategia di caching efficiente e con una corretta invalidazione può essere complicato.
  • Testabilità: testare la logica condizionale basata su queste flag custom richiede un setup aggiuntivo.
  • Mancanza di standardizzazione: ogni progetto potrebbe implementare la logica in modo leggermente diverso.
  • Complessità per funzionalità avanzate: implementare rilasci percentuali, scope per tenant in applicazioni multi-tenancy, o flag che dipendono da più fattori diventa rapidamente molto complesso.

L'avvento di Laravel Pennant: una soluzione integrata e potente

Laravel Pennant, introdotto ufficialmente in Laravel 10, fornisce una soluzione first-party per la gestione delle feature flag, progettata per essere semplice, flessibile e ben integrata con il framework. È la scelta ideale per modernizzare la gestione delle feature in vista di un aggiornamento a Laravel 12.

Installazione e Configurazione di Base

L'installazione è semplice tramite Composer:

composer require laravel/pennant

Successivamente, pubblica la configurazione e le migrazioni (se intendi usare il driver database):

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

Questo creerà una tabella features nel tuo database per memorizzare lo stato delle flag quando si utilizza il driver database.

Pennant si registra automaticamente tramite il suo Service Provider. Il driver di default è array, che memorizza lo stato delle flag in memoria per la durata della richiesta (utile per i test o per flag definite interamente nel codice), ma per una gestione dinamica, il driver database è più indicato. Puoi configurare il driver di default nel file config/pennant.php o tramite la variabile d'ambiente PENNANT_STORE.

Refactoring verso Laravel Pennant in un Contesto Laravel 12

Vediamo ora come modernizzare gli approcci precedenti utilizzando Laravel Pennant.

1. Definizione delle Feature Flag

Le feature flag con Pennant vengono tipicamente definite nel metodo boot di un Service Provider (es. App\Providers\PennantServiceProvider.php, che puoi creare, o anche in AppServiceProvider). La definizione avviene tramite una closure che determina se la feature è attiva. Questa closure può accettare un scope (solitamente l'utente autenticato, ma può essere qualsiasi oggetto).

// app/Providers/PennantServiceProvider.php (o AppServiceProvider)
namespace App\Providers;

use App\Models\User;
use App\Models\Tenant; // Esempio per multi-tenancy
use Illuminate\Support\Facades\Gate; // Per eventuali controlli di permessi
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
use Illuminate\Support\Lottery; // Per rilasci percentuali

class PennantServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Feature semplice, sempre attiva per ora (può essere cambiata da DB)
        Feature::define('new-api-integration');

        // Feature basata sullo stato dell'utente
        Feature::define('new-user-dashboard', function (User $user) {
            // Abilita solo per utenti con ID specifico o con un certo ruolo
            return $user->id === 1 || $user->hasRole('beta-tester');
        });

        // Feature con scope personalizzato (es. per un tenant specifico)
        Feature::define('beta-feature-for-tenant', function (Tenant $tenant) {
            return $tenant->isEarlyAdopter();
        });

        // Feature con rollout percentuale (es. 25% degli accessi)
        // La closure non riceve lo scope, quindi è globale per tutti gli utenti
        // a meno che non si combini con un'ulteriore logica di scope esternamente.
        Feature::define('gradual-rollout-experiment', function () {
            return Lottery::odds(1, 4); // 1 su 4 possibilità (25%)
        });

        // Feature che dipende da un'altra configurazione o permesso
        Feature::define('advanced-reporting', function (User $user = null) {
            // Se l'utente è admin OPPURE se una config specifica è attiva
            // (il null per $user gestisce casi non autenticati se la feature può essere globale)
            return ($user && $user->isAdmin()) || config('app.enable_global_reports');
        });
    }
}

Importante: Registra questo PennantServiceProvider nel tuo config/app.php nella sezione providers se lo hai creato appositamente. Se usi AppServiceProvider, è già registrato.

2. Verifica dello Stato delle Feature Flag

Pennant offre un'API fluente e direttive Blade per verificare lo stato delle flag.

Verifica nel codice PHP (Controller, Service, etc.):

use Laravel\Pennant\Feature;
use App\Models\User;
use App\Models\Tenant;

// ...

// Per feature senza scope specifico (o con scope di default: utente autenticato)
if (Feature::active('new-user-dashboard')) {
    // Mostra la nuova dashboard per l'utente corrente
}

if (Feature::inactive('advanced-reporting')) {
    // Logica se il reporting avanzato NON è attivo
}

// Esecuzione di closure condizionali
Feature::when('new-api-integration',
    fn () => logger('Eseguendo logica per nuova integrazione API...'),
    fn () => logger('Nuova integrazione API non attiva, usando fallback.')
);

// Per feature con scope personalizzato
$user = User::find(123);
$tenant = Tenant::find(45);

if (Feature::for($user)->active('new-user-dashboard')) {
    // La nuova dashboard è attiva per l'utente con ID 123
}

if (Feature::for($tenant)->active('beta-feature-for-tenant')) {
    // La feature beta è attiva per il tenant 45
}

// Recuperare tutte le feature attive per uno scope
$activeFeaturesForUser = Feature::for($user)->active(); // Ritorna un array di nomi di feature

// Verificare più feature contemporaneamente
if (Feature::allAreActive(['new-user-dashboard', 'advanced-reporting'])) {
    // ...
}

if (Feature::someAreActive(['new-user-dashboard', 'gradual-rollout-experiment'])) {
    // ...
}

Utilizzo nelle viste Blade:

{{-- Nuova dashboard --}}
@feature('new-user-dashboard')
    @include('dashboards.new_version_content')
@else
    @include('dashboards.old_version_content')
@endfeature

{{-- Reporting avanzato (mostra solo se attivo, senza else) --}}
@feature('advanced-reporting')
    <a href="{{ route('reports.advancedPage') }}">Visualizza Report Avanzati</a>
@endfeature

{{-- Per uno scope diverso dall'utente autenticato (es. un tenant specifico) --}}
@feature('beta-feature-for-tenant', $specificTenant)
    <p>Funzionalità Beta specifica per il tenant {{ $specificTenant->name }} è ATTIVA!</p>
@endfeature

3. Gestione Dinamica (con Driver database)

Se usi il driver database, puoi attivare o disattivare le feature (e gestire i loro valori per specifici scope) direttamente nel database o tramite comandi Artisan o un'interfaccia di amministrazione che potresti costruire.

Pennant fornisce comandi Artisan per gestire le feature quando si usa il database driver:

  • php artisan pennant:activate <feature-name> [<scope>]
  • php artisan pennant:deactivate <feature-name> [<scope>]
  • php artisan pennant:purge [<feature-name>] (rimuove le feature non più definite nel codice dal DB)

Esempio:

# Attiva 'new-user-dashboard' globalmente (se la sua closure di definizione lo permette)
php artisan pennant:activate new-user-dashboard

# Attiva 'new-user-dashboard' specificamente per l'utente con ID 1
php artisan pennant:activate new-user-dashboard App\\Models\\User:1

# Disattiva 'advanced-reporting' per l'utente con ID 1
php artisan pennant:deactivate advanced-reporting App\\Models\\User:1

Quando una feature è attivata o disattivata nel database per uno specifico scope, questo sovrascrive la logica definita nella closure per quello scope. Se non c'è una voce nel database per lo scope corrente, allora viene eseguita la closure di definizione.

4. Testing con Pennant

Pennant rende il testing della logica condizionale molto semplice.

// tests/Feature/DashboardTest.php
namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Pennant\Feature;
use Tests\TestCase;

class DashboardTest extends TestCase
{
    use RefreshDatabase;

    public function test_old_dashboard_is_shown_when_feature_is_inactive(): void
    {
        $user = User::factory()->create();
        Feature::fake(); // Inizia con tutte le feature disattivate o come definite se non fakeate
        // O specificamente: Feature::deactivate('new-user-dashboard');

        $this->actingAs($user)
             ->get('/dashboard')
             ->assertSee('Vecchia Dashboard'); // Assumendo che il testo sia presente
    }

    public function test_new_dashboard_is_shown_when_feature_is_active(): void
    {
        $user = User::factory()->create();
        Feature::fake();
        Feature::activate('new-user-dashboard'); // Attiva la feature per questo test

        $this->actingAs($user)
             ->get('/dashboard')
             ->assertSee('Nuova Dashboard Super Moderna');
    }

    public function test_feature_can_be_scoped_for_user_in_tests(): void
    {
        $userSpecific = User::factory()->create();
        $otherUser = User::factory()->create();

        Feature::fake();
        // Attiva solo per $userSpecific
        Feature::for($userSpecific)->activate('new-user-dashboard');

        $this->actingAs($userSpecific)->get('/dashboard')->assertSee('Nuova Dashboard');
        $this->actingAs($otherUser)->get('/dashboard')->assertSee('Vecchia Dashboard');
    }
}

Vantaggi del Refactoring a Pennant per Applicazioni Aziendali

Adottare Laravel Pennant per la gestione delle feature flag in un'applicazione Laravel 9/10 in evoluzione verso Laravel 12 porta numerosi vantaggi:

  • Standardizzazione e Manutenibilità: Utilizzare una soluzione first-party riduce il codice custom da mantenere e garantisce un approccio standardizzato, più facile da comprendere per i nuovi membri del team.
  • Maggiore Flessibilità e Dinamicità: La possibilità di definire flag con logica complessa (basata su utente, tenant, percentuale) e di gestirne lo stato dinamicamente (tramite DB o comandi Artisan) offre un controllo granulare sui rilasci.
  • Testabilità Migliorata: L'API di testing di Pennant semplifica enormemente la scrittura di test per la logica condizionale.
  • Integrazione con l'Ecosistema Laravel: Essendo parte di Laravel, si integra perfettamente con il resto del framework.
  • Codice più Pulito: Riduce la necessità di if/else sparsi basati su config() o servizi custom, centralizzando la logica delle feature flag.
  • Supporto a Strategie di Rilascio Avanzate: Facilita dark launches (rilascio di codice in produzione disattivato), canary releases, e A/B testing.

Il Ruolo del Programmatore Laravel Esperto

Effettuare il refactoring da un sistema di feature flag custom a Laravel Pennant, specialmente in un'applicazione aziendale esistente e complessa, richiede competenza. Un programmatore Laravel esperto può:

  • Analizzare l'attuale implementazione delle feature flag e i suoi limiti.
  • Pianificare la migrazione a Pennant minimizzando i rischi.
  • Riscrivere la logica di definizione e verifica delle flag in modo ottimale.
  • Configurare correttamente i driver e gli scope.
  • Assicurare che i test esistenti vengano aggiornati e che ne vengano scritti di nuovi per coprire la logica di Pennant.
  • Formare il team sull'utilizzo efficace del nuovo sistema.

Investire in questa modernizzazione, guidati da un professionista, significa rendere la tua applicazione Laravel più robusta, agile e pronta per le future evoluzioni del tuo business. Per comprendere come la mia esperienza ventennale possa aiutarti in questo tipo di transizioni tecnologiche, puoi consultare la pagina Chi Sono.

Laravel Pennant non è solo un "altro pacchetto"; è uno strumento strategico che, se ben implementato, può trasformare il modo in cui la tua impresa gestisce il ciclo di vita delle funzionalità software.

Se la tua applicazione Laravel necessita di una gestione delle feature flag più moderna e potente, o se stai pianificando un aggiornamento verso Laravel 12 e vuoi sfruttare al meglio le sue potenzialità, contattami per discutere di come posso assisterti.

Ultima modifica: Mercoledì 12 Febbraio 2025, alle 09:25