PHP 8 Enums: sostituire le costanti di classe e i magic strings nei domini di business

PHP 8 Enums: sostituire le costanti di classe e i magic strings nei domini di business

In ogni codebase PHP legacy che eredito - e ne eredito mediamente 4-5 l'anno da clienti PMI che hanno perso il loro sviluppatore originario - trovo lo stesso pattern tossico: gli stati di business sono rappresentati da costanti integer senza significato semantico o da stringhe magiche sparse nel codice. Un gestionale ordini tipico ha const STATUS_1 = 1; const STATUS_2 = 2; const STATUS_3 = 3; in una classe OrderHelper, dove nessuno ricorda più cosa significhi lo stato 3 (è "annullato"? "in attesa"? "spedito"?), e dove la validazione degli stati consiste in un if ($status >= 1 && $status <= 5) che accetta anche valori come 4 e 5 che non corrispondono a nessuno stato definito. In un altro progetto, un e-commerce con sei anni di evoluzione organica, ho contato 40 costanti di stato distribuite tra 12 file diversi - ordini, pagamenti, spedizioni, resi - con nomi come PAYMENT_PENDING, PAY_WAIT, STATUS_WAITING_PAY che rappresentavano tutti lo stesso concetto ma erano usati in punti diversi del codice senza alcuna garanzia di coerenza.

PHP 8.1 ha introdotto i backed enum nativi - il primo tipo di dato nel linguaggio progettato specificamente per rappresentare un insieme chiuso e finito di valori. Non sono classi, non sono interfacce, non sono trait - sono un costrutto del linguaggio che il type system di PHP enforza a compile-time. Se un metodo accetta un parametro di tipo StatoOrdine, è fisicamente impossibile passargli un valore che non sia uno degli stati definiti nell'enum. Niente più if ($status >= 1 && $status <= 5), niente più costanti integer senza significato, niente più stringhe magiche che cambiano tra un file e l'altro. Ho adottato gli enum in ogni progetto PHP dal 2022 in poi, e la differenza nella leggibilità e nella robustezza del codice di dominio è notte e giorno.

Come si definisce un enum di dominio in PHP 8 e cosa lo rende superiore alle costanti?

Un enum PHP è un tipo che definisce un insieme esplicito e chiuso di valori possibili. A differenza delle costanti di classe (che sono valori scalari senza relazione tra loro), gli enum sono un tipo - il che significa che PHP verifica a runtime (e gli IDE verificano a edit-time) che il valore sia uno di quelli ammessi. Un backed enum associa a ciascun valore un valore scalare (string o int) per la persistenza nel database e per la serializzazione:

// app/Enums/StatoOrdine.php - enum di dominio con metodi di business
enum StatoOrdine: string
{
    case Bozza = 'bozza';
    case Confermato = 'confermato';
    case InPreparazione = 'in_preparazione';
    case Spedito = 'spedito';
    case Consegnato = 'consegnato';
    case Annullato = 'annullato';
    case Reso = 'reso';

    // Metodo di dominio: questo stato è modificabile dall'utente?
    public function isModificabile(): bool
    {
        return match ($this) {
            self::Bozza, self::Confermato => true,
            default => false,
        };
    }

    // Metodo di dominio: transizioni ammesse da questo stato
    public function transizioniAmmesse(): array
    {
        return match ($this) {
            self::Bozza => [self::Confermato, self::Annullato],
            self::Confermato => [self::InPreparazione, self::Annullato],
            self::InPreparazione => [self::Spedito],
            self::Spedito => [self::Consegnato, self::Reso],
            self::Consegnato => [self::Reso],
            self::Annullato, self::Reso => [],
        };
    }

    // Metodo di dominio: label leggibile per l'interfaccia
    public function label(): string
    {
        return match ($this) {
            self::Bozza => 'Bozza',
            self::Confermato => 'Confermato',
            self::InPreparazione => 'In preparazione',
            self::Spedito => 'Spedito',
            self::Consegnato => 'Consegnato',
            self::Annullato => 'Annullato',
            self::Reso => 'Reso',
        };
    }

    // Può transitare a un nuovo stato?
    public function puòTransitareA(self $nuovo): bool
    {
        return in_array($nuovo, $this->transizioniAmmesse(), true);
    }
}

