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 usaenv()
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 suconfig()
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