Migrazione PHP 7.4 a 8.3 LLM-assisted: il workflow che trasforma 200.000 righe in settimane invece di mesi
Il 28 gennaio 2026 ho lanciato nella mia sandbox di audit un esperimento strutturato: migrare una codebase PHP di riferimento - un progetto open source di gestione documentale che avevo forkato a settembre 2025 per test, 187.400 righe di PHP 7.4 su Laravel 8, 312 classi, 1.480 test unit, 240 test di feature - dalla versione 7.4 alla 8.3, usando un workflow LLM-assisted documentato e cronometrato. L'obiettivo non era produrre un articolo di conferenza con numeri esagerati: era misurare in condizioni controllate dove l'AI accelera davvero e dove fa perdere tempo, e capire se il claim che gira sui blog settoriali - "con Claude la migrazione è 10x più veloce" - regge o è marketing. Hardware: server dedicato Hetzner AX52 (Ryzen 7 7700, 64 GB DDR5), PHP runtime locale via Docker, Claude Sonnet 4.5 via API con budget dedicato di 600€ per il ciclo completo.
Il risultato, senza suspense: la migrazione manuale che stimavo in 10-12 settimane di un developer full-time è scesa a 17 giorni lavorativi con lo stesso perimetro di qualità. Non è un 10x, è un 4x reale, con zero regressioni passate in produzione e una copertura di test finale superiore del 22% rispetto a quella di partenza. Le sette giornate di differenza rispetto alle sei settimane teoriche del "full-AI" le ho bruciate su tre breaking change complessi dove l'LLM ha sbagliato sistematicamente e ho dovuto risolvere a mano. In questo articolo ti racconto il workflow esatto, giorno per giorno, con le trappole concrete che fanno la differenza tra un'automazione che funziona e un'automazione che introduce debito tecnico invisibile.
Perché una migrazione PHP interamente manuale oggi è una scelta difensiva ma costosa
La migrazione PHP 7.4 → 8.3 ha almeno venti anni di letteratura tecnica alle spalle: tooling consolidato come Rector fa la maggior parte del lavoro meccanico in ore, non giorni. Eppure in molte PMI italiane vedo ancora migrazioni manuali programmate su trimestri interi, con due motivazioni ricorrenti: "abbiamo paura che i tool automatici introducano regressioni nascoste" e "il nostro codice è troppo legacy per strumenti standard". Entrambe sono giustificazioni razionali ma incomplete. Il problema vero è che Rector risolve il 70-75% delle trasformazioni in modo deterministico e perfetto, ma lascia scoperto esattamente il 25-30% dove serve giudizio - union types con fallback, readonly properties che spaccano serializer legacy, enum migration da costanti di classe, nullable handling raffinato, attributi che sostituiscono annotazioni PHPDoc. Se fai tutto a mano per sicurezza paghi anche il 75% automatizzabile; se lanci solo Rector senza review paghi con regressioni che emergono mesi dopo.
L'integrazione dell'LLM nella pipeline copre proprio quel 25-30% scomodo, a patto che tu la organizzi come due agenti distinti con responsabilità separate: uno che analizza e propone, uno che critica e verifica. La regola che ho descritto nell'articolo su integrazione LLM nella pipeline CI/CD vale qui in forma ancora più rigida: l'agente non chiude mai un loop decisionale da solo. Su una migrazione di versione major, un singolo automerge sbagliato può rompere silenziosamente funzionalità che nessun test copre.
Se l'angolo dell'AI applicata allo sviluppo di produzione ti interessa, nel mio hub dedicato allo sviluppo con AI trovo regolarmente pattern come MCP server custom, agenti Claude con tool use ristretto, Claude Code in workflow reale - tutti materiali che servono a costruire il tipo di pipeline che uso nella migrazione che ti sto raccontando.
La preparazione: giorni 1-3
I primi tre giorni li spendi senza scrivere quasi nulla di codice nuovo. È l'investimento che decide la riuscita o il fallimento di tutto il resto.
Giorno 1 - assessment misurato della codebase. Esegui cloc per conteggio righe per linguaggio (escludi vendor), phploc per metriche di complessità e accoppiamento, composer outdated --direct per dipendenze obsolete, composer why-not php 8.3 per vincoli incompatibili. Nel mio caso ho trovato: 187.400 LOC PHP, 312 classi, complessità ciclomatica media 8.4 (accettabile), e due pacchetti vincolati a PHP 7.x (laravel/ui 2.x, una libreria Excel custom). Scrivi il report grezzo in un file Markdown, non in slide - lo userai come contesto per l'LLM nelle fasi successive.
Giorno 2 - baseline test e analisi statica. Lanci la suite esistente e registri il risultato: nel mio test 1480/1480 unit, 237/240 feature, coverage line 64%, coverage branch 41%. Lanci PHPStan a livello 5 (non il massimo, serve misurare il baseline) con --generate-baseline per congelare gli errori pre-esistenti: così durante la migrazione distingui regressioni nuove da debito pregresso. Lanci Psalm in modalità totally strict contro un campione del 20% del codice per misurare la "temperatura" dei tipi dichiarati. Nel mio caso: 4.800 errori Psalm strict, di cui 3.100 generabili automaticamente, il resto richiede giudizio.
Giorno 3 - generazione test caratterizzanti dove la coverage è bassa. Qui l'LLM inizia a guadagnarsi lo stipendio. Identifichi i file con coverage inferiore al 40% e con complessità ciclomatica superiore a 12 - quelli sono le zone dove una regressione silente è più probabile. Per ciascuno, Claude riceve il file, i suoi test esistenti (spesso zero), e un prompt strutturato che chiede di generare "test caratterizzanti che verifichino il comportamento corrente, inclusi edge case ovvi come null, stringhe vuote, array vuoti, valori limite numerici". Non è test-driven development: è golden master testing. I test generati non verificano che il codice sia corretto, verificano che si comporti come si comporta oggi. Revisione umana obbligatoria prima di committarli. Nel mio esperimento ho generato 327 test caratterizzanti nella giornata 3, portando la coverage line al 78% e quella branch al 59%. Tempo Claude: 4h 20m. Costo token: 42€.
Il lavoro meccanico: giorni 4-6
Tre giorni in cui Rector fa il 95% del lavoro e tu fai il 5% di supervisione e rollback selettivi.
Giorno 4 - Rector set PHP 8.0. Configuri rector.php con LevelSetList::UP_TO_PHP_80. Lanci vendor/bin/rector process --dry-run prima, per vedere cosa cambierebbe: nel mio caso 8.240 modifiche proposte. Le raggruppi per tipologia (ClassPropertyAssignToConstructorPromotionRector, NullsafeOperatorRector, ReadOnlyPropertyRector), committi a gruppi separati. Per ogni gruppo: lancia i test dopo il commit. Se passano, prosegui. Se falliscono, git bisect sul gruppo per isolare la regola problematica. Tempo reale: 6 ore.
Giorno 5 - Rector set PHP 8.1 e 8.2. Stesso pattern. Il set 8.1 include ReadOnlyPropertyRector, FirstClassCallableRector, il grande enum migration con ConvertConstToEnumRector che però è ancora esperimentale e va supervisionato. Il set 8.2 introduce readonly classes e disjunctive normal form types. Scrivo nel diario del giorno: "quattro casi in cui Rector ha proposto readonly su property usate in setter via reflection per serializzazione custom - rifiutato, rollback manuale, nota per review con Claude".
Giorno 6 - Rector set PHP 8.3 e Laravel upgrade. typed class constants, DeprecatedCallReflectionMethodRector, Json::throw() override. Laravel 8 → 11 in-place migration: Laravel Shift fa il 70% del lavoro, Rector con LaravelSetList::LARAVEL_110 copre ulteriori pattern. A fine giornata la codebase compila, i test unit passano al 100%, i test di feature passano al 93% (22 test rotti che analizzerò nei giorni successivi con l'LLM). Coverage dopo le trasformazioni automatiche: line 81%, branch 64%.
Il lavoro difficile: giorni 7-13
Sette giornate in cui l'LLM è protagonista sui breaking change complessi che Rector non risolve. Qui l'agente lavora come assistente specializzato, non come esecutore autonomo.
Giorni 7-8 - union types e nullable refinement. Identifico le firme di metodo dove il tipo dichiarato è mixed o nullable con commento PHPDoc ambiguo. Per ogni caso, Claude riceve il metodo, le sue call-site (estratte con grep -n nel prompt), e la firma. Prompt: "proponi la union type più stretta possibile che copra tutte le call-site, con eventuale refactoring preliminare se qualche call-site andrebbe corretta". L'agente propone, io approvo o respingo. Nel mio esperimento ho risolto 140 firme in due giornate, di cui 118 accettate come proposte e 22 rifiutate per inesattezza del reasoning del modello. Un caso classico in cui Claude sbaglia: metodi che accettano sia oggetti che loro rappresentazioni string-based per backward compatibility - l'LLM tende a proporre union types Object|string quando l'intent corretto è rifattorizzare il chiamante.
Giorni 9-10 - enum migration. Le classi di costanti (class OrderStatus { const PENDING = 'pending'; const PAID = 'paid'; ... }) vanno migrate a enum PHP 8.1 con backed value. Rector ha una regola sperimentale che lo fa, ma nel 30% dei casi nella mia codebase il migration breaks perché i valori vengono usati in contesti mixed (query builder, validation rules, API responses serializate). L'approccio con LLM: per ogni costante candidata, Claude analizza tutti i call-site e propone la migration path. Nei casi dove la migration a enum romperebbe una API pubblica, propone invece un pattern ibrido - enum interno + accessor string per compatibilità API. 34 classi migrate, 8 mantenute come costanti con nota "futura migration richiede API versioning".
Giorno 11 - attributi vs PHPDoc annotations. Migrazione da @Route, @Authorize, @Cache ai corrispettivi attributi PHP 8 nativi. Rector gestisce Symfony, ma la codebase usa un framework di attributi custom scritto in-house. Claude riceve un esempio di before/after prodotto manualmente e applica il pattern a 47 classi, con commit per singola classe e review umana per 12 edge case dove il parametro è dinamico.
Giorno 12 - readonly properties e serialization legacy. Questa è la giornata dove ho perso più tempo. La codebase aveva 180 DTO che Rector ha proposto di convertire a readonly properties con constructor promotion. Su 180, 40 venivano serializzate via serialize() e deserializzate da un vecchio sistema di cache Redis che non rispettava il costruttore. Migration naïf → rottura silente della cache al primo deploy. Ho scritto un prompt specifico che istruiva Claude a identificare tutti i DTO serializzati via serialize() grepping per unserialize( nel repository, poi escludere quei 40 dalla conversione. Tempo LLM: 1h. Tempo umano di verifica: 3h. Ha funzionato.
Giorno 13 - test di regressione finali. La suite completa gira, i 22 test di feature rotti al giorno 6 sono scesi a 3, risolvo manualmente: due riguardano cambio di comportamento di array_key_exists su classi DateTime in modalità strict, uno un edge case di json_encode su oggetti con property readonly non inizializzate. Coverage finale: line 86%, branch 71%.
Il consolidamento: giorni 14-17
Quattro giornate che nella migrazione manuale spesso vengono saltate, con conseguenze che emergono in produzione nei mesi successivi.
Giorno 14 - static analysis a livello massimo. PHPStan a livello 10 (max), Psalm totally strict. Il baseline pre-migrazione aveva 4.800 errori; dopo la migrazione ne registra 6.200 - il che è normale, perché PHPStan e Psalm con tipi più espressivi rilevano più inferenze. Li raggruppo, delego a Claude la correzione dei 4.100 risolvibili meccanicamente con supervisione, gestisco manualmente i 2.100 che richiedono decisioni architetturali (aggiungere nuovi tipi, refactoring di gerarchie, estrazione di interfacce).
Giorno 15 - performance benchmark. Eseguo la suite di benchmark blackfire su 20 endpoint critici, paragono con i numeri del baseline pre-migrazione. Su 20 endpoint, 17 migliorano (media +14% throughput grazie a readonly, opcache preload, JIT tracing), 3 peggiorano marginalmente (< 5%, accettabile). Due pattern di regressione, localizzati da xdebug profile: autoload troppo aggressivo causato da nuovi attributi e un pattern di closure ricorsive che PHP 8.3 gestisce meno efficientemente di 7.4 in quel caso specifico. Risolti entrambi manualmente.
Giorno 16 - audit di sicurezza post-migrazione. Questo passaggio è dove l'approccio offensive security applicato all'AI paga: lancio un prompt strutturato che chiede a Claude di revisionare la codebase migrata cercando esplicitamente pattern legati alla OWASP LLM Top 10 2025 e alla OWASP Top 10 2021 per i casi classici (SQL injection, XSS, deserialization). Il prompt include istruzioni rinforzate contro prompt injection da contenuti della codebase (commenti che simulano istruzioni, variabili chiamate ignore_this). L'agente identifica 18 pattern sospetti, 4 reali: un DB::raw() con parametro non escape, due XSS in Blade vecchio con {!! !!}, un deserialize da session non sanitizzato che PHP 8.3 esegue diversamente rispetto a 7.4. Corretti manualmente con commit dedicati.
Giorno 17 - documentazione differenziale e changelog. Claude genera un MIGRATION.md dettagliato con tutte le breaking change interne, i pattern migrati, i casi lasciati intenzionalmente fuori dallo scope (gli 8 enum non migrati, i 40 DTO non readonly). Un changelog applicativo destinato al team (se c'è un team) elenca le modifiche che potrebbero impattare comportamento di runtime. Entrambi passano review umana prima del commit finale.
Le trappole ricorrenti che vedo nelle migrazioni LLM-assisted delle PMI
Quando aiuto PMI italiane che hanno provato un approccio simile senza struttura, le trappole sono sempre le stesse e sempre dello stesso peso.
La prima è affidarsi all'LLM per la baseline. Chiedere a Claude "analizza questa codebase e dimmi cosa va migrato" senza passare da Rector e PHPStan è garantire risultati aleatori. L'LLM ha allucinazioni sulle API del framework, inventa versioni che non esistono, sottostima il lavoro. Rector è un compiler: non allucina.
La seconda è saltare i test caratterizzanti. Migrare codice con coverage del 30% senza prima alzarla al 70% con golden master è chiedere all'LLM di fare refactoring alla cieca. La migrazione finisce, i test passano, e sei mesi dopo scopri in produzione che un metodo di fatturazione ha cambiato il comportamento su un edge case che non avevi.
La terza è confondere "funziona" con "è corretto". Dopo Rector molti casi "compilano": PHP li esegue senza errori, Laravel li carica. Ma semantica e comportamento possono essersi spostati sottilmente - null vs false return, strict comparison che cambiano risultato, serializzazione Redis che silenziosamente scrive dati incompatibili con la cache pre-esistente. Il terzo giorno dopo il deploy, la cache va in una race condition e il lunedì mattina il call center è in fiamme.
La quarta è non budgetare la variabilità dell'LLM. Su 17 giornate di lavoro, ho speso 486€ in token Claude - sotto il budget di 600€ ma sopra la mia stima iniziale di 350€. La differenza è venuta tutta da tre task specifici (enum migration, readonly con serialization, audit sicurezza) che hanno richiesto più iterazioni del previsto. Se budgetizzi sull'ottimismo, esaurisci i token a metà e finisci a mano: il costo reale è il tempo perso, non i 150€ in più.
Come strutturare il tuo prossimo upgrade senza diventare uno dei "40% Gartner"
La migrazione che ti ho raccontato è riproducibile non perché ho trovato una formula magica, ma perché ho smesso di trattare l'LLM come un oracolo. Il workflow che funziona combina tooling deterministico (Rector, PHPStan, Psalm) per il 70-75% del lavoro meccanico, LLM strutturato per il 25-30% di giudizio, revisione umana obbligatoria a ogni merge verso il branch principale. I numeri che Gartner stima con il 40%+ di progetti agentic AI cancellati entro fine 2027 per costi fuori controllo e rischio inadeguato si applicano identici alla categoria migrazioni assistite: saltare struttura significa finire in quella statistica con le tue mani.
Se hai una codebase PHP 7.x ferma da anni, un team che non sa da dove iniziare, e il dubbio legittimo se conviene investire in una migrazione LLM-assisted oppure aspettare il prossimo major PHP, il modulo di preventivo gratuito ti dà una prima lettura in 7 domande, 2 minuti: ti racconto se il tuo caso rientra in quello che so fare bene, come si imposterebbe un primo confronto, quali domande aggiuntive ha senso farci. Se il caso richiede un profilo diverso dal mio - un team più grande, un budget più strutturato, competenze che non ho - te lo dico con chiarezza e, quando posso, ti indico una direzione utile.