Migrare da PHP 7.4 a 8.3: checklist per applicazioni legacy in produzione

Migrare da PHP 7.4 a 8.3: checklist per applicazioni legacy in produzione

Migrare un'applicazione legacy da PHP 7.4 a 8.3 non è un aggiornamento al volo: è un progetto di rischio gestito, con una sua sequenza e una sua rete di sicurezza. La tentazione, su un gestionale che gira da otto anni, è cambiare la versione del runtime sul server e sperare che vada. Non va, quasi mai, e il modo in cui non va è il peggiore possibile: non un errore in faccia al deploy, ma un comportamento sottilmente diverso che si manifesta giorni dopo, su un caso limite, in produzione. In una subentranza su un progetto legacy PHP datato ho visto un confronto tra una stringa e un numero, rimasto identico nel codice per anni, cambiare risultato dopo l'upgrade e rompere silenziosamente una logica di autorizzazione. Per questo la migrazione si affronta con un metodo, non con un apt upgrade. Vediamo la sequenza: perché 7.4 va abbandonata adesso, quali breaking change mordono davvero, come si procede in modo incrementale senza fermare la produzione e quali strumenti fanno da rete sotto al trapezio.

Perché 7.4 nel 2026 non è più una scelta di sicurezza

Cominciamo dal contesto, perché molti rimandano la migrazione senza rendersi conto di quanto siano scoperti. PHP 7.4 è fuori supporto da anni: nessuna patch di sicurezza, nessun aggiornamento. Restare lì significa esporre l'applicazione a vulnerabilità note del runtime, non solo del proprio codice. E il quadro delle versioni supportate da php.net chiarisce anche un punto sul target: alla metà del 2026 PHP 8.3 è entrato nella fase security-only, mentre il ramo attivamente sviluppato è il 8.4 e la versione stabile corrente è il 8.5; perfino il 8.2 chiude il supporto di sicurezza alla fine del 2026. Tradotto: 8.3 è una destinazione solida e ben supportata per uscire dalla 7.4, ma conviene pianificare l'atterraggio finale sul ramo 8.4, che ti dà la finestra di supporto più lunga. Il salto 7.4 verso 8.3 resta il pezzo di lavoro più grosso, perché attraversa quattro major (8.0, 8.1, 8.2, 8.3); arrivare poi da 8.3 a 8.4 è un passo molto più piccolo.

Restare su PHP 7.4 nel 2026 non è "se funziona non si tocca", è correre con un runtime senza patch di sicurezza. Il debito non è solo tecnico, è di esposizione: ogni mese in più è un mese di vulnerabilità del linguaggio non più corrette.

I breaking change che mordono davvero

La lista ufficiale dei cambiamenti incompatibili tra 7.4 e 8.3 è lunga, ma una manciata di voci concentra il 90% dei problemi reali su una codebase legacy. La prima, e la più subdola, è il cambiamento del confronto tra stringhe e numeri introdotto in PHP 8.0. Prima la stringa veniva convertita in numero, ora è il numero a essere convertito in stringa: il classico 0 == "qualcosa", che in 7.4 era true (perché "qualcosa" diventava 0), in 8.0 è false.

// PHP 7.4: true  | PHP 8.0+: false
var_dump(0 == "login_ok");

// dove fa danno: una guardia scritta male che "funzionava per caso"
if ($userRole == 0) { /* ramo admin */ }   // se $userRole è una stringa non numerica

Questa singola modifica può ribaltare logiche di confronto rimaste invariate per anni. La difesa strutturale è passare ai confronti stretti (===) e allo strict typing, ma il primo passo è scovare i confronti larghi sospetti.

Il secondo morso, durissimo sulle applicazioni vecchie, è il cambio di comportamento degli errori di mysqli in PHP 8.1: quello che prima era un warning silenzioso ora lancia un'eccezione. Codice che "sembrava funzionare" inizia a fallire con Uncaught mysqli_sql_exception, e spesso fa emergere bug di database che il vecchio PHP nascondeva.

// PHP 8.1: mysqli lancia eccezioni di default.
// Codice legacy che ignorava il valore di ritorno ora esplode.
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // comportamento implicito in 8.1+

try {
    $result = $mysqli->query($sql);
} catch (\mysqli_sql_exception $e) {
    // ora DEVI gestire l'errore che prima ignoravi
}

Il terzo è la deprecazione delle proprietà dinamiche in PHP 8.2: assegnare a una proprietà non dichiarata genera un avviso di deprecazione e diventerà un errore in PHP 9.0. Su codebase che assegnavano proprietà al volo agli oggetti è una pioggia di deprecazioni. La soluzione corretta è dichiarare le proprietà; la toppa temporanea è l'attributo #[\AllowDynamicProperties] sulla classe. A questi si aggiungono cambiamenti più puntuali ma insidiosi: l'operatore @ in PHP 8.0 non silenzia più gli errori fatali, gli operatori aritmetici lanciano TypeError su array e oggetti, gli argomenti con default null non rendono più implicitamente nullable il tipo, e in PHP 8.3 l'estensione delle date solleva eccezioni specifiche (DateError, DateException) dove prima emetteva warning.

Se stai valutando un intervento di questo tipo su un'applicazione critica e vuoi farlo con un metodo invece che a tentativi, nel mio profilo professionale trovi l'esperienza concreta sul porting di codebase PHP datate verso le versioni moderne, senza riscritture big-bang.

Le dipendenze: il blocco di cui ti accorgi troppo tardi

C'è un fronte che le checklist incentrate sul codice proprio dimenticano, ed è quello che fa naufragare più migrazioni: le dipendenze di terze parti. Il tuo codice può anche essere pronto per 8.3, ma se un pacchetto in composer.json dichiara compatibilità solo fino a PHP 7.4, l'aggiornamento si ferma lì. Il primo controllo che faccio, ancora prima di toccare il codice applicativo, è verificare che ogni dipendenza supporti la versione target: composer why per capire chi tira dentro cosa, e i vincoli "php" nei composer.json dei pacchetti per scoprire i tetti nascosti. Il caso peggiore è il pacchetto abbandonato: una libreria non più mantenuta che non sarà mai aggiornata per 8.3 e che blocca tutta la catena.

Qui le strade sono tre, in ordine di preferenza: trovare un sostituto mantenuto, trovare un fork attivo, oppure mantenerlo tu. Ci sono passato in prima persona: quando il pacchetto LaravelCollective/html è stato abbandonato, ne ho preso e mantenuto un fork compatibile con le versioni moderne di Laravel, oggi con decine di migliaia di download su Packagist. Non lo dico per vantarmi del fork, ma perché illustra la dinamica: su un legacy reale, la migrazione del runtime è spesso bloccata non dal tuo codice ma da una dipendenza che nessuno aggiorna più, e quel nodo va sciolto presto, non scoperto a metà progetto. Va aggiunto anche il controllo delle estensioni PHP: una funzione di un'estensione rimossa o cambiata tra le versioni è un altro blocco silenzioso, da intercettare in fase di analisi e non al primo errore in staging.

La strategia incrementale per non fermare la produzione

Il principio che applico è non toccare la produzione finché la nuova versione non è verde altrove. Concretamente, la prima mossa non è cambiare il PHP del server, è mettere in piedi una pipeline di integrazione continua che faccia girare il codice su 8.3 in parallelo, mentre la produzione resta su 7.4. La matrice di test su più versioni ti dice esattamente cosa si rompe, prima e senza rischio. Solo quando il codice passa pulito su 8.3 in CI si pianifica lo switch del runtime, idealmente con un deploy graduale (una macchina alla volta, o una quota di traffico) invece che con un cambio secco su tutto il parco.

L'ordine delle operazioni conta. Prima si esegue l'analisi statica per trovare i problemi senza eseguire il codice, poi si correggono i breaking change noti partendo dai più diffusi (confronti larghi, mysqli, proprietà dinamiche), poi si fa girare la suite di test con error_reporting(E_ALL) per far emergere ogni deprecazione, e solo dopo si tocca l'ambiente di staging. Questa progressione, dal codice ai test all'ambiente, è la stessa che descrivo nel refactoring del codice PHP legacy senza riscrittura: si riduce il rischio spostandolo dove costa poco, lontano dalla produzione.

Gli strumenti che fanno da rete sotto al trapezio

Una migrazione fatta a occhio è una scommessa; fatta con gli strumenti giusti è ingegneria. L'analisi statica con PHPStan a un livello alto, o con Psalm, intercetta gran parte dei problemi di tipo e dei comportamenti cambiati senza nemmeno eseguire il codice: è la rete che cattura ciò che la lettura umana si perde su decine di migliaia di righe. Per la parte meccanica, Rector automatizza una quota consistente delle correzioni di upgrade, applicando le trasformazioni note tra versioni; non fa tutto, ma toglie il lavoro ripetitivo e ti lascia i casi che richiedono giudizio. Infine la suite di test, che su un legacy spesso non esiste: ed è proprio quello il momento di introdurla, almeno come rete di smoke test sui percorsi critici prima di toccare il runtime, come ho descritto parlando di test minimi su PHP legacy.

La regola d'oro è che la migrazione svela ciò che il vecchio PHP perdonava. Molto codice 7.4 "funzionava" solo grazie a comportamenti permissivi del linguaggio: tolto il velo, i bug latenti vengono a galla. Non è la migrazione a creare quei bug, è la migrazione a renderli visibili, ed è una buona notizia travestita da cattiva.

Il piano di rollback, che quasi nessuno prepara

Una migrazione seria prevede il fallimento. Prima di switchare il runtime in produzione, voglio sapere esattamente come torno indietro se qualcosa si rompe sotto carico reale, perché alcuni problemi si manifestano solo con il traffico vero e nessun ambiente di staging li riproduce al 100%. La rete più semplice, su un sistema con PHP-FPM, è tenere installate entrambe le versioni del runtime e poter ricondurre il pool alla 7.4 con un cambio di configurazione e un reload, in pochi secondi, senza ridistribuire nulla. Su infrastrutture più strutturate il pattern è il blue-green o il canary: il nuovo runtime serve prima una piccola quota di traffico, si osservano log ed errori, e solo se regge si allarga.

Il momento più delicato non è il deploy, sono le ore subito dopo. Tengo sotto osservazione il log degli errori PHP filtrato sulle eccezioni nuove (le mysqli_sql_exception in testa), i tempi di risposta e i tassi di errore applicativi, perché un confronto cambiato o una query che ora lancia un'eccezione si vedono lì prima che li segnali un cliente. La regola che non tradisco è che lo switch del runtime si fa quando c'è qualcuno a guardare i grafici, mai a fine giornata o prima di un weekend. Avere un rollback provato, e non solo teorizzato, è ciò che trasforma la migrazione da scommessa in operazione controllata.

Quanto tempo serve per migrare da 7.4 a 8.3?

La risposta onesta è che non dipende dalle dimensioni del codice, dipende dalla sua qualità e dalla sua copertura di test. Una codebase di centomila righe ben strutturata e con test decenti si migra in giorni; una di ventimila righe senza un solo test, piena di confronti larghi e accessi mysqli non gestiti, può richiedere settimane, perché ogni modifica va verificata a mano. Il costo reale della migrazione non è il codice che cambi, è il codice non testato che devi verificare per essere sicuro di non aver rotto nulla. Per questo il primo investimento sensato, quando la copertura è zero, è scrivere i test di percorso critico: ti servono comunque per migrare, e ti restano per sempre.

Tirando le fila, migrare da 7.4 a 8.3 è uno di quei lavori in cui la differenza tra un professionista e un dilettante non sta nel sapere cosa è cambiato, quello è scritto nel manuale, ma nel sapere in che ordine affrontarlo e dove mettere le reti. Si parte dalla consapevolezza che restare su 7.4 è un rischio di sicurezza non negoziabile, si intercettano i breaking change che mordono davvero con l'analisi statica prima ancora di eseguire il codice, si tiene la produzione al riparo dietro una pipeline che testa la nuova versione in parallelo, e si usano gli strumenti per trasformare un salto nel buio in un percorso misurabile. Pianificando l'atterraggio sul ramo attivo, non su quello già in coda di vita. Se hai un'applicazione PHP ferma a una versione fuori supporto e vuoi affrontare la migrazione come un progetto con un piano e una rete, invece che come un salto della fede, contattami per una consulenza diretta: di solito la parte più rischiosa non è il codice nuovo, è capire cosa il vecchio runtime stava nascondendo, e prepararsi a gestirlo prima che lo faccia emergere il traffico di produzione.

Ultima modifica: