Analisi statica per la migrazione: PHPCompatibility, PHPStan e Rector in pratica

Come trovare i problemi di una migrazione a PHP 8 prima di cambiare versione: cosa fanno PHPCompatibility, Rector e PHPStan, come configurarli su un progetto legacy, in quale ordine usarli e cosa resta da verificare con i test.

Copertina con il logo php∞, la categoria Framework e codice custom, il titolo PHPCompatibility, PHPStan e Rector e il numero 40

Il modo più comune di migrare un’applicazione a PHP 8 è anche il più lento: installare la nuova versione su un server di prova, aprire le pagine una per una e correggere gli errori man mano che compaiono. Funziona per le pagine che qualcuno si ricorda di aprire. Le altre, come l’esportazione di fine mese, il webhook del corriere o lo script lanciato da cron alle tre di notte, si rompono dopo il rilascio, in produzione.

L’analisi statica ribalta il metodo: legge tutto il codice senza eseguirlo e produce un elenco dei problemi prima ancora di cambiare versione. In questo articolo vediamo i tre strumenti che usiamo in ogni migrazione, PHPCompatibility, PHPStan e Rector, cosa trova ciascuno, come configurarli su un progetto legacy e in quale ordine usarli perché il lavoro resti sotto controllo.

Perché l’analisi statica in una migrazione

Un’analisi statica esamina i file sorgente come testo strutturato: riconosce funzioni, classi, chiamate e tipi, e li confronta con un insieme di regole. Non ha bisogno del database, dei dati di produzione o di un utente che clicchi. Per questo copre anche il codice che si esegue di rado, che è proprio quello che i test manuali dimenticano.

Il risultato più utile, all’inizio, è la misura del lavoro. Sapere che ci sono 40 occorrenze di funzioni rimosse in 12 file, oppure 2.000 in 400 file, cambia completamente la stima dei tempi, la scelta tra una migrazione unica e una per fasi e la necessità di proteggere nel frattempo la produzione.

Tre strumenti, tre ruoli

I tre strumenti si sovrappongono poco e si completano bene. PHPCompatibility risponde alla domanda “questo codice gira sulla versione X?”. Rector applica in automatico le correzioni ripetitive. PHPStan trova gli errori che restano, cioè chiamate a metodi inesistenti, tipi sbagliati e variabili mai definite, che spesso diventano visibili proprio con PHP 8.

Tre schede a confronto. PHPCompatibility, regole per PHP_CodeSniffer: trova funzioni, estensioni e sintassi rimosse o nuove rispetto alla versione indicata con testVersion, per esempio 5.6-8.3; non modifica il codice; produce un elenco di problemi per file e riga. Rector: applica correzioni automatiche come ereg verso preg_match, each verso foreach e create_function verso funzioni anonime; si prova con dry-run e produce un diff da rivedere. PHPStan: trova chiamate a metodi inesistenti, tipi incompatibili e variabili non definite, con livelli da 0 a 10 e una baseline per gli errori già noti; non modifica il codice. PHPCompatibility 10 gira su PHP 7.2 o superiore, Rector e PHPStan su PHP 7.4 o superiore
Uno misura la compatibilità, uno corregge, uno controlla la coerenza del codice.

PHPCompatibility: cosa cambia tra le versioni

PHPCompatibility è un insieme di regole per PHP_CodeSniffer che conosce, versione per versione, funzioni, costanti, estensioni e costrutti di sintassi aggiunti, deprecati e rimossi. Gli si indica la versione di destinazione con il parametro testVersion, oppure un intervallo, e segnala ogni riga che non sarebbe compatibile.

Un dettaglio pratico conta molto. L’ultima versione stabile, la 9.3.5, risale a dicembre 2019 e non conosce buona parte dei cambiamenti di PHP 8. Le regole aggiornate fino a PHP 8.5 sono nella serie 10, che a ottobre 2026 è ancora pubblicata come versione alpha e si installa così:

# Installazione nel progetto, solo per lo sviluppo
composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev phpcompatibility/php-compatibility:"^10.0.0@dev"

# Compatibilità con PHP 8.3, escludendo le librerie di terzi
vendor/bin/phpcs -ps --standard=PHPCompatibility --runtime-set testVersion 8.3 \
  --extensions=php --ignore='vendor/*' --report-file=compatibilita-8.3.txt .

# Codice che deve girare sia su PHP 5.6 sia su PHP 8.3 durante la transizione
vendor/bin/phpcs -ps --standard=PHPCompatibility --runtime-set testVersion 5.6-8.3 \
  --extensions=php --ignore='vendor/*' --report=summary .

L’intervallo è utile quando le correzioni vanno in produzione prima del cambio di versione: segnala sia ciò che non funziona su PHP 8.3 sia l’uso di sintassi troppo recente per PHP 5.6. Il rapporto summary elenca il numero di problemi per file ed è un buon punto di partenza per la stima; il rapporto completo indica riga per riga cosa cambiare.

Se l’applicazione è basata su WordPress o Joomla, oppure usa i polyfill di Symfony o di Paragonie, esistono insiemi di regole dedicati: PHPCompatibilityWP, PHPCompatibilityJoomla, PHPCompatibilitySymfony e PHPCompatibilityParagonie. Tengono conto delle funzioni che quei progetti reimplementano per le versioni vecchie di PHP ed evitano molti falsi positivi. La versione di destinazione va indicata comunque con testVersion.

Rector: le correzioni automatiche

Rector legge il codice, applica regole di trasformazione e riscrive i file. Molte correzioni richieste da una migrazione sono ripetitive e sicure da automatizzare: ereg() che diventa preg_match(), i cicli con each() che diventano foreach, create_function() che diventa una funzione anonima, i costruttori in stile PHP 4 che diventano __construct().

Rector offre anche insiemi di regole per versione, attivabili con withPhpSets(), che però riscrivono il codice usando la sintassi della versione di destinazione. Se le modifiche devono andare in produzione mentre il server è ancora sulla versione vecchia, conviene partire da regole scelte una per una, il cui risultato funziona su entrambe:

<?php
// rector.php: solo correzioni compatibili sia con PHP 5.6 sia con PHP 8

use Rector\Config\RectorConfig;
use Rector\Php70\Rector\ClassMethod\Php4ConstructorRector;
use Rector\Php70\Rector\FuncCall\EregToPregMatchRector;
use Rector\Php72\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector;
use Rector\Php72\Rector\While_\WhileEachToForeachRector;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/public', __DIR__ . '/include'])
    ->withSkip([__DIR__ . '/include/librerie-esterne'])
    ->withRules([
        Php4ConstructorRector::class,
        EregToPregMatchRector::class,
        WhileEachToForeachRector::class,
        CreateFunctionToAnonymousFunctionRector::class,
    ]);

// Più avanti, quando la produzione è su PHP 8.3:
// ->withPhpSets(php83: true)

Il comando vendor/bin/rector process --dry-run mostra le modifiche senza applicarle; senza --dry-run le scrive sui file. Il diff va sempre letto da una persona prima del commit, perché Rector applica regole sulla sintassi e ignora le intenzioni di chi ha scritto il codice. Conviene applicare una regola alla volta, con un commit per regola, così ogni modifica si rivede e si annulla facilmente.

PHPStan: gli errori che restano

PHPStan analizza il codice come farebbe un compilatore: verifica che le classi e i metodi chiamati esistano, che il numero di argomenti sia giusto, che i tipi siano coerenti. Ha undici livelli di severità, da 0 a 10. Su un progetto legacy si parte dal livello 0, che controlla classi e funzioni sconosciute, numero di argomenti e variabili mai definite, e si sale gradualmente.

# phpstan.neon
parameters:
    level: 0
    phpVersion: 80300        # analizza il codice come se girasse su PHP 8.3
    paths:
        - public
        - include
    excludePaths:
        - include/librerie-esterne
    # senza autoload di Composer, indica dove si trovano le classi
    scanDirectories:
        - include/classi

Al primo avvio su un’applicazione di qualche anno gli errori sono spesso centinaia. Lo strumento giusto è la baseline: il comando vendor/bin/phpstan analyse --generate-baseline salva gli errori attuali nel file phpstan-baseline.neon, che si include nella configurazione. Da quel momento PHPStan segnala solo gli errori nuovi, e la baseline diventa una lista di lavoro da ridurre nel tempo invece di un muro che blocca tutto.

Nelle applicazioni legacy molte costanti e funzioni sono definite in file di configurazione caricati con include, e PHPStan le segnala come sconosciute. Si risolve indicando quei file nell’opzione bootstrapFiles, che PHPStan carica prima dell’analisi. Quei file vengono eseguiti davvero: non devono aprire connessioni al database, inviare email o modificare dati, e a volte conviene prepararne una versione ridotta solo per l’analisi.

Per una migrazione è utile anche l’estensione ufficiale phpstan-deprecation-rules, che segnala l’uso di funzioni e metodi deprecati. Sono le rimozioni della prossima versione principale, e conviene vederle subito.

Un flusso di lavoro in pratica

L’ordine in cui si usano gli strumenti conta quanto gli strumenti stessi. Questo è il flusso che seguiamo di solito, con un commit separato per ogni passaggio:

  1. Inventario con PHPCompatibility e il rapporto summary, per misurare il lavoro e decidere le fasi.
  2. Correzioni automatiche con Rector, una regola alla volta, con --dry-run e revisione del diff.
  3. Correzioni manuali di ciò che PHPCompatibility segnala ancora, come mysql_*, mcrypt e le librerie copiate nel progetto.
  4. PHPStan al livello 0 con la baseline, sulla versione di destinazione.
  5. Test sui percorsi critici in un ambiente di prova con PHP 8 e tutti gli avvisi attivi.
  6. Livelli successivi di PHPStan e riduzione della baseline, anche dopo il rilascio.
Flusso di lavoro in sei passaggi collegati da frecce: inventario con PHPCompatibility e rapporto per file; correzioni automatiche con Rector in dry-run, una regola per commit; correzioni manuali di mysql, mcrypt e librerie copiate; PHPStan al livello 0 con baseline sulla versione di destinazione; test dei percorsi critici su PHP 8 con tutti gli avvisi attivi; rilascio con piano di rollback. Una freccia tratteggiata di ritorno dal rilascio al passaggio di PHPStan indica che dopo il rilascio si alzano i livelli e si accorcia la baseline. Sotto, una nota ricorda che gli strumenti girano in sviluppo o in integrazione continua, mai sul server di produzione
Prima si misura, poi si corregge in automatico, infine si verifica.

Le correzioni dei passaggi 2 e 3 riguardano in gran parte le funzioni che descriviamo nell’articolo sulle funzioni rimosse in PHP 8. Il caso di mysql_*, il più esteso, ha un metodo a parte, raccontato in da mysql_* a PDO.

Dove far girare gli strumenti

Le versioni attuali di PHPStan e Rector richiedono almeno PHP 7.4 per funzionare, e la serie 10 di PHPCompatibility richiede PHP 7.2. Il vincolo riguarda solo l’ambiente in cui girano. Un’applicazione scritta per PHP 5.6 si analizza tranquillamente con gli strumenti installati su PHP 8. L’importante è non installarli sul server di produzione.

  • Postazione di sviluppo o container: un’immagine con PHP 8 e Composer, che monta la cartella del progetto, basta per l’inventario iniziale.
  • Integrazione continua: una volta configurati, i tre comandi girano a ogni modifica e impediscono di reintrodurre funzioni rimosse.
  • Ramo dedicato: le modifiche di Rector vanno su un ramo separato, unito solo dopo la revisione.

I limiti dell’analisi statica

Gli strumenti vedono il codice come è scritto, e il codice legacy spesso decide a runtime cosa fare. Restano fuori, o producono falsi positivi, le chiamate costruite in modo dinamico come $funzione() o call_user_func($nome), il codice eseguito con eval(), i file inclusi con un percorso calcolato, le proprietà e i metodi gestiti da __get() e __call(), e le convenzioni dei framework che risolvono classi a partire da stringhe.

Restano fuori anche i cambiamenti di comportamento, come il nuovo confronto tra numeri e stringhe di PHP 8.0, che dipendono dai valori. Per questi servono i test, automatici dove esistono e manuali sui percorsi critici. L’analisi statica riduce drasticamente quello che i test devono scoprire, e li rende più brevi. Se vuoi applicare questo metodo alla tua applicazione, è la prima fase del nostro servizio di migrazione.

In breve

L’analisi statica permette di trovare i problemi di una migrazione a PHP 8 prima di cambiare versione, anche nel codice che si esegue di rado. PHPCompatibility, con testVersion e la serie 10 che conosce i cambiamenti fino a PHP 8.5, misura il lavoro e indica riga per riga cosa non è compatibile. Rector applica in automatico le correzioni ripetitive, meglio se una regola alla volta, con --dry-run e revisione del diff. PHPStan, partendo dal livello 0 con una baseline, trova gli errori che restano e accompagna il codice anche dopo il rilascio. Gli strumenti girano su PHP 7.4 o superiore, fuori dalla produzione, e lavorano insieme ai test, riducendo quello che i test devono scoprire.

IL PROSSIMO PASSO

Mettiamo
in sicurezza.

Indicaci versione di PHP, sistema operativo e cosa ti preoccupa. Ti risponderemo con una valutazione dell’esposizione e una proposta di intervento.

Richiedi supporto

Ti porta al modulo di richiesta, in quattro passi.

PHP LEGACY · MANAGED SERVER S.R.L.info@phplegacy.it +39 02 5656 9681

Lunedì – venerdì · 09:30 – 19:30

Urgenza in corso? Reperibilità H24
+39 340 985 4556 ↗