P.S. (pre-scriptum) Mi sono accorto che ho scritto uno sbrodolo
come al solito. Chi va di fretta perché parte per le vacanze può
tranquillamente evitare :-)
Le origini del PHP sono umili e la barriera di ingresso per i
non-programmatori è molto bassa, consentento a tanti novizi l'ingresso
nel magico mondo del web (a me, per esempio) pur senza avere una adeguata
preparazione teorica e senza avere né il tempo né la voglia di farsela.
E' a questo genere di utilizzatori che il PHP si rivolge, ed è su questi
criteri che gli sviluppatori storici non ammettono deroghe (ne andrebbe
del successo stesso del linguaggio, dove i difetti lamentati da pochi
sono proprio i pregi apprezzati dai più). Ecco perché le funzioni di
libreria standard continuano a ignorare la disponibilità delle eccezioni
e così prevedo che sarà ancora per molto tempo, anche con PHP 8.
Poi però i progetti si fanno più articolati e complessi, i tempi
di debugging si allungano in modo spropositato, tenere insieme tutto
l'ambaradam diventa anche più costoso che scriverlo la prima volta,
per cui la safety non è più un concetto astratto per i manualoni di
teoria, ma diventa un imperativo per tagliare tempi e costi e mantenere
il controllo sul progetto.
"Mantenere il controllo sul progetto" per me vuol dire sapere sempre se
qualcosa è andato storto. Ecco perché le eccezioni sono una manna per
semplificare il programma pur mantenendo intatta la safety.
Premesso che io mappo sempre gli errori in ErrorException (incluso anche
gli "insignificanti" E_NOTICE che spesso rilevano eventi catastrofici, vedi
appendice 1),
error_reporting(PHP_INT_MAX);
function my_error_handler($errno, $message) {
throw new ErrorException($message);
}
set_error_handler("my_error_handler");
function my_exception_handler($e)
{
error_log( "Uncaught $e");
exit(1);
}
set_exception_handler("my_exception_handler");
due sono le strategie che si possono adottare:
1. Ignorare del tutto le eccezioni. La scrittura del codice si semplifica
enormemente perché non è più necessario testare il valore di ritorno
della funzioni:
$f = fopen("last-update-date.txt", "rb");
$s = fread($f, 999);
...
fclose($f);
$upd = DateTimeTZ::parse( trim($s) );
echo "Last update time was: $upd";
Qui assumo che il file da leggere esista realmente, che sia leggible,
e che contenga una data valida parsabile da una certa mia classe
DateTimeTZ. Se qualcosa va storto nella lettura del file ho un
ErrorException; se qualcosa va storto nel parsing ho un ParseException.
Questa implementazione è perfetta per i programmi destinati a fare quello
che ci sia aspetta che facciano oppure muoiano nell'intento; non sono
previste mezze misure perché non sarebbero safe. Nell'esempio specifico,
se non riesco a determinare l'ultima data di aggiornamento di un certo
qualcosa, è inutile continuare perché rischierei di fare aggiornamenti
continui o non farli mai senza capire che cosa sta succedendo.
Il programma rimane safe perché se qualcosa va storto mi ritrovo il
messaggio di errore nei log con tanto di stack trace, per cui posso
intervenire puntualmente a correggere il problema. Intanto l'utente si
è beccato una pagina vuota o incompleta (cosa che per i programmatori
PHP è come l'aglio per i vampiri) ma siccome monitoro con regolarità i
log files, e siccome faccio in modo che siano sempre vuoti, sono sicuro
di poter risolvere rapidamente.
2. Gestire le eccezioni. Vuol dire catturarle con try/catch e gestirle
quando esiste un piano B da applicare, oppure quando il problema non
riguarda il programma in sé ma l'accesso a una risorsa esterna e vogliamo
dare una retroazione puntuale via interfaccia utente.
Nel tempo ho scoperto che sono pochi i casi in cui realmente esiste un
piano B (va già grassa se il programma ha un piano A da svolgere e lo
fa bene), per cui è inutile catturare le eccezioni per questo fine. E ho
anche scoperto che è quasi sempre inutile dare all'utente una retroazione
puntuale e tecnica del problema accaduto in qualche recesso interno del
programma, cosa che sarebbe per lui del tutto inutile.
Ho invece scoperto che nei rari casi in cui uso try/catch lo faccio per
trasformare una eccezione di basso livello (per esempio un ErrorException)
in un'altra di più alto livello specifica del blocco di programma,
oppure catturo l'eccezione per "rimettere a posto" le cose prima di
rilanciarla. In definitiva, ho scoperto che il try/catch serve raramente,
e che il compito di intercettare le eccezioni spetta ai piani alti del
programma, là dove si gestire l'interfaccia utente oppure si gestisce la
logica generale del programma; è lì e solo lì che bisogna catturare
le eccezioni. Diventa quindi fondamentale sfruttare il meccanismo di
propagazione automatico della eccezioni, cosa che non si può fare o è
estremamente difficile fare con gli altri sistemi.
Il tutto va naturalmente condito con un validatore statico di sorgente
capace di tracciare la propagazione delle eccezioni e capace di dire dove
si originano, dove sono gestite, dove vengono deliberatamente propagate e,
soprattutto, dove ci si è dimenticati di fare questa scelta fondamentale
tra gestione locale o propagazione. E' anche per questo che mi sono
dovuto scrivere il mio validatore statico PHPLint, con il quale si può
puntare a un livello di safety paragonabile a Java ed irraggiungibile con
qualunque altro sistema o linguaggio (neppure con C++, C# e compagnia).
"php -l" semplicemente non serve a niente.
Il discorso cambia completamente quando ci si affida al testing del valore
di ritorno delle funzioni. I programmatori diligenti testano il valore
di ritorno di ogni singola funzione, ma si accorgono che si tratta di una
"mission impossible" perché il codice diventa un orrore del tipo:
$f = fopen(...);
if( $f === NULL ){
// Mo' che faccio? Bò!
die("fopen fallito");
}
ecc. ecc.
Quando lo sbrodolo si è allungato abbastanza decide per la scrittura
concisa, che fa molto hacker:
$f = fopen(...) || die(...);
Purtroppo non si avrà uno stack trace, per cui sarà difficile indagare
sul punto dove si è verificato il problema, mentre il codice si "sporca"
e presto passa la voglia, si rinuncia a gestire gli errori, mentre il
log file si allunga, e finalmente si opta per nascondere gli errori da
php.ini nel sito di produzione. Che poi è l'ultimo e più grave errore
da fare per qualunque cosa appena più importante del blog personale.
In definitiva, le funzioni che ritornano NULL, FALSE o -1 su errore,
o altre cose ancora, non sono "safe" perché se il programmatore si
dimentica di verificare il valore ritornato, il programma prosegue
ciecamente andando incontro a un destino orribile e imprevedibile:
corruzione dei dati, presentazione all'utente di ridicoli dati sballati,
corruzione del data base, crash inaspettato centinaia di righe più
avanti nel codice che è poi difficile da diagnosticare.
Inoltre non è possibile propagare gli errori ai piani alti del programma
ma costringe a gestirli localmente oppure rinunciare; non è possibile
avere lo stack trace; non è possibile veicolare preziose informazioni
quali il tipo di errore ed eventuali dettagli specifici che invece
un oggetto può trasportare. Tutto questo rende i programmi unsafe,
difficili e frustranti da debuggare, e allunga i tempi di rilascio.
APPENDICI
1. Istruzioni e funzioni che generano un "insignificante" E_NOTICE su errore
che normalmente viene spento nel "sito in produzione":
- GET['n'] quando il dato 'n' non c'è.
- unserialize() se i dati sono corrotti e non è possibile ricostruire
il valore.
- Quasi tutti i metodi e funzioni di DOM, iconv e tidy danno anche
un E_NOTICE se c'è un problema; non sempre in questi casi il valore
ritornato è un FALSE o un NULL, ma spesso è semplicemente una cosa
imprevedibile perché la funzione non ha fatto quello che il programma
si aspettava.
- Probabilmente molti altri casi che adesso non mi vengono in mente o
non sono documentati nel manuale.