Un’applicazione scritta per PHP 5 che viene avviata per la prima volta su PHP 8 raramente si ferma per un solo motivo. Di solito il primo errore è una funzione che non esiste più: mysql_connect(), ereg(), each(), create_function(). Lo correggi, ricarichi la pagina e ne compare un altro, poi un altro ancora. Dopo qualche ora resta l’impressione di non sapere quanto manca alla fine, e il progetto rischia di essere giudicato più grande, o più piccolo, di quello che è davvero.
In questo articolo mettiamo in ordine il problema. Vediamo in quale versione è sparita ciascuna delle funzioni che incontriamo più spesso nel codice legacy, con cosa si sostituisce, quali cambiamenti di comportamento di PHP 8 rompono il codice anche quando tutte le funzioni esistono ancora, e come trovare tutto in anticipo invece di scoprirlo una pagina alla volta.
Prima deprecate, poi rimosse
Il progetto PHP non toglie quasi mai una funzione da un giorno all’altro. Di regola la funzione viene prima deprecata: continua a funzionare, ma emette un avviso di tipo E_DEPRECATED. In una versione principale successiva viene rimossa, e da quel momento chiamarla produce un errore fatale. Tra i due momenti passano spesso diversi anni.
Il guaio è che quegli avvisi, sui server di produzione, quasi nessuno li vede. Fino a PHP 7.4 il livello predefinito di error_reporting escludeva proprio E_DEPRECATED ed E_NOTICE, e molte configurazioni li nascondevano comunque. Così un’applicazione può attraversare tutte le versioni della serie 5 e arrivare a PHP 7 o 8 con decine di chiamate a funzioni dichiarate obsolete dieci anni prima.
Cosa è sparito con PHP 7.0
PHP 7.0, uscito alla fine del 2015, è la prima grande pulizia. Secondo la guida di migrazione ufficiale, le rimozioni che contano di più per il codice scritto negli anni 2000 sono queste:
- l’estensione
mysql, cioè tutte le funzionimysql_*, deprecata dalla 5.5. Va sostituita con mysqli o PDO; - l’estensione
mssqlper SQL Server, da sostituire con i driver SQLSRV o PDO_SQLSRV; - tutte le funzioni
ereg(ereg(),eregi(),ereg_replace(),split()e simili), al cui posto si usano le funzioni PCRE comepreg_match(); - il modificatore
/edipreg_replace(), che eseguiva come codice PHP il testo di sostituzione. Si riscrive conpreg_replace_callback(); call_user_method()eset_magic_quotes_runtime(), già deprecate da molti anni;- i tag ASP (
<%e%>) e il tag<script language="php">.
Nella stessa versione i costruttori in stile PHP 4, cioè i metodi con lo stesso nome della classe, diventano deprecati: verranno rimossi in PHP 8.0. Cambia anche l’ordine di valutazione di espressioni come $$nome['chiave'], e molti errori fatali diventano eccezioni della classe Error, che un vecchio gestore dichiarato con il tipo Exception non intercetta.
Da PHP 7.1 a 7.4: le deprecazioni che diventano errori
Le versioni intermedie della serie 7 rimuovono poco, ma preparano il terreno per PHP 8. In PHP 7.2 l’estensione mcrypt, molto usata per cifrare dati e token, esce dalla distribuzione standard e resta disponibile solo come pacchetto PECL. Nella stessa versione diventano deprecati each(), create_function(), __autoload(), il cast (unset) e parse_str() senza il secondo argomento.
PHP 7.4 aggiunge un’altra serie di avvisi: money_format(), get_magic_quotes_gpc(), get_magic_quotes_runtime(), hebrevc(), convert_cyr_string(), il cast (real), l’accesso agli indici con le parentesi graffe ($stringa{0}), gli operatori ternari annidati senza parentesi e implode() con gli argomenti in ordine inverso. Chi ha un’applicazione su PHP 7.4 e ha i log delle deprecazioni attivi ha già in mano buona parte della lista di lavoro per PHP 8.
PHP 8.0: la seconda grande pulizia
PHP 8.0, uscito a novembre 2020, rimuove in blocco quasi tutto quello che era stato deprecato nella serie 7. Sono sparite tra le altre each(), create_function(), __autoload(), money_format(), get_magic_quotes_gpc(), fgetss(), restore_include_path(), read_exif_data(), image2wbmp(), ldap_sort() e gmp_random(), insieme ai cast (real) e (unset), alla direttiva track_errors con la variabile $php_errormsg, alle costanti senza distinzione tra maiuscole e minuscole e all’accesso con le parentesi graffe.
Due rimozioni meritano attenzione perché non riguardano una funzione e quindi sfuggono a una ricerca testuale. I metodi con lo stesso nome della classe non sono più costruttori: la classe si crea senza errori, ma il codice di inizializzazione non viene eseguito. E non è più possibile chiamare in modo statico un metodo non statico, una scorciatoia comune nelle classi di utilità scritte per PHP 4 e 5.
I cambiamenti di comportamento che non danno errori subito
Una funzione mancante almeno si fa notare. Più insidiosi sono i cambiamenti di PHP 8.0 che lasciano il codice sintatticamente valido e ne cambiano il risultato. I principali, elencati nella guida di migrazione a PHP 8.0, sono questi:
- confronto tra numeri e stringhe:
0 == "abc"era vero e ora è falso, così come0 == "". I controlli scritti con l’uguaglianza debole su valori che arrivano da form o database possono cambiare esito; - avvisi promossi a eccezioni: passare un tipo non numerabile a
count(), fare aritmetica su un array o scrivere una proprietà su un valore che non è un oggetto produce unTypeErroro unError, e la pagina si interrompe; - l’operatore
@non nasconde più gli errori fatali; - il livello predefinito degli errori diventa
E_ALL, e la lettura di una variabile non definita passa da notice a warning: i log si riempiono rapidamente; - PDO lancia eccezioni per impostazione predefinita invece di restituire
falsein silenzio; - precedenza della concatenazione: in
"Totale: " . $a + $bla somma viene ora eseguita prima della concatenazione.
Questi punti vanno verificati con i test, o almeno con prove mirate sui percorsi critici: login, carrello, calcolo di prezzi e totali, esportazioni. Un controllo automatico li segnala solo in parte, perché dipendono dai valori che il codice riceve durante l’esecuzione.
Dopo PHP 8.0: le deprecazioni da tenere d’occhio
Arrivare a PHP 8.0 non chiude il discorso, perché 8.0 e 8.1 sono a loro volta fuori supporto e la destinazione realistica oggi è 8.3, 8.4 o 8.5. Le versioni successive hanno rimosso poco, ma hanno deprecato abitudini molto diffuse:
- PHP 8.1 depreca il passaggio di
nullai parametri non annullabili delle funzioni interne, per esempiostrlen(null)otrim(null), insieme astrftime()e al filtroFILTER_SANITIZE_STRING; - PHP 8.2 depreca le proprietà dinamiche, cioè quelle assegnate a un oggetto senza essere dichiarate nella classe, oltre a
utf8_encode(),utf8_decode()e all’interpolazione"${variabile}"; - PHP 8.4 depreca i parametri resi implicitamente annullabili da un valore predefinito
nulle sposta su PECL le estensioni IMAP, OCI8, PDO_OCI e PSpell; - PHP 8.5 depreca l’operatore backtick come alias di
shell_exec()e i cast non canonici come(integer)e(boolean).
Una deprecazione lascia funzionare l’applicazione e annuncia una rimozione nella prossima versione principale. Conviene correggerle durante la migrazione, quando il codice è già sotto esame, invece di accumularle per il prossimo salto.
Come sostituire le funzioni più comuni
Molte sostituzioni sono meccaniche e si fanno in pochi minuti per ogni occorrenza. Ecco i casi che troviamo più spesso nel codice legacy, nella versione originale e in quella corretta:
<?php
// ereg() e split(): sostituite da PCRE (attenzione ai delimitatori)
if (ereg('^[0-9]{5}$', $cap)) { /* ... */ } // prima
if (preg_match('/^[0-9]{5}$/', $cap)) { /* ... */ } // dopo
$parti = split(',', $riga); // prima
$parti = explode(',', $riga); // dopo (separatore semplice)
// each(): sostituito da foreach
while (list($chiave, $valore) = each($dati)) { /* ... */ } // prima
foreach ($dati as $chiave => $valore) { /* ... */ } // dopo
// create_function(): sostituita da una funzione anonima
usort($righe, create_function('$a,$b', 'return $a["n"] - $b["n"];')); // prima
usort($righe, function ($a, $b) { return $a['n'] - $b['n']; }); // dopo
// preg_replace() con /e: sostituito da preg_replace_callback()
$t = preg_replace('/\{(\w+)\}/e', 'strtoupper("$1")', $t); // prima
$t = preg_replace_callback('/\{(\w+)\}/', function ($m) { // dopo
return strtoupper($m[1]);
}, $t);
// __autoload(): sostituito da spl_autoload_register()
spl_autoload_register(function ($classe) {
require __DIR__ . '/classi/' . $classe . '.php';
});
Altri casi richiedono più attenzione. Il passaggio da mysql_* a PDO tocca la sicurezza delle query e lo trattiamo in un articolo dedicato, da mysql_* a PDO. Per mcrypt il punto delicato sono i dati già cifrati: prima di cambiare libreria bisogna sapere quale algoritmo, modalità e padding sono stati usati, altrimenti i dati salvati non si leggono più. money_format() si sostituisce con la classe NumberFormatter dell’estensione intl, che però formatta secondo le regole della lingua e può dare risultati leggermente diversi da prima.
Trovare tutto prima di cominciare
Correggere gli errori uno alla volta, man mano che compaiono, è il modo più lento e meno affidabile di procedere: alcune pagine si aprono di rado, alcuni percorsi si attivano una volta al mese, e l’ultimo errore lo scopre un cliente. Conviene partire da un inventario completo, che si ottiene in tre passaggi.
- Ricerca testuale delle funzioni rimosse più comuni, per avere subito un ordine di grandezza.
- Analisi statica con strumenti come PHPCompatibility, che conosce le funzioni rimosse e deprecate versione per versione e segnala anche i costrutti di sintassi, e PHPStan. Li descriviamo nel dettaglio nell’articolo sull’analisi statica per la migrazione.
- Esecuzione con tutti gli avvisi attivi in un ambiente di prova sulla versione di destinazione, con
error_reporting = E_ALLe i log delle deprecazioni letti con regolarità.
Per il primo passaggio basta un comando:
# Conta le occorrenze delle funzioni rimosse più comuni, escludendo vendor/
grep -rnoE --include='*.php' --exclude-dir=vendor \
'\b(mysql_[a-z_]+|eregi?(_replace)?|split|each|create_function|__autoload|money_format|get_magic_quotes_gpc|mcrypt_[a-z_]+)\s*\(' . \
| sed -E 's/.*:([a-z_]+)\s*\(/\1/i' | sort | uniq -c | sort -rn
La lista che ne esce va ripulita a mano, perché la ricerca trova anche metodi con lo stesso nome, per esempio un ->split() di una classe, e non trova chiamate costruite dinamicamente. Serve però a capire in pochi minuti se il problema riguarda dieci file o trecento, e quindi a stimare il lavoro prima di aprire il primo file.
Le librerie incluse nel progetto
Nel codice legacy le funzioni rimosse non stanno solo nei file scritti dal team. Spesso si trovano in librerie copiate a mano nel progetto anni fa: vecchie versioni di generatori di PDF, client SOAP, classi per l’invio di email, template engine. Per queste la strada più sicura è aggiornare la libreria a una versione compatibile, o sostituirla con un equivalente mantenuto installato con Composer. Correggere a mano il codice di terzi funziona, ma rende impossibili gli aggiornamenti futuri e va documentato con cura.
Quando le correzioni sono tante e l’applicazione deve restare online, il lavoro si può dividere in fasi: prima le modifiche compatibili con la versione attuale e con quella di destinazione, rilasciate sulla produzione esistente, poi il passaggio vero e proprio. Nel frattempo la produzione va protetta. Sui rami fuori supporto il nostro servizio di migrazione lavora proprio così, con il backport delle patch di sicurezza a fare da ponte fino al rilascio sulla nuova versione.
Tirando le somme
Il passaggio da PHP 5 a PHP 8 attraversa due grandi pulizie. PHP 7.0 ha rimosso le estensioni mysql e mssql, le funzioni ereg, il modificatore /e e i tag ASP; PHP 8.0 ha tolto quasi tutto ciò che era stato deprecato nella serie 7, da each() a create_function(), da __autoload() a money_format(), insieme ai costruttori in stile PHP 4. A questo si aggiungono i cambiamenti di comportamento, come i confronti tra numeri e stringhe e gli avvisi diventati eccezioni, che non si trovano cercando un nome di funzione. Per quasi ogni rimozione esiste un sostituto diretto, e molte correzioni sono meccaniche. Il lavoro diventa prevedibile quando parte da un inventario completo, fatto con ricerca testuale, analisi statica ed esecuzione in un ambiente di prova con tutti gli avvisi attivi.