Modernizzare applicazioni legacy in PHP: una guida pratica con Laravel e Symfony per la sicurezza e la crescita della tua applicazione web
Ho perso il conto delle volte in cui un imprenditore mi ha riassunto il suo problema con la stessa frase: "il gestionale funziona, ma ogni volta che dobbiamo cambiare qualcosa è un disastro". Dietro quella frase c'è quasi sempre lo stesso scenario, e lo riconosco prima ancora di aprire il codice: un applicativo PHP costruito otto o dodici anni fa, scritto in PHP 5.x senza framework, senza test automatici, spesso senza nemmeno il version control, su cui si sono stratificate le modifiche di tre o quattro sviluppatori diversi che non si sono mai parlati. Il codice "funziona" nel senso che produce l'output giusto, ma nessuno ha più il coraggio di toccarlo: ogni modifica rischia di rompere qualcos'altro a tre tabelle di distanza, e nessuno sa dire dove.
Questa è la fisiologia del debito tecnico nelle PMI italiane, ed è un terreno su cui lavoro da vent'anni: scrivo PHP dal 2003, dai tempi della versione 4, e una buona parte della mia attività di consulenza è esattamente il recupero di codebase ventennali che nessuno documenta più. In questo articolo ti mostro perché la modernizzazione non è un lusso ma una necessità operativa, quali rischi concreti stai correndo adesso, e soprattutto come affrontare il percorso con Laravel o Symfony senza mai fermare il business: non con una riscrittura eroica, ma con una strategia incrementale che un titolare può finanziare e misurare un pezzo alla volta.
Cosa succede davvero quando un'applicazione PHP invecchia?
Il problema non è "PHP è vecchio". PHP è un linguaggio moderno e performante: la current stable è la 8.5, uscita a novembre 2025, con type system solido, enum, readonly properties, property hooks. Il problema è il codice scritto in PHP 5.x senza architettura, che accumula rischio su tre fronti distinti e simultanei.
Il primo è la sicurezza del runtime. Le versioni datate di PHP non ricevono più patch: secondo le supported versions ufficiali, persino la 8.2 esce dal supporto di sicurezza il 31 dicembre 2026, e tutto ciò che gira su 7.4, 5.6 o 5.3 è scoperto da anni. Ma il runtime è la parte facile, perché si aggiorna. Il pericolo vero vive nel codice applicativo: query SQL concatenando stringhe, input utente usato senza sanitizzazione, sessioni senza protezione CSRF, upload senza validazione. La OWASP Top 10:2025, pubblicata nella sua versione finale a gennaio 2026, mette al primo posto Broken Access Control (A01, che ora assorbe anche la SSRF) e introduce come novità di rilievo le Software Supply Chain Failures (A03): le dipendenze del tuo applicativo, librerie scaricate anni fa e mai più aggiornate, sono ora ufficialmente un vettore d'attacco di primo livello. È esattamente ciò che trovo in ogni audit su codice legacy.
Il secondo fronte sono i costi di manutenzione, ed è quello che il titolare sente sul conto corrente anche senza capirne la causa. Un applicativo senza test, senza separazione delle responsabilità, senza dependency injection, ha un costo di modifica che cresce in modo non lineare. Ho misurato casi concreti, sempre lo stesso ordine di grandezza: aggiungere un campo a un form in un'applicazione legacy richiede quattro o sei ore, perché devi trovare tutti i punti impattati a mano, testare cliccando, e deployare via FTP sperando. La stessa identica modifica su un'applicazione Laravel strutturata richiede venti, trenta minuti. Moltiplica la differenza per duecento modifiche l'anno e hai il prezzo del debito tecnico, espresso in giornate-uomo che il tuo fornitore ti fattura senza che tu sappia perché.
Il terzo fronte è la conformità normativa, e in Italia ha smesso di essere teoria. La Direttiva NIS2 (UE 2022/2555) è stata recepita con il D.Lgs. 138/2024, in vigore dal 16 ottobre 2024, e il 2026 è l'anno in cui le scadenze diventano operative: secondo le determinazioni dell'ACN, le misure di sicurezza di base vanno implementate entro il 31 ottobre 2026, data dalla quale l'Agenzia può anche avviare le verifiche ispettive. Un applicativo legacy, semplicemente, non sa fare ciò che la norma chiede: logging strutturato degli accessi, gestione tracciata degli incidenti, sicurezza della supply chain software. Le sanzioni arrivano fino a 10 milioni di euro o al 2% del fatturato per i soggetti essenziali. Ne ho scritto operativamente nella checklist di hardening per Laravel e Symfony allineata a NIS2.
Ecco un esempio che trovo in quasi ogni audit. Questo codice è reale, estratto e anonimizzato da un gestionale in produzione presso un'azienda del settore servizi:
// Codice legacy reale: vulnerabile a SQL injection, XSS e path traversal
$id = $_GET['id'];
$result = mysql_query("SELECT * FROM clienti WHERE id = $id");
$row = mysql_fetch_assoc($result);
echo "<h2>Cliente: " . $row['nome'] . "</h2>";
echo "<a href='download.php?file=" . $row['allegato'] . "'>Scarica</a>";Tre vulnerabilità in sei righe: SQL injection (l'input finisce nella query senza filtri), XSS (l'output non è escaped), path traversal potenziale (il nome del file viene dal database e arriva intatto in un link di download). Con Laravel, lo stesso risultato funzionale diventa sicuro per costruzione:
// Laravel: sicuro by default
public function show(int $id): View
{
$cliente = Cliente::findOrFail($id); // query parametrizzata, 404 automatico
return view('clienti.show', compact('cliente'));
// Blade escapa l'output automaticamente: {{ $cliente->nome }}
// Il download passa da Storage::download(), con validazione del path
}Con Symfony l'esito è identico, con un'enfasi maggiore sull'esplicitezza dei tipi e del routing:
// Symfony: sicuro by default
#[Route('/clienti/{id}', methods: ['GET'])]
public function show(int $id, ClienteRepository $repo): Response
{
$cliente = $repo->find($id) ?? throw $this->createNotFoundException();
return $this->render('clienti/show.html.twig', ['cliente' => $cliente]);
// Twig escapa l'output automaticamente: {{ cliente.nome }}
}La differenza non è cosmetica, è strutturale: type-hinting sull'id (niente injection sul parametro), ORM con query parametrizzate, template engine con auto-escaping, routing dichiarativo. Il codice sicuro non richiede disciplina eroica a ogni riga: è il comportamento di default del framework. Ed è qui che si misura la distanza tra "scrivere PHP" e "ingegnerizzare un'applicazione".
Perché Laravel o Symfony e non una riscrittura "da zero"?
La domanda me la fanno quasi tutti, ed è la più pericolosa: "ma non sarebbe meglio rifare tutto pulito?". La risposta breve è: quasi mai. La riscrittura completa, il cosiddetto Big Bang, ha tre problemi che per una PMI sono spesso fatali.
Il primo è il tempo. Un gestionale con cinquanta o cento tabelle e duecento schermate richiede da sei a diciotto mesi per essere riscritto da zero. Nel frattempo il business non si ferma: continua a girare sul sistema vecchio, che riceve modifiche urgenti che nessuno riporta nel nuovo. Quando il nuovo sistema è "pronto", è già indietro rispetto al vecchio. Il secondo è il rischio: il secondo sistema replica gli errori del primo e ne aggiunge di propri, è il Second System Effect descritto da Fred Brooks mezzo secolo fa e ancora attualissimo. Il terzo è il costo: il budget di una riscrittura totale è tre o cinque volte quello di una modernizzazione incrementale, con un ritorno che arriva molto più tardi, quando arriva.
L'alternativa professionale è lo Strangler Fig Pattern, il pattern coniato da Martin Fowler: il sistema nuovo cresce attorno al vecchio come un fico strangolatore attorno al suo albero, sostituendone i moduli uno alla volta, mentre il vecchio continua a servire tutto il resto. Il cuore del pattern non sono i microservizi, è la coesistenza controllata con cutover graduale. In pratica, un reverse proxy davanti all'applicazione decide modulo per modulo chi serve la richiesta:
server {
location /api/clienti { proxy_pass http://127.0.0.1:8080; } # migrato a Laravel
location /api/fatture { proxy_pass http://127.0.0.1:8080; } # migrato a Laravel
location / { # tutto il resto -> legacy
root /var/www/legacy;
try_files $uri $uri/ /index.php?$args;
}
}Questo ti permette di migrare un modulo alla settimana, validarlo in produzione su traffico reale, e tornare indietro spostando una sola location se qualcosa non va. Il rischio è confinato al singolo modulo, non all'intero sistema, ed è questa la differenza tra un progetto finanziabile e una scommessa al buio.
Lo Strangler Fig non è solo una tecnica di migrazione: è un modo di trasformare un rischio enorme e indivisibile (riscrivere tutto) in una sequenza di rischi piccoli, reversibili e fatturabili a sprint. È quello che rende la modernizzazione un investimento gestibile invece di un salto nel vuoto.
Stai valutando una modernizzazione su un applicativo critico per la tua azienda e vuoi capire se il percorso incrementale è applicabile al tuo caso? Nel mio profilo professionale trovi l'esperienza concreta sul porting di codebase PHP ventennali verso le versioni moderne, dal coordinamento di team offshore alla messa in sicurezza di gestionali in produzione, senza riscritture big bang.
Come si migra un modulo senza romperlo: i test di caratterizzazione
C'è un anello che la maggior parte degli interventi fai-da-te salta, e che invece è il vero abilitatore dello Strangler Fig: prima di riscrivere un modulo legacy, devi sapere cosa fa davvero, non cosa dovrebbe fare. La documentazione non esiste, e il comportamento corretto è quello che il codice produce oggi, bug compresi su cui magari un cliente fa affidamento. Lo strumento si chiama test di caratterizzazione (characterization test), il concetto chiave di Michael Feathers in Working Effectively with Legacy Code: non scrivi un test che verifica la correttezza secondo una specifica, scrivi un test che fotografa l'output attuale del codice legacy per un insieme di input reali, e lo congeli come riferimento.
// Test di caratterizzazione: fotografa il comportamento ATTUALE, non quello "giusto"
public function test_calcolo_imponibile_replica_il_legacy(): void
{
// Input estratti da casi di produzione reali (anonimizzati)
$output = (new CalcoloImponibile())->calcola(ordineId: 48213);
// Non sto verificando la "regola fiscale corretta":
// sto bloccando l'output che il legacy produce OGGI, arrotondamenti compresi.
$this->assertSame(1247.86, $output->imponibile);
$this->assertSame(274.53, $output->iva);
}Con una rete di test di caratterizzazione attorno al modulo, puoi riscriverlo in Laravel o Symfony e confrontare bit per bit il nuovo output con il vecchio: se diverge, lo sai subito, e sai esattamente dove. È la rete di sicurezza che trasforma un refactoring spericolato in un'operazione controllata. Quando un progetto non ha nemmeno Git, il punto di partenza è proprio questo, e ne ho scritto la pratica minima nell'articolo su come introdurre test in un progetto legacy senza bloccare lo sviluppo.
L'anti-corruption layer: far parlare il vecchio e il nuovo senza contaminarli
Durante la migrazione, il modulo nuovo e quello vecchio devono convivere e spesso chiamarsi a vicenda: il controller Laravel della fatturazione ha bisogno dei dati anagrafici che ancora vivono nelle tabelle e nelle convenzioni del legacy. Se lasci che il modello dati vecchio (colonne con nomi criptici, flag a stringa, date in formati incoerenti) penetri direttamente nel codice nuovo, hai appena infettato il nuovo con il debito del vecchio. La contromisura è l'anti-corruption layer: un sottile strato di traduzione che converte le strutture legacy nei modelli puliti del nuovo dominio, e isola il codice moderno da ogni stranezza dell'eredità.
// Anti-corruption layer: traduce il modello legacy senza farlo entrare nel dominio nuovo
final class LegacyClienteAdapter
{
public function __construct(private LegacyDb $legacy) {}
public function find(int $id): Cliente
{
$row = $this->legacy->query('SELECT * FROM anag_cli WHERE cli_id = ?', [$id]);
// Qui assorbo TUTTE le incoerenze del legacy: nomi colonna, flag a stringa, date
return new Cliente(
id: $id,
ragioneSociale: trim($row['cli_rag_soc']),
attivo: $row['cli_stato'] === 'A', // 'A'/'D' -> bool
partitaIva: $this->normalizzaPiva($row['cli_piva']),
);
}
}Il dominio nuovo conosce solo l'oggetto Cliente pulito e tipizzato; non sa, e non deve sapere, che dietro c'è una tabella anag_cli con vent'anni di abbreviazioni. Quando un giorno migrerai anche l'anagrafica, butterai via l'adapter e nient'altro. Questo è ciò che impedisce alla modernizzazione di diventare un nuovo strato di legacy.
Quando Laravel e quando Symfony
Tra i due framework non c'è un vincitore assoluto, c'è un'aderenza al problema. Laravel è la scelta più rapida quando il progetto è un'applicazione web a forte componente CRUD, con API RESTful e autenticazione utenti: la curva di apprendimento bassa e l'ecosistema orientato alla produttività (Eloquent, Blade, Artisan, le code) accelerano la migrazione. La versione corrente è Laravel 13, rilasciata il 17 marzo 2026, che richiede come minimo PHP 8.3 e porta in casa un AI SDK first-party e la ricerca vettoriale nativa, oltre a una serie di attributi PHP che riducono il boilerplate. Per chi parte oggi, è il riferimento; chi è fermo a Laravel 11 va avvisato che la sua finestra di sicurezza si è chiusa a marzo 2026.
Symfony è preferibile per applicazioni enterprise con logica di business complessa, integrazioni multiple e requisiti di modularità estrema: l'architettura a componenti e il container di dependency injection lo rendono ideale per sistemi pensati per durare oltre dieci anni. Sul fronte versioni, la scelta professionale è netta: per un progetto che deve essere stabile e a lungo manutenibile raccomando Symfony 7.4 LTS, uscita a novembre 2025 con supporto di sicurezza fino a novembre 2029, mentre la 8.1 è la current stable per chi vuole le feature più recenti. La trappola da evitare è Symfony 8.0, in fine vita a luglio 2026: chi ha saltato la 7.4 per inseguire la 8.0 si è messo da solo in un angolo.
Come si pianifica la modernizzazione? I passi che seguo con ogni cliente
Non esiste una ricetta universale, ma dopo molti interventi ho cristallizzato un metodo che regge per PMI dai cinque ai cinquanta dipendenti con un applicativo PHP critico in produzione. Lo presento come una sequenza, ma in pratica i confini tra le fasi sono porosi.
Si parte sempre dall'audit tecnico, una o due settimane in cui non tocco una riga di codice ma mappo il sistema: versione PHP reale, dipendenze e loro stato, copertura di test (quasi sempre zero), complessità, vulnerabilità note ai punti di ingresso. L'output è un report che classifica il debito per impatto e rischio, non un elenco di lamentele. Il metodo completo è nell'articolo sull'audit tecnico dei primi 30 giorni. Da lì discende il triage: non tutto va modernizzato, e soprattutto non subito. I moduli si dividono in critici (vulnerabilità attive sui punti di ingresso, si fanno ora), importanti (debito alto, si pianificano), differibili (funzionano, non si toccano). La regola che applico è secca: prima si mettono in sicurezza autenticazione, upload e query, poi si modernizza la struttura.
Il terzo passo è quello che le PMI sottovalutano sempre: il setup infrastrutturale. Version control, una pipeline di CI/CD anche minimale, e un ambiente di staging che replica la produzione con dati reali anonimizzati. Su questo punto sono inflessibile, perché testare con un database vuoto o con dati finti non intercetta la stragrande maggioranza dei bug veri: il legacy esplode proprio sui casi sporchi che solo i dati di produzione contengono. Solo a questo punto comincia la migrazione incrementale vera e propria, un modulo alla volta con lo Strangler Fig, ciascuno completo di test, query parametrizzate, validazione e logging. Infine l'hardening e la compliance: quando il nucleo critico è migrato, applico la checklist completa (header HTTP come CSP e HSTS, rate limiting, audit log, gestione incidenti) e il piano di Disaster Recovery, perché senza procedure di backup e ripristino testate anche il framework più moderno non ti salva da un guasto hardware o da un ransomware.
Un caso concreto chiarisce come si tengono insieme i pezzi. In una subentranza su un gestionale ordini legacy, con ottanta tabelle MySQL, frontend in PHP 5.x e HTML inline, zero test, il modulo più delicato era la fatturazione elettronica, che deve dialogare con il Sistema di Interscambio e non tollera downtime. Abbiamo migrato prima quello: un controller Laravel con validazione rigorosa, job asincroni per l'invio allo SDI, retry con backoff esponenziale, il tutto protetto da una rete di test di caratterizzazione e da un anti-corruption layer verso le anagrafiche ancora legacy. Mentre il modulo girava in produzione su Laravel, monitorato, il resto del gestionale continuava indisturbato sul vecchio codice, e l'abbiamo migrato un pezzo alla volta nei mesi successivi. Nessuna notte di go-live col cuore in gola, nessun rollback totale: solo cutover piccoli e reversibili.
Quanto costa non fare niente?
È la domanda che separa le PMI che crescono da quelle che subiscono. Il costo della modernizzazione è visibile e pianificabile; il costo del non intervento è invisibile e cumulativo, e per questo è più pericoloso. Si compone di voci che il titolare di solito non collega tra loro: ogni incidente di sicurezza che colpisce una PMI ha un costo medio nell'ordine delle decine di migliaia di euro tra ripristino, fermo e danno reputazionale; ogni ora di downtime non pianificato vale il fatturato orario dell'azienda, ventiquattr'ore su ventiquattro; ogni mese di ritardo sull'adeguamento NIS2 è un mese di esposizione diretta a sanzioni che ora hanno un calendario preciso.
La modernizzazione incrementale, sul contrario, è un investimento misurabile, distribuito su mesi e con un ritorno che si vede sul costo di gestione. Il dato che riporto sempre ai clienti è questo: dopo la migrazione, il costo medio per implementare una nuova funzionalità cala in modo netto, tipicamente di due terzi. Dove prima servivano giorni per aggiungere un report al gestionale (trovare il punto giusto, replicare la connessione al database, formattare l'output a mano, testare manualmente dieci scenari), con un framework moderno servono poche ore, perché il routing è dichiarativo, l'ORM gestisce le query, il template engine formatta e i test verificano le regressioni da soli. Su decine di richieste di modifica l'anno, sono centinaia di ore-uomo che smetti di pagare.
C'è poi un fattore meno misurabile ma altrettanto reale: la capacità di attrarre sviluppatori competenti. Un progetto Laravel o Symfony ben strutturato è leggibile e attraente per qualsiasi professionista qualificato che conosca il framework, perché le convenzioni sono condivise e documentate. Un groviglio di PHP 5.x procedurale senza documentazione, invece, non lo vuole toccare nessuno, e chi accetta lo fa a un prezzo maggiorato per compensare la sofferenza. È lo stesso meccanismo che ti mette in ginocchio il giorno in cui l'unico sviluppatore che conosceva il sistema se ne va: una situazione di cui ho scritto la procedura d'emergenza nell'articolo su cosa fare quando resti senza manutentore su codice PHP legacy. Il framework moderno non è solo un upgrade tecnico: è un upgrade della capacità della tua azienda di competere per il talento e di non dipendere da una singola persona.
Se ti riconosci in questo quadro, il punto di partenza non è scegliere il framework, è capire dove sei: un audit tecnico per fotografare il debito, classificarlo e disegnare il percorso incrementale che il tuo business può davvero sostenere. La modernizzazione del legacy PHP non è un atto di fede né una riscrittura traumatica da rimandare ancora un anno, è una sequenza di passi misurabili che mette in sicurezza ciò che oggi è esposto, abbatte il costo di ogni modifica futura e ti rende conforme a obblighi che, dal 2026, hanno smesso di essere ipotetici. Se vuoi affrontarlo con un metodo collaudato invece che a tentoni, contattami per una consulenza diretta: partiamo dall'audit e definiamo insieme cosa va fatto ora, cosa può aspettare e cosa è meglio non toccare affatto.