Migrazione da Symfony 5 a Symfony 7: guida pratica con casi reali di breaking change

Migrazione da Symfony 5 a Symfony 7: guida pratica con casi reali di breaking change

Nel 2025 ho migrato tre applicazioni Symfony da versione 5 a versione 7 per clienti del settore servizi professionali e fintech. La prima era un'applicazione di gestione documentale con 45.000 righe di codice su Symfony 5.4 e PHP 8.0, ferma da due anni sulla stessa versione perché il team aveva paura di rompere le cose aggiornando. La seconda era un'API REST per un portale B2B su Symfony 5.3 con 80 endpoint e 34 bundle third-party. La terza era un monolite Symfony 5.4 con front-end Twig e un set di 120 form type che nessuno osava toccare. In tutti e tre i casi, il mandato del cliente era identico: "Aggiorna a Symfony 7 senza rompere nulla in produzione." La mia risposta è stata identica in tutti e tre i casi: "Non si migra da Symfony 5 a Symfony 7 direttamente. Si passa da 5 a 6, si risolvono tutte le deprecation, e poi da 6 a 7. Sono due migrazioni separate, e saltare la prima è la ricetta per un disastro."

Il motivo è strutturale nella filosofia di versionamento di Symfony. La documentazione ufficiale di Symfony sulle migrazioni è cristallina su questo punto: ogni versione minor (5.1, 5.2, 5.3, 5.4) introduce deprecation notice per le funzionalità che verranno rimosse nella versione major successiva. La versione 5.4 è l'ultima della serie 5.x e contiene tutte le deprecation che diventeranno breaking change in 6.0. La versione 6.4 contiene tutte le deprecation per 7.0. Se salti dalla 5.x alla 7.0 direttamente, ti trovi ad affrontare due set di breaking change sovrapposti senza sapere quale appartiene a quale transizione - un debugging impossibile. Il processo corretto è: aggiorna a 5.4, risolvi tutte le deprecation, aggiorna a 6.0, risolvi i breaking change residui, aggiorna progressivamente fino a 6.4, risolvi le nuove deprecation, e finalmente aggiorna a 7.0.

Come si preparano e si risolvono le deprecation prima della migrazione major?

Il primo passaggio è misurare l'entità del problema. Symfony ha un sistema di deprecation logging integrato che registra ogni chiamata a codice deprecato durante l'esecuzione. La configurazione nel file config/packages/dev/deprecation.yaml (o nel framework.yaml con php_errors: log: true) attiva il logging di tutte le deprecation. Ma il modo più efficace è eseguire la suite di test con le deprecation abilitate come errori:

# Esegui i test trattando le deprecation come errori
# Questo forza la correzione prima della migrazione
SYMFONY_DEPRECATIONS_HELPER=max[direct]=0 php bin/phpunit

# Risultato tipico sulla prima applicazione:
# 1 deprecation triggered
# 47 remaining direct deprecation notices (47 invocations)
# 12 remaining indirect deprecation notices (89 invocations)

La distinzione tra direct e indirect deprecation è fondamentale per pianificare il lavoro. Le deprecation dirette sono nel tuo codice - devi correggerle tu. Le deprecation indirette sono nei bundle di terze parti che usi - devi aspettare che il maintainer del bundle rilasci una versione compatibile, o forkare il bundle e correggerle tu. Sulla prima applicazione, le 47 deprecation dirette richiesero 3 giorni di lavoro distribuiti su una settimana. Le 12 indirette richiesero l'aggiornamento di 5 bundle (di cui 2 avevano già una versione Symfony 6-compatible e 3 richiesero un fork temporaneo con patch manuale).

I breaking change che mi hanno sorpreso di più nelle tre migrazioni sono stati quelli che non generavano deprecation notice - casi in cui il comportamento del framework cambiava silenziosamente senza avvertimento. Il caso più insidioso è stato il cambiamento nella gestione del session.cookie_secure in Symfony 6: il valore di default è passato da false a auto, il che significa che in produzione dietro un reverse proxy senza il header X-Forwarded-Proto correttamente configurato, i cookie di sessione non venivano impostati come secure e gli utenti vedevano un logout inaspettato ad ogni cambio di pagina. Nessuna deprecation avvertiva di questo cambiamento - era una modifica di default documentata solo nelle release notes della 6.0.

Nel mio profilo professionale trovi l'esperienza specifica sulla migrazione di applicazioni Symfony tra versioni major - un'operazione che richiede non solo competenza sul framework, ma anche familiarità con l'ecosistema dei bundle di terze parti e con i pattern di configurazione che cambiano tra una versione e l'altra.

L'ordine di migrazione: quali componenti aggiornare per primi

L'ordine in cui aggiorni i componenti durante una migrazione Symfony non è arbitrario - c'è una sequenza che minimizza il rischio di conflitti e di regressioni. Nel mio processo, l'ordine è:

  1. PHP version first. Symfony 6 richiede PHP 8.1 minimo, Symfony 7 richiede PHP 8.2. Se l'applicazione gira su PHP 8.0, il primo passo è l'upgrade del runtime - senza toccare Symfony. Questo isolamento ti permette di verificare che il codice PHP sia compatibile con la nuova versione del linguaggio prima di aggiungere la complessità della migrazione framework
  2. Symfony core packages. Aggiorna symfony/framework-bundle, symfony/http-kernel, symfony/routing, symfony/security-bundle e i componenti core tutti insieme nella stessa operazione Composer. Aggiornarli uno alla volta causa conflitti di versione perché i componenti Symfony hanno vincoli di compatibilità incrociati
  3. Bundle di terze parti. Dopo che il core Symfony è aggiornato, aggiorna i bundle uno alla volta, eseguendo i test dopo ogni aggiornamento. Questo ti permette di isolare quale bundle causa un'eventuale regressione
  4. Configurazione e YAML. Il formato di configurazione cambia tra le versioni major - ad esempio, la struttura del security.yaml è stata significativamente riscritta tra Symfony 5 e 6, con il nuovo sistema di autenticazione basato su authenticator che sostituisce i guard handler. Questo passaggio è quasi sempre il più laborioso perché le deprecation nel YAML non generano errori PHP - generano warning nel log che è facile ignorare

Il passaggio più rischioso è sempre il security bundle. Symfony 6 ha introdotto il nuovo sistema di autenticazione basato su security.firewalls.*.custom_authenticator che sostituisce il vecchio pattern con guard authenticator. La migrazione richiede di riscrivere ogni authenticator custom - nel caso della seconda applicazione (API REST con 80 endpoint), avevo 3 authenticator custom (JWT, API key, e OAuth2 per integrazioni esterne) che sono stati riscritti in 6 ore di lavoro. Il nuovo sistema è oggettivamente migliore (più pulito, più testabile, meno boilerplate), ma la migrazione non è banale e non è automatizzabile.

I form type: il campo minato silenzioso

Dei tre componenti che richiedono più lavoro durante una migrazione Symfony, i form type sono il più insidioso perché i cambiamenti sono sottili e spesso non generano errori fino a quando un utente non compila il form in un modo specifico. I breaking change nei form type tra Symfony 5 e 7 includono: la gestione dei campi non mappati ('mapped' => false) che ora richiede un tipo di dato esplicito, il comportamento dei ChoiceType con expanded => true che cambia la struttura HTML renderizzata, e il handling dei CSRF token che in Symfony 7 è più restrittivo di default.

Sulla terza applicazione (il monolite con 120 form type), ho adottato un approccio sistematico: per ogni form type, ho scritto un test funzionale che compila il form con dati validi, lo sottomette, e verifica che i dati vengano persistiti correttamente nel database. Questi test non esistevano prima della migrazione - e la loro scrittura (circa 20 minuti per form type, 40 ore totali) è stata l'investimento che ha reso la migrazione sicura. Senza quei test, avremmo scoperto i breaking change dei form type in produzione, quando un utente dell'applicazione avrebbe compilato un form che non funzionava più. Ho descritto un approccio simile alla validazione sistematica nel mio articolo sui test automatici per codebase PHP legacy, dove la regola è la stessa: se non hai test prima della modifica, scrivili. Il costo dei test è una frazione del costo dei bug in produzione.

Doctrine e le migration del database: il rischio nascosto

Un aspetto della migrazione Symfony che molti sviluppatori trascurano è l'impatto sulle migration di Doctrine. Quando aggiorni doctrine/orm dalla versione 2.x (tipica di Symfony 5) alla versione 3.x (richiesta da Symfony 7), il generatore di migration cambia il modo in cui produce DDL - in particolare, la gestione delle colonne con default, i tipi di dato per le enum PHP, e la nomenclatura degli indici automatici. Il risultato è che doctrine:migrations:diff genera una migration "fantasma" che vuole rinominare indici e modificare colonne che sono già corrette nel database di produzione. Se esegui quella migration in produzione senza verificarla, rischi di ricostruire indici su tabelle grandi con lock esclusivi che bloccano l'applicazione per minuti.

La soluzione che uso è eseguire doctrine:migrations:diff immediatamente dopo l'aggiornamento di Doctrine, leggere la migration generata con attenzione, e scartare tutte le operazioni che sono solo cosmetiche (rinomina di indici, cambio di tipo di default che non cambia il comportamento). Solo le modifiche strutturali reali vanno mantenute. Per la seconda applicazione (l'API REST), la migration generata automaticamente dopo l'upgrade di Doctrine conteneva 34 operazioni di cui solo 3 erano modifiche reali - le altre 31 erano rinominazioni di indici che Doctrine 3 genera con un pattern di naming diverso da Doctrine 2. Se avessi eseguito quella migration senza review, 31 indici sarebbero stati ricostruiti inutilmente su tabelle con milioni di righe, con un downtime stimato di 15-20 minuti.

Un altro punto critico è la gestione dei tipi personalizzati di Doctrine. Se hai definito custom DBAL types (comuni nelle applicazioni che gestiscono JSON, UUID o enum PHP come tipi di colonna), l'API di registrazione dei tipi è cambiata significativamente tra Doctrine DBAL 3 e DBAL 4. Il vecchio Type::addType() è stato sostituito da un pattern di configurazione nel doctrine.yaml che usa doctrine.dbal.types, e la classe base Type ha metodi astratti diversi. Ogni custom type deve essere riscritto per la nuova API - un lavoro che sulla terza applicazione ha richiesto 4 ore per 6 custom types.

Symfony 7.2: cosa vale l'aggiornamento

Una volta arrivati a Symfony 7, la domanda naturale è: vale la pena? La risposta è sì, per tre motivi concreti. Il primo è il supporto long-term: Symfony 5.4 è in fine supporto a novembre 2025, e dopo quella data non riceve più nemmeno i security fix - il che significa che qualsiasi vulnerabilità scoperta nel framework dopo quella data resta aperta nella tua applicazione. Il secondo è la compatibilità con PHP 8.3 e 8.4, che portano miglioramenti di performance significativi (typed class constants, readonly classes, lazy objects nativi di cui ho parlato in un altro articolo). Il terzo è l'accesso ai nuovi componenti e alle nuove funzionalità: il Scheduler component per la pianificazione di task ricorrenti senza cronjob esterni, il nuovo mapper component per la trasformazione di dati tra DTO, e i miglioramenti al Messenger che supporta ora il batching nativo dei messaggi.

Il costo della migrazione - misurato sui tre progetti - è stato in media di 60-80 ore di lavoro per applicazione, distribuite su 4-6 settimane. Non è un costo trascurabile, ma è un investimento che si ripaga in sicurezza (security fix garantiti per i prossimi 3 anni), in performance (PHP 8.4 + Symfony 7 è mediamente il 15-20% più veloce di PHP 8.0 + Symfony 5.4 sugli stessi benchmark applicativi), e in produttività del team (le nuove API del framework sono più ergonomiche e richiedono meno boilerplate). Se hai un'applicazione Symfony 5 o 6 e stai rimandando l'aggiornamento per paura di rompere qualcosa, la paura è comprensibile ma il rimandare è un rischio crescente - ogni mese che passa, il gap di deprecation si allarga e la migrazione diventa più complessa. Contattami per una valutazione della migrazione: in una giornata di analisi misuriamo le deprecation, mappiamo i bundle incompatibili, e produciamo un piano di migrazione con tempi e rischi quantificati.

Ultima modifica: