Migrazione del codice legacy: perché non puoi affidarti a un consulente generico
C'è un momento preciso in cui una migrazione legacy fatta male si manifesta, e di solito non è durante il deploy: è qualche giorno dopo, quando un cliente segnala che una fattura ha un importo sbagliato o che un nome con la lettera accentata viene salvato troncato. Il sito è online, nessun errore in pagina, eppure i dati si stanno corrompendo in silenzio. È lo scenario tipico di una migrazione trattata come un semplice aggiornamento di versione, quando invece migrare codice legacy significa gestire cambi di comportamento che non lanciano eccezioni, ma alterano i risultati.
Questo articolo non è un elenco di motivi per cui dovresti diffidare del "cugino che sa di computer". È una guida a come funziona davvero una migrazione legacy, dove fallisce, e quali tecniche la rendono controllabile. Alla fine il motivo per cui serve competenza specifica risulterà evidente da solo, perché lo avrai visto nei dettagli tecnici, non in uno slogan.
Perché una migrazione legacy non è un aggiornamento di versione
L'equivoco di fondo è pensare che portare un'applicazione da PHP 5 a PHP 8 sia come aggiornare un'app dello smartphone. In realtà tra quelle due versioni ci sono anni di rimozioni, cambi di semantica e irrigidimenti del linguaggio che trasformano codice "funzionante" in codice che si comporta diversamente. Alcuni esempi concreti che incontro in quasi ogni progetto:
- Funzioni rimosse, non solo deprecate. L'intera famiglia
mysql_*è sparita,each()non esiste più,create_function()è andata. Un codice che le usa non emette un warning: smette di funzionare o, peggio, va sostituito con qualcosa che si comporta in modo sottilmente diverso. - Type juggling più severo. PHP 8 ha cambiato il confronto tra stringhe e numeri:
0 == "foo"eratruefino a PHP 7, èfalseda PHP 8. Una sola condizione del genere dentro una logica di autorizzazione o di calcolo può cambiare il comportamento di un intero flusso senza che nulla si rompa visibilmente. - Da warning a eccezione. Molti errori che PHP 5/7 trattava come warning silenziosi (accesso a un indice di array inesistente, argomenti errati a funzioni interne) sono diventati
TypeErroroValueError. Codice che "andava" perché ignorava i warning, sotto PHP 8 cade. - Dipendenze nascoste. Estensioni abbandonate, librerie inchiodate a versioni incompatibili, codice che assume una
php.iniconregister_globalsomagic_quotesattivi (concetti che non esistono più).
Il pericolo di una migrazione legacy non sono gli errori che vedi: sono i cambi di comportamento che non lanciano nulla. Un deploy "riuscito" può nascondere una corruzione dati che scopri settimane dopo.
Per il dettaglio versione per versione ho scritto due guide operative complete, sulla migrazione da PHP 5.6 a PHP 8 e sulla checklist per portare in produzione un'applicazione da PHP 7.4 a 8.3: lì trovi gli incompatibili specifici, qui ti spiego il metodo che li rende gestibili.
Si può migrare un'applicazione PHP legacy senza riscriverla da zero?
Sì, e nella stragrande maggioranza dei casi è anche la scelta giusta. La riscrittura big-bang, quella in cui si congela lo sviluppo, si riscrive tutto in parallelo e si fa un cutover unico, è il modo più affidabile di trasformare un progetto rischioso in un disastro: il nuovo sistema insegue per mesi le funzionalità del vecchio, che nel frattempo continua a cambiare, e il giorno del passaggio nessuno sa davvero cosa si rompe. L'alternativa professionale è la migrazione incrementale, e ha un nome preciso.
Lo strangler fig: migrare un pezzo alla volta
Il pattern si chiama strangler fig application, descritto da Martin Fowler: come il fico strangolatore cresce attorno all'albero ospite fino a sostituirlo, si costruisce il nuovo sistema attorno al vecchio, intercettando una funzionalità alla volta e ridirigendola verso il codice moderno, finché del legacy non resta più nulla. In pratica significa mettere davanti all'applicazione un punto di instradamento (un reverse proxy, un router applicativo, un middleware) che decide, richiesta per richiesta, se servirla dal vecchio o dal nuovo codice.
Il vantaggio è enorme: a ogni passo il sistema è interamente in produzione e interamente funzionante, e ogni modulo migrato è piccolo abbastanza da poter essere testato, rilasciato e, se serve, annullato in isolamento. Il rischio si spalma su decine di piccoli rilasci reversibili invece di concentrarsi in un unico salto nel buio. Tra il vecchio e il nuovo si interpone spesso un anti-corruption layer, uno strato di traduzione che impedisce ai modelli di dati arcaici del legacy di contaminare il design pulito del nuovo codice.
Quando il legacy va integrato anziché sostituito subito, valgono gli stessi principi di isolamento che ho descritto parlando di come esporre un ERP legacy dietro un'API PHP senza farsi del male: si tratta il vecchio sistema come una scatola nera con un contratto esplicito, non come qualcosa da toccare a cuore aperto.
I characterization test: la rete prima di toccare il codice
C'è una regola che separa chi migra legacy con metodo da chi improvvisa: non si modifica codice legacy senza prima averlo coperto di test che ne fotografano il comportamento attuale, corretto o sbagliato che sia. Si chiamano characterization test, e non verificano cosa il codice "dovrebbe" fare: verificano cosa fa oggi, esattamente come lo fa, bug compresi. Sono la rete di sicurezza che ti dice, dopo ogni refactoring, se hai cambiato un comportamento che non volevi cambiare.
Il problema pratico è che il codice legacy è quasi sempre scritto in modo da rendere i test difficili: dipendenze globali, query e logica mischiate, nessuna separazione tra I/O e regole di business. La caratterizzazione iniziale è quindi spesso fatta ad alto livello, sugli endpoint e sull'output, prima di poter scendere ai singoli metodi. È esattamente il tipo di lavoro su cui gli strumenti moderni aiutano molto: ho documentato come uso un LLM per generare test di copertura su una codebase PHP legacy e portarla rapidamente a una soglia di sicurezza, che è precisamente la rete che serve prima di iniziare a strangolare il vecchio codice.
Il livello dati: dove le migrazioni muoiono davvero
Se c'è un punto in cui le migrazioni improvvisate falliscono in modo silenzioso e costoso, è il livello dati. Spostare il codice da mysql_* a mysqli o PDO non è una sostituzione meccanica di nomi di funzione: cambia la gestione del charset, dell'escaping, dei tipi restituiti e degli errori. Ho dedicato una guida specifica al porting delle funzioni mysql verso mysqli, ma i punti dove ho visto perdere dati sono ricorrenti:
- Il charset. Una vecchia connessione
mysql_*spesso girava inlatin1di fatto, e i dati erano salvati con una codifica incoerente. Passando a una connessione moderna che dichiarautf8mb4, i caratteri accentati o gli emoji possono troncarsi o trasformarsi in mojibake se la migrazione del charset dei dati non è gestita separatamente da quella del codice. - Lo strict mode di MySQL. Le versioni recenti rifiutano per default valori che le vecchie troncavano in silenzio: un campo troppo lungo non viene più tagliato, l'INSERT fallisce. È il classico errore "Data truncated for column" che emerge solo in produzione, su dati reali.
- Le query implicite. Codice che si affidava a conversioni di tipo permissive o a
GROUP BYnon standard si comporta diversamente sotto le modalità SQL moderne.
La regola è trattare la migrazione dei dati come un progetto a sé, con un dump verificabile, un ambiente di staging che contenga dati reali (anonimizzati) e un confronto riga per riga tra prima e dopo sui campi critici. Il dato è l'unica cosa dell'applicazione che non puoi rigenerare: il codice si riscrive, una fattura corrotta no.
Le fasi di una migrazione fatta con metodo
Messi insieme questi principi, il percorso che applico ha una forma stabile, indipendente dalla dimensione del progetto.
Audit tecnico e funzionale. Si parte mappando cosa fa l'applicazione e come è fatta dentro: versioni, estensioni, dipendenze, punti di accesso al database, integrazioni esterne, e soprattutto le funzionalità che il business considera critiche. L'output è un inventario dei rischi, non un elenco di file.
Rete di sicurezza. Prima di toccare qualunque cosa, si costruiscono i characterization test sugli endpoint e sui flussi critici. È la fase che gli improvvisatori saltano, ed è la ragione per cui le loro migrazioni "sembrano andare" finché un cliente non scopre il contrario.
Strangolamento incrementale. Si introduce il punto di instradamento e si migra un modulo alla volta, dal meno rischioso al più critico, con un rilascio piccolo e reversibile per ciascuno. A ogni passo i test confermano che il comportamento osservabile non è cambiato.
Parallel run sui flussi critici. Per le funzioni dove un errore costa caro (fatturazione, pagamenti, prenotazioni) si fa girare vecchio e nuovo in parallelo sugli stessi input, confrontando gli output prima di spegnere il legacy. È la differenza tra "spero che funzioni" e "ho la prova che produce gli stessi risultati".
Cutover e formazione. Solo quando il nuovo percorso è dimostrabilmente equivalente si spegne il vecchio, e si affianca il personale interno alla piattaforma aggiornata, perché una migrazione tecnica perfetta che nessuno sa usare resta un fallimento operativo.
Lungo tutto il percorso, modernizzare non vuol dire solo cambiare versione: dove ha senso si introducono i principi SOLID e un service layer per rendere il codice finalmente manutenibile, ma sempre in modo incrementale, mai come scusa per la riscrittura totale.
Gli errori che trasformano una migrazione in un incidente
Il caso che vedo ripetersi ha sempre la stessa forma. Un gestionale del settore turistico viene migrato in fretta da una vecchia versione di PHP a una recente: le query mysql_* vengono sostituite a tappeto, il codice gira, il deploy "riesce". Nessuno però ha gestito il cambio di charset né attivato un confronto dei dati: per qualche giorno le prenotazioni con nomi accentati si salvano troncate, e quando il problema emerge è già finito nei dati di produzione e nelle fatture. Il recupero costa molto più di quanto sarebbe costata una migrazione fatta con metodo, perché ora bisogna prima capire quali record sono corrotti e poi ricostruirli.
Le radici di questi disastri sono quasi sempre le stesse tre. La prima è partire senza audit né rete di test, cioè muoversi a occhi chiusi su un sistema di cui non si conosce il comportamento reale. La seconda è scegliere sul prezzo iniziale più basso, che è una decisione miope: il costo vero di una migrazione non è chi la fa partire, ma chi la rimette in piedi quando si rompe. La terza è trattare i dati come un dettaglio, quando sono l'unica parte non rigenerabile dell'intero sistema. Se stai affrontando questo tipo di progetto e vuoi capire dove sono i tuoi rischi reali prima di muoverti, nella mia pagina professionale trovi l'esperienza concreta su modernizzazione e refactoring di codebase legacy senza riscritture big-bang.
Domande frequenti sulla migrazione legacy
Conviene riscrivere da zero o migrare il codice esistente?
Nella grande maggioranza dei casi conviene migrare in modo incrementale, non riscrivere. La riscrittura big-bang congela l'evoluzione del prodotto per mesi, insegue un bersaglio mobile e concentra tutto il rischio nel giorno del passaggio. Lo strangolamento progressivo, al contrario, mantiene il sistema sempre in produzione e spalma il rischio su tanti piccoli rilasci reversibili. La riscrittura totale ha senso solo quando il legacy è talmente compromesso da non poter più essere isolato e testato, ed è una decisione che va presa con dati alla mano, non per istinto.
Quanto dura una migrazione legacy?
Dipende dalla superficie funzionale e dalla qualità del codice di partenza, ma proprio perché si procede a moduli reversibili la domanda giusta non è "quanto dura tutto", bensì "quando il primo modulo critico è migrato e in sicurezza". Un percorso incrementale produce valore e riduzione del rischio fin dai primi rilasci, invece di chiedere mesi di lavoro al buio prima di vedere qualcosa in produzione.
La migrazione come riduzione del rischio, non come acquisto
Una migrazione legacy ben fatta non è una spesa tecnologica fine a sé stessa: è la rimozione di un rischio operativo che cresce ogni mese che passa, perché ogni versione di PHP che va in end-of-life lascia il tuo sistema senza patch di sicurezza, e ogni anno di codice non manutenuto rende il prossimo intervento più costoso. Il valore non è "avere PHP 8" o "usare Laravel": è poter evolvere il software al ritmo del business invece di temere ogni modifica, e dormire sonni tranquilli sul fatto che i dati restino integri.
La differenza tra un'evoluzione controllata e un incidente sta quasi tutta nelle scelte che non si vedono: la rete di test prima di toccare il codice, lo strangolamento incrementale invece del salto nel buio, il livello dati trattato con il rispetto che merita. Sono esattamente le scelte che un approccio improvvisato salta, perché non producono nulla di visibile finché non è troppo tardi. Se hai un'applicazione legacy che ti serve ancora e che non puoi permetterti di rompere, contattami: partiamo da un audit dei rischi reali e costruiamo un percorso in cui ogni passo è piccolo, testato e reversibile, così che la migrazione diventi una leva per crescere e non l'origine del prossimo problema.