Riepilogo post nella categoria Best Practice PHP

La validazione dei dati in input è una pietra miliare nella costruzione di applicazioni web sicure e robuste. In Laravel, il sistema di validazione è estremamente potente e flessibile, offrendo una vasta gamma di regole predefinite e diversi modi per definire logiche custom. Tuttavia, in applicazioni Laravel 9 o Laravel 10 con una certa storia o complessità, è facile che le logiche di validazione più specifiche del business finiscano per essere implementate tramite closure all'interno delle Form Request o dei controller, oppure come metodi custom sparsi qua e là. Sebbene questi approcci funzionino, possono portare a codice meno leggibile, difficilmente riutilizzabile e complicato da testare in isolamento.

Per le imprese che puntano a un codice di alta qualità, manutenibile e scalabile in un contesto Laravel 12, è il momento di considerare un refactoring strategico verso l'uso estensivo dei Rule Objects dedicati. Questo approccio, sebbene i Rule Objects esistano in Laravel da versioni precedenti, rappresenta una best practice per incapsulare logiche di validazione complesse in classi specifiche, promuovendo i principi SOLID (in particolare la Single Responsibility Principle - SRP) e migliorando drasticamente la testabilità.

Questo articolo tecnico ti guiderà, con abbondanti esempi di codice, attraverso il processo di identificazione delle logiche di validazione "legacy" e il loro refactoring in Rule Objects riutilizzabili.

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.

Validazione in Laravel 9/10: approcci comuni (e i loro limiti)

Prima di vedere come modernizzare, riconosciamo alcuni pattern di validazione comuni in applicazioni Laravel 9/10 che potrebbero beneficiare di un refactoring.

1. Validazione nel Controller

Per validazioni semplici o in endpoint specifici, è comune trovare la logica direttamente nel controller utilizzando il metodo validate dell'oggetto $request.

// Esempio: app/Http/Controllers/LegacyProductController.php (Laravel 9/10 style)
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; // Per una regola custom che interroga il DB
use Illuminate\Validation\Rule;   // Per regole più complesse come 'unique'

class LegacyProductController extends Controller
{
    public function store(Request $request)
    {
        $validatedData = $request->validate([
            'name' => 'required|string|max:255',
            'sku' => [
                'required',
                'string',
                'alpha_dash',
                Rule::unique('products', 'sku')->where(function ($query) use ($request) {
                    // Esempio di unicità condizionale basata su un altro campo
                    return $query->where('tenant_id', $request->user()->tenant_id);
                }),
            ],
            'price' => 'required|numeric|min:0',
            'custom_code' => [
                'nullable',
                'string',
                function (string $attribute, mixed $value, \Closure $fail) {
                    // Logica di validazione custom inline tramite closure
                    if (strtoupper(substr($value, 0, 2)) !== 'CC') {
                        $fail('Il campo :attribute deve iniziare con "CC".');
                    }
                    if (strlen($value) < 5) {
                        $fail('Il campo :attribute deve essere lungo almeno 5 caratteri.');
                    }
                }
            ],
        ]);

        // ... logica per creare il prodotto ...
        return response()->json(['message' => 'Prodotto creato con successo!'], 201);
    }
}

Problemi di questo approccio:

  • Controller "gonfi": i controller dovrebbero orchestrare, non contenere logica di business complessa come la validazione.
  • Non riutilizzabile: se la stessa logica per custom_code o la regola sku unica per tenant serve altrove, bisogna duplicarla.
  • Difficile da testare isolatamente: testare la logica della closure richiede di effettuare una richiesta HTTP completa al controller.

2. Validazione nelle Form Request

Le Form Request sono un ottimo modo per separare la logica di validazione (e autorizzazione) dai controller. Tuttavia, anche qui, logiche complesse possono finire in closure all'interno del metodo rules().

// Esempio: app/Http/Requests/StoreProductFormRequest.php (Laravel 9/10 style)
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;

class StoreProductFormRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // O $this->user()->can('create', Product::class);
    }

    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'sku' => [
                'required',
                'string',
                'alpha_dash',
                Rule::unique('products', 'sku')->where(function ($query) {
                    return $query->where('tenant_id', $this->user()->tenant_id);
                })->ignore($this->product?->id), // Gestione per l'update
            ],
            'price' => 'required|numeric|min:0',
            'complex_field' => [
                'required',
                'string',
                function (string $attribute, mixed $value, \Closure $fail) {
                    // Immagina una logica complessa qui:
                    // 1. Chiama un servizio esterno per validare 'value'
                    // 2. Controlla 'value' contro altri campi del $this->input()
                    // 3. Esegue una query DB specifica
                    if (strlen($value) < 10) {
                        $fail('Il campo :attribute è troppo corto.');
                    }
                    if (!preg_match('/[A-Z]/', $value) || !preg_match('/[0-9]/', $value)) {
                        $fail('Il campo :attribute deve contenere almeno una maiuscola e un numero.');
                    }
                    // ... e così via
                }
            ],
        ];
    }
}

Limiti (quando si abusa delle closure per logiche complesse):

  • FormRequest verbosa: può diventare difficile da leggere se contiene molte closure lunghe.
  • Testabilità della logica interna alla closure: ancora legata al contesto della FormRequest.
  • Riutilizzabilità limitata: se la logica di complex_field serve per un altro attributo o in un'altra FormRequest, si tende a duplicare.

Verso Laravel 12: la potenza e la chiarezza dei Rule Objects dedicati

Un Rule Object è una semplice classe PHP che incapsula la logica per validare un singolo attributo. Questo approccio promuove codice pulito, riutilizzabile e facilmente testabile. Laravel supporta i Rule Objects da diverse versioni, ma il loro uso sistematico per il refactoring di logiche complesse è una best practice per applicazioni Laravel 12 moderne.

Interfacce coinvolte:

  • Illuminate\Contracts\Validation\Rule: l'interfaccia "classica", richiede metodi passes($attribute, $value) e message().
  • Illuminate\Contracts\Validation\ValidationRule: introdotta in Laravel 10, più moderna, richiede un singolo metodo validate(string $attribute, mixed $value, Closure $fail). Il messaggio di fallimento viene gestito tramite il callback $fail. Questa è spesso preferita oggi.
  • Illuminate\Contracts\Validation\InvokableRule: una regola semplice che implementa solo il metodo __invoke(string $attribute, mixed $value, Closure $fail).
  • Illuminate\Contracts\Validation\DataAwareRule: permette al Rule Object di accedere a tutti gli altri dati della richiesta in validazione, utile per regole dipendenti.
  • Illuminate\Contracts\Validation\ValidatorAwareRule: permette al Rule Object di accedere all'istanza del validatore.

Guida pratica al refactoring: creare e usare Rule Objects

Passo 1: Identificare logiche di validazione complesse o ripetute Analizza i tuoi controller e le tue Form Request. Ci sono closure di validazione lunghe? Regole che si ripetono? Logiche che richiedono query al database o interazioni con altri servizi? Questi sono ottimi candidati per il refactoring in Rule Objects.

Passo 2: Creare un Rule Object con php artisan make:rule Laravel fornisce un comodo comando Artisan per generare lo stub di un Rule Object. Di default, crea una classe che implementa ValidationRule.

php artisan make:rule IsValidBusinessRegistrationNumber
php artisan make:rule UniqueProductNameInCategory

Passo 3: Implementare la logica di validazione nel Rule Object

Esempio 1: IsValidBusinessRegistrationNumber (refactoring di custom_code dall'esempio controller)

// app/Rules/IsValidBusinessRegistrationNumber.php
namespace App\Rules;

use Illuminate\Contracts\Validation\ValidationRule;
use Closure; // Per il callback di fallimento $fail

class IsValidBusinessRegistrationNumber implements ValidationRule
{
    protected string $prefix;
    protected int $minLength;

    /**
     * Crea una nuova istanza della regola.
     * Possiamo passare parametri al costruttore per rendere la regola più flessibile.
     */
    public function __construct(string $prefix = 'CC', int $minLength = 5)
    {
        $this->prefix = $prefix;
        $this->minLength = $minLength;
    }

    /**
     * Esegue la regola di validazione.
     *
     * @param  \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString  $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (!is_string($value)) {
            // È buona norma controllare il tipo, anche se Laravel di solito lo fa prima
            // con regole come 'string'.
            $fail("Il campo :attribute fornito non è una stringa valida.");
            return;
        }

        if (strtoupper(substr($value, 0, strlen($this->prefix))) !== strtoupper($this->prefix)) {
            $fail("Il campo :attribute deve iniziare con \"{$this->prefix}\".");
        }

        if (strlen($value) < $this->minLength) {
            $fail("Il campo :attribute deve essere lungo almeno {$this->minLength} caratteri.");
        }

        // Qui potresti aggiungere logiche più complesse,
        // ad esempio un checksum o una chiamata a un servizio esterno (con cautela per le performance).
        // if (!$this->externalCheckService->isValid($value)) {
        //     $fail('Il codice di registrazione fornito non è valido secondo il nostro sistema esterno.');
        // }
    }
}

Esempio 2: UniqueProductNameInCategory (refactoring della logica SKU unico per tenant/categoria) Questo Rule Object avrà bisogno di accedere ad altri dati della richiesta (la categoria), quindi implementerà DataAwareRule.

// app/Rules/UniqueProductNameInCategory.php
namespace App\Rules;

use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Contracts\Validation\DataAwareRule; // Per accedere ad altri dati
use Illuminate\Support\Facades\DB;
use Closure;

class UniqueProductNameInCategory implements ValidationRule, DataAwareRule
{
    protected array $data = []; // Qui verranno iniettati tutti i dati della richiesta
    protected ?int $ignoreId = null;
    protected string $categoryFieldName;

    /**
     * @param int|null $ignoreId L'ID del record da ignorare (utile per gli update)
     * @param string $categoryFieldName Il nome del campo nella richiesta che contiene l'ID/nome della categoria
     */
    public function __construct(?int $ignoreId = null, string $categoryFieldName = 'category_id')
    {
        $this->ignoreId = $ignoreId;
        $this->categoryFieldName = $categoryFieldName;
    }

    /**
     * Imposta i dati aggiuntivi per la validazione.
     */
    public function setData(array $data): static
    {
        $this->data = $data;
        return $this;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $productName = (string) $value;
        $categoryId = $this->data[$this->categoryFieldName] ?? null;

        if (is_null($categoryId)) {
            // Potremmo decidere di non validare se la categoria non è fornita,
            // o far fallire la validazione se la categoria è obbligatoria (gestito da altre regole 'required').
            // Per questo esempio, se la categoria non c'è, non possiamo verificare l'unicità *in* quella categoria.
            return;
        }

        $query = DB::table('products')
                    ->where('name', $productName)
                    ->where('category_id', $categoryId); // Assumendo una colonna category_id

        if ($this->ignoreId) {
            $query->where('id', '!=', $this->ignoreId);
        }

        if ($query->exists()) {
            $fail("Il nome prodotto \"{$productName}\" esiste già in questa categoria.");
        }
    }
}

Passo 4: Utilizzare i Rule Objects nelle Form Request (o nel Validator) Ora refactoriamo la StoreProductFormRequest dell'esempio precedente:

// app/Http/Requests/StoreProductFormRequest.php (Laravel 11/12 style con Rule Objects)
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use App\Rules\IsValidBusinessRegistrationNumber; // Importa la regola
use App\Rules\UniqueProductNameInCategory;   // Importa la regola
use Illuminate\Validation\Rule as IlluminateRule;

class StoreProductFormRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        $productIdToIgnore = $this->route('product') ? $this->route('product')->id : null;

        return [
            'name' => [
                'required',
                'string',
                'max:255',
                // Passiamo l'ID da ignorare e il nome del campo categoria
                new UniqueProductNameInCategory($productIdToIgnore, 'category_id_field'),
            ],
            'sku' => [ /* ... la regola unique standard di Laravel è spesso sufficiente ... */ ],
            'price' => 'required|numeric|min:0',
            'category_id_field' => 'required|integer|exists:categories,id', // Campo per la categoria
            'registration_code' => [
                'nullable',
                'string',
                new IsValidBusinessRegistrationNumber(prefix: 'REG', minLength: 8), // Passiamo argomenti!
            ],
            'complex_field' => [ /* ... potrebbe diventare un altro Rule Object ... */ ],
            'release_date' => 'required|date',
            'expiry_date' => [ // Esempio di regola che dipende da un altro campo, usando un Rule Object DataAware
                'nullable',
                'date',
                'after_or_equal:release_date', // Regola standard, ma se fosse più complessa...
                // new EndDateAfterStartDateRule('release_date'), // ...potrebbe diventare un Rule Object
            ],
        ];
    }

    public function messages(): array
    {
        return [
            'name.required' => 'Il nome del prodotto è un campo obbligatorio.',
            // I messaggi per i Rule Objects possono essere definiti nel Rule Object stesso
            // o sovrascritti qui se necessario, ma è meno comune.
        ];
    }
}

Passo 5: Testare i Rule Objects Uno dei maggiori vantaggi è la testabilità unitaria dei Rule Objects.

// tests/Unit/Rules/UniqueProductNameInCategoryTest.php
namespace Tests\Unit\Rules;

use App\Rules\UniqueProductNameInCategory;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase; // Utile se il rule object interagisce col DB

class UniqueProductNameInCategoryTest extends TestCase
{
    use RefreshDatabase; // Pulisce il DB per ogni test

    protected function setUp(): void
    {
        parent::setUp();
        // Popola il DB con dati di test se necessario
        DB::table('categories')->insert(['id' => 1, 'name' => 'Elettronica']);
        DB::table('products')->insert([
            'name' => 'Laptop SuperFast',
            'category_id' => 1,
            // ... altri campi
        ]);
    }

    public function test_new_product_with_unique_name_in_category_passes(): void
    {
        $rule = new UniqueProductNameInCategory(null, 'category_id'); // ignoreId è null per un nuovo prodotto
        $rule->setData(['category_id' => 1, 'name' => 'Mouse Wireless']); // Dati della richiesta

        $passes = true;
        $rule->validate('name', 'Mouse Wireless', function ($message) use (&$passes) {
            $passes = false;
        });
        $this->assertTrue($passes);
    }

    public function test_new_product_with_existing_name_in_same_category_fails(): void
    {
        $rule = new UniqueProductNameInCategory(null, 'category_id');
        $rule->setData(['category_id' => 1, 'name' => 'Laptop SuperFast']);

        $validationResult = 'passed';
        $rule->validate('name', 'Laptop SuperFast', function ($message) use (&$validationResult) {
            $validationResult = $message;
        });
        $this->assertStringContainsString('esiste già in questa categoria', $validationResult);
    }

    public function test_updating_product_with_same_name_in_category_passes_if_ignoring_self(): void
    {
        $existingProduct = DB::table('products')->where('name', 'Laptop SuperFast')->first();
        $rule = new UniqueProductNameInCategory($existingProduct->id, 'category_id'); // Ignora l'ID esistente
        $rule->setData(['category_id' => 1, 'name' => 'Laptop SuperFast']);

        $passes = true;
        $rule->validate('name', 'Laptop SuperFast', function ($message) use (&$passes) {
            $passes = false;
        });
        $this->assertTrue($passes);
    }
}

Benefici del refactoring a Rule Objects

L'adozione sistematica dei Rule Objects per la validazione complessa porta a:

  • Riutilizzabilità del codice: la stessa regola può essere applicata in più punti della tua applicazione (diverse Form Request, validatori manuali).
  • Testabilità migliorata: ogni regola può essere testata unitariamente in completo isolamento, garantendo la sua correttezza e rendendo i test più veloci e affidabili.
  • Leggibilità del codice: le Form Request e i controller diventano più snelli e focalizzati. La logica di validazione è chiaramente incapsulata nel suo Rule Object con un nome auto-esplicativo.
  • Adesione al principio di singola responsabilità (SRP): ogni Rule Object ha un solo compito: validare un attributo secondo una specifica logica.
  • Migliore organizzazione: le regole di validazione specifiche del dominio della tua impresa sono ben organizzate nella directory app/Rules, facili da trovare e modificare.
  • Manutenibilità: quando una regola di business cambia, modifichi un solo Rule Object, e la modifica si riflette ovunque venga utilizzato.

Il ruolo del programmatore Laravel esperto

Identificare le opportunità di refactoring, progettare Rule Objects efficaci e testabili, e integrarli in una base di codice esistente, specialmente in applicazioni aziendali di una certa dimensione, è un compito che beneficia grandemente dell'esperienza. Come sviluppatore laravel con una profonda conoscenza del framework e dei principi di software design, posso aiutare la tua impresa a:

  • Analizzare le attuali logiche di validazione e identificare i candidati per il refactoring.
  • Progettare e implementare Rule Objects robusti, riutilizzabili e performanti.
  • Scrivere unit test completi per ogni Rule Object.
  • Integrare le nuove regole nel flusso di validazione esistente.

Il mio approccio è sempre orientato a fornire soluzioni ingegneristicamente solide che portino valore tangibile al tuo business. Per maggiori dettagli sulla mia filosofia di lavoro, ti invito a visitare la pagina Chi Sono.

Investire nel refactoring della logica di validazione verso i Rule Objects non è solo un miglioramento tecnico; è un passo verso un'applicazione Laravel 12 più resiliente, più facile da far evolvere e che protegge meglio l'integrità dei dati della tua impresa.

Se senti che la gestione della validazione nella tua applicazione Laravel è diventata un punto dolente e vuoi esplorare come modernizzarla, contattami per una consulenza approfondita.

Ultima modifica: Venerdì 21 Febbraio 2025, alle 10:37

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