La potenza di questo approccio rispetto alle costanti emerge in tre punti specifici. Il primo è la type safety: un metodo che dichiara function aggiornaStato(StatoOrdine $nuovoStato) rifiuta qualsiasi valore che non sia uno dei 7 stati definiti - il type system di PHP lo verifica automaticamente. Con le costanti integer, un function aggiornaStato(int $nuovoStato) accetta qualsiasi intero, inclusi valori come 99 che non corrispondono a nessuno stato. Il secondo è l'encapsulation della logica di dominio: il metodo transizioniAmmesse() vive nell'enum insieme ai valori - non in un service separato, non in un helper dimenticato, non in un commento sopra una costante. La logica di business è nel posto più naturale: accanto ai dati che descrive. Il terzo è l'autocompletamento: l'IDE sa quali sono i valori possibili e suggerisce StatoOrdine::Confermato quando digiti StatoOrdine:: - un beneficio di produttività che con le costanti è parziale (perché le costanti non hanno un tipo comune).

Nel mio profilo professionale trovi il dettaglio dell'esperienza che porto nella modernizzazione di codebase PHP legacy - e la migrazione da costanti a enum è uno dei primi interventi che faccio in ogni progetto di refactoring, perché il rapporto costo/beneficio è eccezionale: poche ore di lavoro per un miglioramento permanente della leggibilità e della robustezza del codice.

Integrazione con Eloquent e con il database

Laravel supporta gli enum come cast nativi nei model Eloquent dalla versione 9. La configurazione è una riga nel model:

// app/Models/Ordine.php
class Ordine extends Model
{
    protected $casts = [
        'stato' => StatoOrdine::class,
    ];
}

// Utilizzo: il campo stato è sempre un'istanza dell'enum
$ordine = Ordine::find(1);
$ordine->stato; // StatoOrdine::Confermato (non la stringa "confermato")

// Type-safe nelle query
Ordine::where('stato', StatoOrdine::Spedito)->get();

// Validazione delle transizioni nel service
if (!$ordine->stato->puòTransitareA(StatoOrdine::Spedito)) {
    throw new TransizioneNonAmmessaException(
        "Non è possibile passare da {$ordine->stato->label()} a Spedito"
    );
}

Il cast di Eloquent gestisce automaticamente la conversione tra il valore string nel database ("confermato") e l'istanza dell'enum (StatoOrdine::Confermato) nel codice PHP. Quando leggi un model, il campo è un enum. Quando scrivi, Eloquent persiste il valore backed dell'enum. Se il database contiene un valore che non corrisponde a nessun caso dell'enum (ad esempio una vecchia riga con lo stato "sospeso" che non esiste più nell'enum), Eloquent lancia un'eccezione chiara - il che è un vantaggio, non un problema, perché ti segnala immediatamente un dato inconsistente che altrimenti passerebbe inosservato.

Enum nelle API e nella validazione: un contratto esplicito

Un vantaggio degli enum che emerge chiaramente nelle API REST è la capacità di generare automaticamente la lista dei valori ammessi per la documentazione e la validazione. In un FormRequest Laravel, la validazione di un campo enum è una singola regola:

// Validazione del campo stato con enum
$request->validate([
    'stato' => ['required', new Enum(StatoOrdine::class)],
]);

Questa regola verifica che il valore inviato dal client corrisponda a uno dei valori backed dell'enum ("bozza", "confermato", ecc.) e restituisce un errore di validazione 422 con un messaggio chiaro se il valore non è ammesso. Con le costanti, la validazione equivalente sarebbe un Rule::in(['bozza', 'confermato', 'in_preparazione', ...]) dove la lista dei valori ammessi deve essere mantenuta manualmente sincronizzata con le costanti - un'altra fonte di drift tra definizione e validazione che con gli enum non esiste.

Nelle risposte API, gli enum permettono di serializzare sia il valore tecnico sia il label leggibile senza codice aggiuntivo nel transformer o nella Resource. Un OrdineResource può esporre 'stato' => $ordine->stato->value per il valore tecnico (usato dal frontend per la logica) e 'stato_label' => $ordine->stato->label() per il testo leggibile (mostrato all'utente). Se domani aggiungi uno stato nuovo all'enum, il label viene generato dal metodo dell'enum - nessun file di traduzione separato da aggiornare, nessun risk di label mancante.

L'integrazione con il frontend è altrettanto pulita. L'endpoint GET /api/ordini/stati può restituire tutti i valori possibili dell'enum con i relativi label usando StatoOrdine::cases() e array_map - un endpoint che il frontend usa per popolare i dropdown e i filtri, garantendo che le opzioni mostrate all'utente siano sempre allineate con i valori accettati dal backend. Con le costanti, questo endpoint non esiste o viene mantenuto manualmente con il rischio costante di disallineamento.

Un aspetto che apprezzo particolarmente è la capacità degli enum PHP di implementare interfacce. Se hai più enum che rappresentano stati in domini diversi (stato ordine, stato pagamento, stato spedizione), puoi definire un'interfaccia HasLabel con il metodo label(): string e implementarla in ogni enum - il che permette di avere un rendering generico dei label in Blade o nei componenti Vue senza conoscere il tipo specifico dell'enum. Questo pattern riduce la duplicazione e rende il codice di presentazione indipendente dal dominio specifico.

La migrazione da costanti a enum: il processo incrementale

La migrazione di 40 costanti sparse in 12 file verso enum tipizzati non è un'operazione che si fa in un colpo solo - è un processo incrementale che segue lo stesso principio di qualsiasi refactoring di codice PHP legacy: una modifica alla volta, con test dopo ogni modifica, e mai smettere di funzionare.

Il processo che seguo è in quattro fasi. Prima, creo l'enum con tutti i valori corrispondenti alle costanti esistenti - l'enum è un superset delle costanti, non un sostituto immediato. Secondo, aggiungo il cast al model Eloquent e verifico che la lettura e la scrittura funzionino. Terzo, sostituisco i riferimenti alle costanti con i casi dell'enum nei controller e nei service, uno alla volta, eseguendo i test dopo ogni sostituzione. Quarto, quando tutti i riferimenti sono stati migrati, rimuovo le vecchie costanti e il refactoring è completo.

La fase più critica è la terza, perché i riferimenti alle costanti possono essere in posti inaspettati: nelle query WHERE stato = 'confermato' hard-coded nelle migration o nei seeder, nei job in coda serializzati con il vecchio formato, nelle risposte API che i client esterni parsano con aspettative specifiche. Ogni punto di utilizzo deve essere identificato (grep globale sul repository) e aggiornato con attenzione. Il vantaggio è che il type system di PHP segnalerà un errore immediato se dimentichi un punto: un metodo che accetta StatoOrdine rifiuterà la stringa "confermato" e l'intero 2 - obbligandoti a convertire ogni punto di contatto.

La lezione che tiro da questi refactoring è che gli enum PHP non sono un "feature nice to have" - sono un cambiamento fondamentale nel modo in cui il codice PHP esprime i vincoli del dominio di business. Un'applicazione che usa enum per gli stati, le categorie, i tipi e i ruoli è un'applicazione dove il compilatore cattura errori che altrimenti arriverebbero in produzione come bug logici. Nel progetto dell'e-commerce con 40 costanti sparse in 12 file, la migrazione completa ha richiesto 12 ore di lavoro distribuite su una settimana - due ore al giorno, con deploy incrementali dopo ogni gruppo di costanti migrate. Al termine, il codice aveva 5 enum (StatoOrdine, StatoPagamento, StatoSpedizione, TipoCliente, MetodoPagamento) con un totale di 28 casi, ciascuno con metodi di dominio appropriati (label, transizioni ammesse, è modificabile, è finale). Le 40 costanti originali e le 15 funzioni helper che le accompagnavano (funzioni come getStatusLabel($status), canTransition($from, $to), isEditable($status)) sono state eliminate completamente. Il codice è passato da 450 righe distribuite in 12 file a 200 righe concentrate in 5 enum - meno codice, più espressivo, e garantito dal type system.

Il costo della migrazione è trascurabile (poche ore per un dominio di media complessità), il beneficio è permanente, e ogni sviluppatore che tocca il codice dopo di te ti ringrazierà per avergli reso impossibile passare un valore sbagliato a un metodo di business. Se la tua codebase PHP è ancora piena di costanti integer e stringhe magiche per gli stati di business, contattami per pianificare la migrazione: in una giornata identifichiamo i domini candidati, creiamo gli enum con i metodi di business appropriati, e definiamo il piano di migrazione incrementale che porta il codice nel 2025 senza rompere la produzione.

Ultima modifica: