Migrare da Laravel 9 a Laravel 12: guida pratica per la produzione

Migrare da Laravel 9 a Laravel 12: guida pratica per la produzione

Portare un'applicazione da Laravel 9 a Laravel 12 non è un upgrade, è una scala. Non puoi saltare in un colpo solo: devi salire un gradino major alla volta, 9 verso 10, poi 11, poi 12, testando a ogni passo. E la sorpresa, per chi affronta questo lavoro la prima volta, è che il framework spesso non è la parte difficile. In un porting reale da Laravel 9 a una major recente, il tempo non se ne va sui cambiamenti di Laravel, se ne va sul salto della versione PHP minima richiesta e sui conflitti di versione delle dipendenze di terze parti. Laravel 9 girava su PHP 8.0, Laravel 12 ne richiede almeno 8.2: prima ancora di toccare il framework, devi sapere che tutto il tuo stack regge quel salto. Questa guida segue la sequenza per quello che è, con i breaking change che contano gradino per gradino, il punto dove il vero lavoro si nasconde, e la ragione per cui "zero breaking change dichiarati" non vuol dire "zero differenze di comportamento".

Non è un salto, è una scala: 9 verso 10, 11, 12

La regola d'oro degli upgrade di Laravel è che si sale una major alla volta. Non esiste un percorso diretto da 9 a 12, e provare a forzarlo significa accumulare i problemi di quattro versioni in un unico debugging ingestibile. Il metodo corretto è salire un gradino, far girare l'applicazione e la suite di test fino a quando è verde, e solo allora salire il successivo. In questo modo, quando qualcosa si rompe, sai esattamente quale major l'ha rotto, perché hai cambiato una cosa per volta.

Saltare le versioni intermedie non fa risparmiare tempo, lo sposta soltanto, e lo concentra nel momento peggiore: il debugging finale, dove i problemi di quattro major si presentano tutti insieme e indistinguibili. Salire un gradino alla volta è più lento all'apparenza e molto più veloce nei fatti.

Questo ha una conseguenza pratica sulla pianificazione: una migrazione 9 verso 12 non si stima come un singolo task, si stima come tre upgrade incrementali con tre cicli di test. Il primo, da 9 a 10, è relativamente leggero: richiede PHP 8.1 e introduce dichiarazioni di tipo native nello scheletro del framework, ma raramente rompe l'applicazione. È il secondo gradino quello che pesa.

Il lavoro vero spesso non è Laravel, è PHP e le dipendenze

Se chiedi a chi ha appena fatto questo upgrade cosa l'ha bloccato, la risposta quasi sempre non è una funzione di Laravel cambiata: sono le dipendenze di terze parti che litigano sulle versioni. Ogni pacchetto nel tuo composer.json dichiara con quali versioni di Laravel e di PHP è compatibile, e un solo pacchetto fermo a un vincolo vecchio blocca l'intera salita. Il primo controllo, prima ancora di alzare la versione del framework, è mappare le dipendenze e le loro compatibilità con la versione target. In pratica significa guardare i vincoli "laravel/framework" e "php" dichiarati da ogni pacchetto, individuare i tetti che bloccano la salita e, per ognuno, decidere la strada: aggiornare alla versione compatibile, sostituire con un'alternativa mantenuta, o (nel caso peggiore) gestire un fork se il pacchetto è abbandonato. È un censimento che va fatto a tavolino, perché scoprire a metà del 10 verso 11 che una libreria centrale non ha una versione compatibile, e non la avrà mai, è il modo più frustrante di far deragliare una migrazione già avviata. Meglio saperlo il primo giorno, quando puoi ancora pianificare la sostituzione con calma.

A questo si lega il salto del requisito PHP, che è il vero collo di bottiglia. Passare da PHP 8.0 a 8.2 può richiedere a sua volta correzioni nel codice applicativo, ed è un lavoro che merita un capitolo a sé: l'ho trattato parlando della migrazione del runtime PHP su applicazioni legacy in produzione, e vale la pena affrontarlo come pre-requisito, non come effetto collaterale. Una nota che fa risparmiare tempo: salendo a Laravel 11 puoi rimuovere la dipendenza doctrine/dbal, che il framework non richiede più.

Se hai un'applicazione Laravel ferma a una versione vecchia e vuoi pianificare la salita senza rompere la produzione, nel mio profilo professionale trovi l'esperienza concreta sul porting di applicazioni Laravel attraverso più major, dalla 9 alle versioni correnti.

Il gradino che conta: da Laravel 10 a 11

Il salto da 10 a 11 è quello che concentra i breaking change più significativi, ed è il punto in cui la guida di upgrade ufficiale di Laravel 11 va letta riga per riga. Il primo: Laravel 11 richiede PHP 8.2.0 o superiore e il client HTTP vuole curl 7.34 o superiore. Il secondo, che sorprende molti, riguarda le migrazioni dei pacchetti: librerie come Sanctum, Passport, Cashier e Telescope non caricano più automaticamente le proprie migrazioni dalla loro cartella, quindi devi pubblicarle nella tua applicazione con vendor:publish. Se usi Sanctum, va aggiornato alla versione ^4.0 (la 3.x non è più supportata); se usi Passport, alla ^12.0, e attenzione: il password grant è disabilitato di default e va riabilitato esplicitamente se ci fai affidamento.

C'è poi un breaking change tanto silenzioso quanto insidioso: il rate limiting al secondo. In Laravel 11 il rate limiting passa dalla granularità al minuto a quella al secondo, e di conseguenza la proprietà decayMinutes della classe Limit è stata rinominata in decaySeconds e ora contiene secondi. Se avevi configurazioni di rate limiting personalizzate, vanno riviste, perché un numero rimasto identico ora significa una cosa diversa. Infine, una feature che è anche un cambio di comportamento: Laravel 11 rifà automaticamente l'hash delle password durante l'autenticazione se il fattore di lavoro dell'algoritmo è cambiato, un meccanismo utile che ho approfondito parlando di rotazione delle chiavi e rehashing delle password in Laravel.

Lo scheletro nuovo è opzionale: non riscrivere la struttura

Qui sta l'equivoco più costoso, quello che fa perdere giorni a chi non lo sa. Laravel 11 ha introdotto il celebre slim skeleton, con la rimozione delle classi Kernel e lo spostamento di middleware, routing ed eccezioni in bootstrap/app.php. La tentazione, durante l'upgrade, è riscrivere la propria applicazione per adottare la nuova struttura. Non fatelo. Il team di Laravel è esplicito su questo punto: la nuova struttura è pensata per le installazioni nuove, e le applicazioni aggiornate da una versione precedente devono mantenere la loro struttura esistente. Laravel 11 è stato calibrato apposta per supportare anche lo scheletro di Laravel 10.

Questa è la differenza tra un upgrade e una riscrittura. Spostare a mano decine di file per inseguire una struttura "più pulita" introduce rischio enorme per un beneficio nullo in fase di migrazione: l'applicazione funziona identica con la vecchia struttura. L'unico ritocco che vale la pena fare è rimuovere il trait CreatesApplication dai test, perché in Laravel 11 è ormai fornito dal TestCase di base. Tutto il resto della vecchia struttura si lascia dov'è. Se un domani vuoi davvero migrare allo scheletro nuovo, lo fai come progetto separato e consapevole, non in mezzo a un upgrade di major.

Da 11 a 12: un upgrade finalmente leggero

Dopo lo scoglio della 11, il salto a Laravel 12 è un sollievo. I breaking change, elencati nella guida di upgrade di Laravel 12, sono pochi e mirati. Il principale è che il supporto a Carbon 2.x è stato rimosso: tutte le applicazioni Laravel 12 richiedono Carbon 3.x, da aggiornare nelle dipendenze. Cambia poi il comportamento di alcuni metodi dello Schema (come getTables, getViews, getTypes), che ora includono i risultati di tutti gli schemi di default, rilevante solo se lavori con database multi-schema. Le altre modifiche toccano classi di basso livello del database e interessano quasi esclusivamente gli autori di pacchetti, non lo sviluppo applicativo normale. Per la maggior parte delle applicazioni, il gradino da 11 a 12 si esaurisce nell'aggiornamento delle dipendenze e in un giro di test.

Il frontend e il tooling: il pezzo che tutti dimenticano

Una migrazione di Laravel non è solo backend, ed è qui che molti progetti inciampano sull'ultimo metro. Attraversando quattro major si attraversa anche un cambio di paradigma del build frontend: Laravel è passato da Laravel Mix a Vite come tool di default, e un'applicazione partita su 9 potrebbe ancora avere una configurazione Mix che va aggiornata o migrata. Insieme vanno aggiornati Node, npm e gli eventuali framework frontend (i pacchetti in package.json), perché versioni datate possono non essere compatibili con la toolchain richiesta dalle versioni recenti.

Il sintomo tipico di questa dimenticanza è un upgrade del backend che passa pulito in test e poi un deploy in cui gli asset non si compilano più, o si compilano con avvisi che diventano errori. La regola che applico è trattare package.json con la stessa serietà di composer.json: è una catena di dipendenze parallela, con i suoi vincoli di versione e i suoi pacchetti che possono restare indietro. Verificarla a monte, non a deploy fatto, evita la classica sorpresa dell'ultimo momento, quando il backend è pronto ma l'interfaccia non si costruisce.

La strategia a branch: un gradino, un ramo, un giro di test

Sul piano operativo, il modo in cui organizzo questo lavoro è con un branch dedicato per ogni gradino della scala. Un ramo per il 9 verso 10, lo si porta verde, lo si fa rivedere e lo si fonde; poi un ramo per il 10 verso 11, e così via. Questo tiene ogni salto isolato e revisionabile, e permette di fermarsi e rilasciare in produzione a una major intermedia se serve, invece di restare bloccati in un unico branch gigante che diverge dalla produzione per settimane. Su ogni ramo l'aggiornamento di composer.json è chirurgico: si alzano i vincoli del framework e delle dipendenze alla versione compatibile con quel gradino preciso, non all'ultima in assoluto, perché alzare troppo presto un pacchetto introduce conflitti che non c'entrano con il gradino su cui stai lavorando.

Il vantaggio di questo approccio è che la produzione non resta mai ostaggio della migrazione. Se a metà strada arriva una priorità urgente, hai già rilasciato i gradini completati e puoi sospendere senza perdere il lavoro fatto. È la stessa logica incrementale che riduce il rischio spostandolo in piccoli pezzi verificabili, invece di concentrarlo in un unico evento ad alto rischio.

Perché "zero breaking change" non significa zero differenze?

È la domanda che separa chi migra con metodo da chi si fida delle note di rilascio e basta. Quando una versione dichiara pochi o nessun breaking change, sta dicendo che non ha rotto le API pubbliche in modo intenzionale, non che la tua applicazione si comporterà in modo identico. Il rate limiting al secondo della 11 è l'esempio perfetto: nessuno chiamerà "breaking" il fatto che decayMinutes sia diventato decaySeconds, eppure se non aggiorni i valori il comportamento cambia in silenzio. A questo si aggiungono le differenze indotte dal salto PHP, dove il runtime più recente è più severo del precedente. La conseguenza operativa è che la suite di test non è opzionale: è l'unico modo per scoprire le differenze comportamentali che nessuna nota di rilascio elenca, ed è la ragione per cui, su un'applicazione senza test, il primo lavoro di una migrazione è scrivere almeno i test di percorso critico, come ho descritto parlando di testing delle API con Pest per la stabilità del backend.

Tirando le somme, migrare da Laravel 9 a 12 è un lavoro lineare ma non banale, e la competenza sta nel sapere dove si nasconde il rischio reale. Non è nel framework, che documenta i suoi cambiamenti con cura e li tiene contenuti gradino per gradino; è nel salto della versione PHP, nei pacchetti di terze parti che non seguono il passo, e nelle differenze di comportamento che nessuno chiama breaking ma che mordono in produzione. Si sale una major alla volta, si testa a ogni gradino, si lascia stare lo scheletro vecchio che funziona benissimo, e si tratta l'aggiornamento delle dipendenze come il vero cuore del progetto invece che come un dettaglio. Fatto così, un upgrade che molti rimandano per paura diventa un'operazione misurabile, e l'applicazione si ritrova su un framework supportato, più veloce e più sicuro, senza il salto nel buio che tutti temono. Se hai un'applicazione Laravel bloccata su una major fuori supporto e vuoi pianificare la salita con un percorso testato, contattami per una consulenza diretta: di solito la parte più delicata non è Laravel, è la mappa delle tue dipendenze, ed è lì che un occhio esperto fa la differenza.

Ultima modifica: