Da PHP 5 a PHP 8: le funzioni rimosse che bloccano il tuo codice

Quali funzioni sono sparite in PHP 7.0 e in PHP 8.0, con cosa si sostituiscono, quali cambiamenti di comportamento rompono il codice senza errori evidenti e come fare l’inventario completo prima di iniziare la migrazione.

Copertina con il logo php∞, la categoria Framework e codice custom, il titolo Le funzioni rimosse in PHP 8 e il numero 38

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.

Linea del tempo delle rimozioni in PHP dalla 7.0 alla 8.5. In PHP 7.0 vengono rimosse le estensioni mysql e mssql, le funzioni ereg, il modificatore e di preg_replace, call_user_method e i tag ASP. In PHP 7.2 l’estensione mcrypt esce dal pacchetto standard e vengono deprecate each, create_function e __autoload. In PHP 7.4 vengono deprecate money_format, get_magic_quotes_gpc e le parentesi graffe per l’accesso agli indici. In PHP 8.0 tutte queste funzioni vengono rimosse e cambiano confronti ed errori. In PHP 8.1 e 8.2 sono deprecati il passaggio di null alle funzioni interne, le proprietà dinamiche e utf8_encode. In PHP 8.4 le estensioni IMAP, OCI8, PDO_OCI e PSpell passano a PECL
Le due tappe che rompono più codice legacy sono PHP 7.0 e PHP 8.0.

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 funzioni mysql_*, deprecata dalla 5.5. Va sostituita con mysqli o PDO;
  • l’estensione mssql per 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 come preg_match();
  • il modificatore /e di preg_replace(), che eseguiva come codice PHP il testo di sostituzione. Si riscrive con preg_replace_callback();
  • call_user_method() e set_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.

Tabella con dieci funzioni rimosse, la versione di rimozione e il sostituto: mysql_* rimosse in PHP 7.0, sostituite da PDO o mysqli; ereg ed eregi, 7.0, sostituite da preg_match; split, 7.0, sostituita da explode o preg_split; preg_replace con il modificatore e, 7.0, sostituita da preg_replace_callback; mcrypt, fuori dal pacchetto standard da 7.2, sostituita da OpenSSL o Sodium; each, 8.0, sostituita da foreach; create_function, 8.0, sostituita da una funzione anonima; __autoload, 8.0, sostituita da spl_autoload_register; money_format, 8.0, sostituita da NumberFormatter di intl; get_magic_quotes_gpc, 8.0, da eliminare perché restituiva sempre false
Per quasi ogni funzione rimossa esiste un sostituto diretto nel PHP di oggi.

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ì come 0 == "". 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 un TypeError o un Error, 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 false in silenzio;
  • precedenza della concatenazione: in "Totale: " . $a + $b la 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 null ai parametri non annullabili delle funzioni interne, per esempio strlen(null) o trim(null), insieme a strftime() e al filtro FILTER_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 null e 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.

  1. Ricerca testuale delle funzioni rimosse più comuni, per avere subito un ordine di grandezza.
  2. 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.
  3. Esecuzione con tutti gli avvisi attivi in un ambiente di prova sulla versione di destinazione, con error_reporting = E_ALL e 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.

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 ↗