Home / Indice sezione
 www.icosaedro.it 

 PHP Analisi Critica

Ultimo aggiornamento: 2012-02-18

Il linguaggio di programmazione PHP è sempre più popolare e forse il più usato in assoluto per lo sviluppo di siti WEB dinamici. Si tratta di un linguaggio nato quasi per caso per assolvere ad esigenze pratiche, ed ha punti di forza ma anche qualche debolezza. Questo articolo analizza i pro e i contro del PHP, con particolare riguardo alla affidabilità (safety). L'articolo PHP Security si occupa più specificatamente degli aspetti di sicurezza dalle aggressioni (security). Per alcuni punti viene discusso come il validatore PHPLint può aiutare il programmatore a risolvere il problema.

Indice

I punti di forza del PHP
Le debolezze del PHP
Array associativi
Questioni legate all'OOP
Critiche poco rilevanti
Rimedi
Riferimenti
Argomenti correlati

I punti di forza del PHP

Il PHP non è stato progettato "a tavolino" sulla base di astratti principi di programmazione. Il PHP è nato poco alla volta per rispondere ad esigenze pratiche, e così è diventato forse il più popolare linguaggio per lo sviluppo di siti WEB dinamici. I suoi meriti sono tanti, e un elenco completo sarebbe davvero lungo, ma declamare le virtù di un prodotto aiuta poco alla sua evoluzione. Qui riassumo solo le sue valenze principali.

E un linguaggio interpretato. Ogni modifica apportata al sorgente è immediatamente effettiva.

Supporta la programmazione procedurale e la programmazione orientata agli oggetti. Il linguaggio PHP non presenta sorprese per coloro che conoscono già un altro linguaggio di programmazione, e non introduce alcun concetto radicalmente nuovo: variabili, array, istruzioni condizioni, cicli, classi e oggetti. Insomma, le solite cose. Il programmatore può scegliere il paradigma di programmazione che meglio si adatta al problema da risolvere. PHP5 offre un supporto più ricco e completo alla OOP.

Non richiede dichiarazioni di tipo. Il tipo delle variabili viene stabilito dinamicamente quando viene assegnato un valore ad esse. Questo semplifica il codice e velocizza lo sviluppo.

Ricca dotazione di librerie di funzioni utili e di uso immediato. Il PHP è concepito principalmente per lo sviluppo di applicazioni WEB e, almeno in questo ambito, non gli manca proprio nulla: dalla gestione dei file upload alla crittografia, e dall'accesso ai data base al parsing dell'XML.

E' disponibile su tutti i sistemi operativi e lo si può installare su Linux, Mac OS X, Microsoft Windows, ...

Ha una impronta modesta sul sistema. Anche una macchina di modeste prestazioni è sufficiente per far girare un server WEB con PHP, non occorrono grandi quantità di memoria o processori molto potenti. Questo è uno dei motivi per cui PHP è disponibile su tutte le offerte di hosting.

Le debolezze del PHP

E' un linguaggio interpretato. Poco efficiente, soprattutto la fase iniziale di parsing del sorgente. Il manuale PHP suggerisce di usare l'estensione APC disponibile dal progetto PECL. APC mantiene in memoria il codice intermedio prodotto dal parser, in modo da velocizzare l'avvio della esecuzione delle pagine WEB. Esiste anche bcompiler, che converte il codice PHP in un programma direttamente eseguibile dal processore, senza necessità dell'interprete PHP.

PROPOSTA: implementare una cache del codice intermedio direttamente nel pacchetto PHP distribuito, come già fanno certi prodotti di terze parti. Questo eleverebbe di molto l'efficienza delle applicazioni WEB, almeno per i siti molto oberati.

E' difficile implementare un compilatore per il PHP capace di generare codice macchina direttamente eseguibile dal computer. Causa di questo problema sono la mutabilità di tipo delle variabili, la possibilità di definire dinamicamente le costanti e il meccanismo di inclusione di file. Se il linguaggio fosse appena più rigoroso si aprirebbero nuove strade per l'applicazione del PHP in altri contesti oltre allo scripting di siti WEB. Alcune delle proposte che qui suggerisco vanno proprio nella direzione di rendere il PHP un linguaggio "compilabile".

PHPLint riconosce un sotto-insieme del linguaggio PHP ed esclude proprio quelle funzionalità del PHP che impediscono la realizzazione di un compilatore efficiente, come ad esempio le variabili dal nome variabile $$x.

Molti messaggi di avvertimento che l'interprete può generare a run-time si possono disabilitare. Ed è esattamente quello che fanno molti programmatori: vedendo allungarsi il log file di oscuri messaggi, preferiscono risolvere questa noia disabilitandone la generazione piuttosto che avvalersene per correggere e migliorare il proprio programma. Ad esempio, l'avvertimento quando si usa il valore di una variabile non inizializzata (errore spesso fatale in altri linguaggi) in PHP è un NOTICE disabilitato per default. E' proprio trascurando questo problema che poi si è generata tutta la questione dei bachi causati dalla impostazione "register globals". FIXME: elenca messaggi di errore da promuovere a "fatali" o almeno "warning".

PHPLint segnala le variabili usate ma non inizializzate.

Le parole chiave del linguaggio sono case-insensitive. Dunque è ammesso scrivere cose come questa:

IF ($i > 0 && $i < 10 ) {
    Echo "Valore nell'intervallo previsto.";
Else {
    ECHO "Valore fuori dell'intervallo previsto.";
}

Questo oscura il sorgente, non ha alcuna utilità pratica, ed è brutto a vedersi.

PROPOSTA: tutte le keyword sono case-sensitive, e di norma sono minuscole. Le keyword che rappresentano le costanti predefinite (TRUE, FALSE, NULL, ...) devono essere maiuscole, come da comune convenzione per i nomi di costanti.

PHPLint genera un messaggio di errore quando non si rispetta l'ortografia delle keyword.

Regole di scope un po' confuse. I nomi delle costanti sono globali, e quindi visibili anche dentro alle funzioni, dentro le classi e i metodi, indipendentemente da dove queste costanti sono state definite. Idem per i nomi di funzione. Le variabili globali hanno invece l'accesso regolato dalla istruzione global e sono accessibili dall'interno di funzioni e classi via $GLOBALS[]. Secondo il mio modo di vedere le cose della programmazione strutturata, un identificatore definito all'interno di qualcosa è locale a quel contesto a meno che non sia esplicitamente indicato diversamente.

I vari tipi di identificatori occupano spazi dei nomi separati. Ad esempio, un nome di costante può coincidere col nome di una variabile: PI e $PI sono due identificatori diversi. In queste caso nessun problema reale, visto che la presenza del $ permette di distinguere bene le due cose. Ma cosa dire della funzione PI() e della classe PI? Questo sorgente risulta valido su PHP5:

define("defUsername", "pippo");

$defUsername = "pluto";

function defUsername() { return defUsername; }

class defUsername {
    const defUsername = "topolino";
    public $defUsername = "paperino";
    function defUsername($defUsername)
    { $this->defUsername = defUsername; }
}

$u = new defUsername( defUsername() );
echo $u->defUsername;

Tralasciamo per il momento le considerazioni sulla salute mentale del programmatore che ha scritto questo codice (cioè io... :-) Domanda: che nome stampa l'ultimo echo? Un po' confuso, non è vero? E che dire se sorgesse la necessità di convertire un simile sorgente in un altro linguaggio meno permissivo? oppure volessimo compilarlo con un ipotetico compilatore? Ma vediamo alcune conseguenze ancora più strane in questo codice:

# Questo codice funziona, anche se l'ID della
# variabile è una keyword:

$if = "ciao";
echo $if;

# Si può definire la costante IF ma poi
# non la si può usare perché risulta essere
# una keyword:

define("IF", "hello");  # <-- consentito, ma...
echo IF;                # <-- ...poi si ha un errore fatale

# Anche nelle classi abbiamo dei comportamenti non
# proprio cristallini: gli ID di costanti e funzioni
# non possono essere keyword:

class unaClasse
{
    const if = "bye";     # <-- vietato: ragionevole
    public $if = "ciao";  # <-- consentito, ma vedi dopo
    function if(){}       # <-- vietato: ragionevole
}

$obj = new unaClasse();
$obj->if = "hello";       # <-- consentito!

Conclusione di questa piccola indagine: i nomi delle variabili e delle proprietà possono essere delle keyword, mentre gli altri identificatori no. Ma non sarebbe meglio avere nomi diversi per cose diverse?

PROPOSTA: tutti gli identificatori e le keyword sono case-sensitive e quindi si possono scrivere in un modo solo; lo spazio dei nomi degli identificatori globali (costanti, variabili, funzioni, classi) deve essere unico; gli identificatori interni alle funzioni e alle classi sono locali e non collidono con quelli globali (questo lo fa già, ma lo diciamo giusto per completezza); nessun identificatore può coincidere con una keyword.


IdentificatoreCase-sensitive?Scope
KeywordNo--
CostanteSì/NoGlobale
VariabileLocale
Argomento formale di funzioneLocale
FunzioneNoGlobale
ClasseNoGlobale
Costante di classe (PHP5)Scope protetto
ProprietàScope protetto
MetodoNoScope protetto

Quadro degli identificatori e loro proprietà.


PHPLint impone uno stile "all case sensitive", per cui tutti gli identificatori devono essere sempre scritti rispettando l'ortografia con la quale sono stati definiti. Gli identificatori non possono coincidere con una keyword, quindi una costante IF e una variabile $if non sono ammessi.

Inoltre, per mitigare l'affollamento dello spazio dei nomi globale, PHPLint introduce il qualificatore /*.private.*/ per rendere determinate entità private al file dove sono definite. Costanti, variabili, funzioni e classi ad uso locale si possono così proteggere rispetto agli altri file. Esempio:

/*. private .*/ define("UM_MAX_LOGGED_USERS", 100);
/*. private .*/ $UM_currLoggedUsers = 0;
/*. private void .*/ function UM_addUser(/*. string .*/ $name){ ... }
/*. private .*/ class UM_LoggedUser { ... }

PHPLint solleva un errore se una di queste entità private viene usata o ridefinita in un altro file. Per evitare collisioni accidentati, è bene che il nome delle entità non sia troppo banale. Una convenzione utile è dare un prefisso ai nomi, nel nostro esempio UM_*. Le entità private non vengono riportate da PHPLint Documentator.

Non si possono annidare i commenti multi-linea. A volte sarebbe utile poter escludere al volo una sezione di codice, soprattutto in fase di test del programma, e i commenti multi-linea sembrano adatti a questo scopo. Purtroppo questa soluzione non è applicabile quando nella sezione di codice sono già presenti degli altri commenti multi-linea. Se ciononostante si prova a commentare il codice, si possono verificare comportamenti inattesi:

$i = 9;
/**** CODICE ESCLUSO
$j = $i + 1; /* xxx yyy zzz */
$i_max = 20;
FINE CODICE ESCLUSO *****/

In questo esempio l'assegnamento alla variabile $i_max viene comunque eseguito, anche se risulta inserito nel blocco di codice commentato. L'interprete segnala poi un errore "misterioso" perché l'istruzione "FINE" non risulta definita. In una sezione di codice appena più lunga e intricata questo potrebbe rubare un po' di tempo al programmatore.

PROPOSTA:

PHPLint rileva come errore i commenti multilinea annidati.

Variabili per riferimento. L'argomento è un po' fumoso. Argomentare (FIXME).

Le variabili locali a funzione dichiarate static possono non avere un valore iniziale, e allora vengono inizializzate al valore NULL. Di solito questo funziona perché il valore NULL viene equiparato al numero 0 o alla stringa vuota a seconda del contesto, e in generale questo è quello che serve. Tuttavia per evitare ambiguità sarebbe meglio specificare sempre un valore iniziale esplicito come in questo esempio:

function f()
{
    static $counter = 0;

    return ++$counter;
}

PROPOSTA: Le variabili locali static dovrebbero obbligatoriamente avere sempre un valore iniziale.

PHPLint avvisa quando una variabile locale "static" non ha un valore iniziale.

Gli array sono trattati come dati "elementari". Questo fatto ha diverse implicazioni subdole:

Osserviamo che anche il passaggio di un array come argomento di funzione comporta la copia dell'array: l'argomento formale della funzione assume come valore una copia del valore della variabile attuale. In questo esempio l'array $a viene copiato in $b, quindi $b viene modificato e i due array vengono stampati. Si vede così che i due array sono diversi:

$a = array("a", "b", "c");
$b = $a;          # copio $a in $b
$b[2] = "ZZZZZ";  # modifico $b
var_dump($a);     # --> array("a", "b", "c")
var_dump($b);     # --> array("a", "b", "ZZZZZ")

Nessun allarme per quanto riguarda l'efficienza: in realtà la copia dell'array avviene con un meccanismo del tipo "copy on write", cioè la duplicazione in memoria avviene solo se uno dei due array viene modificato. In PHP3 e PHP4 gli oggetti erano implementati internamente come array, e di questi ereditavano lo stesso comportamento rispetto all'assegnamento e al passaggio come argomento di funzione.

Il PHP5 ha ribaltato la cosa rispetto agli oggetti, che diventano strutture dati a sè stanti trasferite "per riferimento" e non più "per valore".

NOTA. La copia e il passaggio di argomento a funzione per valore significa che avviene una copia della variabile, e quindi le modifiche apportate alla copia non si riflettono sull'originale. Per riferimento significa il contrario, e quindi le modifiche a una variabile si riflettono sull'altra.


$a = $b;
func($a);
PHP3 e PHP4 PHP5
array per valore per valore
oggetto per valore per riferimento

Assegnamento e passaggio di argomento di array e oggetti.


Il cambio di comportamento nella copia degli oggetti nel passaggio da PHP4 a PHP5 sarebbe stato il momento opportuno per adottare lo stesso comportamento anche per gli array perché è quello che si aspetta la maggior parte dei programmatori e sarebbe anche la soluzione più indicata nella prospettiva di realizzare un vero compilatore per il PHP.

Non si possono annidare le funzioni. Se la funzione f() contiene la dichiarazione della funzione g(), allora: la funzione g() esiste solo se viene chiamata f(); una volta definita, la funzione g() risulta visibile globalmente; se la funzione f() viene chiamata una seconda volta si ha un errore fatale perché risulta un tentativo di ridichiarare la funzione g(), cosa che è vietata.

function f()
{

    function g() { ... }

    ...
}

f();  # dopo questa chiamata, g() esiste anche globalmente
g();  # posso chiamare g() anche se locale a f()!
f();  # errore fatale, g() viene ridefinita!

Anche se il linguaggio non le vieta, nella pratica in PHP non si possono annidare funzioni dentro altre funzioni. Alcuni algoritmi complessi si avvantaggiano della possibilità di scomporli in funzioni più piccole e specifiche. L'impossibilità di scrivere funzioni annidate costringe a dichiarare funzioni accessorie visibili globalmente anche se usate solo localmente. Anche per i metodi delle classi sarebbe utile poter disporre di funzioni annidate.

PROPOSTA:

PHPLint segnala come errore le funzioni annidate.

I nomi delle costanti predefinite, delle funzioni e delle classi non sono sensibili alla differenza tra maiuscole e minuscole. Ad esempio, la funzione per aprire un file si può scrivere indifferentmente fopen() FOpen() FOPEN(), mentre per le costanti predefinite si può scrivere indifferentemente null Null NULL true True TRUE. Non un vero problema, ma una questione di stile che poi ha riflessi sull'ordine e sulla leggibilità del sorgente.

PHPLint segnala l'errata ortografia dei nomi di costanti, funzioni e classi. Gli identificatori delle librerie standard del PHP vanno scritti come riportato nella documentazione ufficiale, quindi fopen va bene, mentre FOpen no.

Il meccanismo per la definizione delle costanti non è coerente. Le costanti si definiscono con l'istruzione define(EXPR1, EXPR2); dove EXPR1 è una espressione che genera il nome della costante ed EXPR2 è il suo valore. Siccome il nome della costante e il suo valore si possono generare dinamicamente, alcuni programmatori sfruttano questa possibilità, così ottenendo "costanti" di fatto variabili e dal nome a priori indefinito. Pessimo stile di programmazione. Inoltre si possono dichiarare costanti dal nome non valido che poi risulta non utilizzabile, come in questo esempio:

define("NOME NON VALIDO", 123);
define("^%@&", 456);

Inoltre l'istruzione define() ammette un terzo argomento opzionale per dire se il nome della costante è o non è case-sensitive. I nomi delle costanti sono sempre globali, anche se definite dentro a una funzione. Inoltre la sintassi per la definizione delle costanti di classe in PHP5 usa una sintassi diversa.

PROPOSTA:

In definitiva, secondo la mia proposta le costanti devono essere effettivamente... costanti! Correntemente, invece, le costanti vengono usate spesso come variabili assegnabili una sola volta, e sono preferite alle variabili per via del fatto che il loro scope è globale, e quindi si possono usare direttamente nelle funzioni e nelle classi.

PHPLint segnala un errore se ricorre uno dei seguenti casi: 1) il nome della costante non è una stringa letterale; 2) il nome della costante non è un identificatore valido; 3) il valore della costante non è staticamente determinabile; 4) il nome della costante è una keyword.

Le costanti speciali STDIN, STDOUT e STDERR sono di tipo resource. Il PHP vieta all'utente di definire costanti di tipo risorsa, eppure nel PHP compilato in modalità CLI sono disponibili queste costanti che rappresentano i file di standard input, output e error. Sarebbe più coerente definire delle variabili, oppure delle funzioni che ritornano questi flussi.

PHPLint definisce internamente le costanti STDIN, STDOUT e STDERR perché non c'è modo di definirle altrimenti usando codice PHP regolare.

Manca supporto multi-lingue nelle stringhe. Le stringhe sono banalmente sequenze di byte, senza informazioni sul charset o sulla codifica del testo che contengono. L'estensione multibyte permette di aggirare il problema, ma sarebbe meglio un supporto nativo per l'UNICODE.

L'accesso al file system è limitato al solo charset ASCII. E' una conseguenza del fatto che le stringhe usano un solo byte per carattere, per cui non si possono usare file il cui nome contiene caratteri esotici. In realtà la situazione è più articolata, e dipende dal sistema operativo sottostante. Su sistemi Windows esiste un encoding default usato quando i programmi formulano nomi file a un byte per carattere (cosa che nella terminologia tecnica di Windows viene chiamata "codifica ANSI"), ma questo encoding default varia da macchina a macchina. In Unix e Linux è ormai invalso l'uso dell'UTF-8 come encoding del file system, che si imposta con la variabile d'ambiente LC_PAPER=it_IT.UTF-8 e che risolve completamente il problema.
Morale: per non avere problemi nel passaggio da un sistema a un altro ed evitare dipendenze dalla configurazione specifica del server sul quale il nostro programma girerà, è meglio limitarsi all'uso del solo ASCII per i nomi dei file da mettere dentro alle funzioni come fopen() e anche i nomi dei sorgenti PHP e i namespace devono limitarsi al solo ASCII. Ricordare che questa limitazione si applica non solo al nome del file, ma anche a tutto il path.

Manca un tipo numerico per i calcoli monetari. Usare i tipi interi o a virgola mobile per i calcoli monetari induce a commettere errori subdoli che a volte portano a risultati inattesi. Ad esempio l'importo 0.10 (dieci cent) non può essere rappresentato in modo esatto dal tipo float, producendo un troncamento di cui spesso i programmatori non tengono conto. L'argomento viene sviluppato nell'articolo Calcoli monetari con numeri frazionari in PHP, dove si propongono due possibili soluzioni.

Type juggling. Così si chiama il meccanismo automatico di conversione dei tipi. Il PHP tenta sempre di calcolare le espressioni convertendo il tipo di una variabile a seconda del contesto. Questo semplifica a volte il lavoro del programmatore, ma induce a commettere degli errori difficili da individuare. Il PHP permette di usare una variabile di tipo stringa all'interno di una espressione numerica, e il suo valore viene automaticamente convertito in numero ogni volta che l'espressione viene calcolata. Ovviamente questa conversione richiede tempo e quindi incide sulle prestazioni del programma.

Dai test che ho fatto risulta che l'espressione

123 + 456

è ben 3 volte più veloce della espressione

"123" + "456"

dove gli addendi devono essere convertiti da stringa a numero prima che si possa fare la somma. La stessa penalizzazione si verifica anche quando si fanno i confronti, gli incrementi e ogni qualvolta si attiva la conversione automatica di tipo.

Se al posto dei valori letterali mettiamo delle variabili, il ragionamento non cambia. In questo esempio la variabile $a è una stringa che viene convertita in intero per ben 100 volte per eseguire una certa operazione aritmetica:

$a = $_GET['parametro'];  # supponiamo che ora sia $a==="123"
for($i=0; $i<100; $i++)
    echo ($i+$a), "\n";   # qui $a viene convertito in int

E' evidente la penalizzazione prestazionale che ne risulta. Se il linguaggio desse almeno un avvertimento ogni volta che una espressione contiene un valore di tipo improprio, il programmatore si accorgerebbe del punto critico del suo programma e apporterebbe le dovute correzioni. In questo caso si tratta semplicemente di eseguire un typecast sulla variabile $a nel momento in cui viene assegnata, in modo che $a risulti essere un numero intero fin dall'inizio. Ecco il codice corretto:

$a = (int) $_GET['parametro'];  # $a risulta di tipo int
for($i=0; $i<100; $i++)
    echo ($i+$a), "\n";         # nessuna conversione necessaria

In certi casi sarebbe meglio imporre una conversione esplicita, in modo da aiutare il programmatore a scrivere programmi più efficienti ma anche più facili da capire, e consentire in prospettiva la realizzazione di compilatori del linguaggio PHP realmente efficienti. Si potrebbe obiettare che nell'ipotesi che il PHP fosse reso un giorno più severo, il programmatore pigro risolverebbe semplicemente disseminando di typecast tutte le espressioni in modo da aggirare il problema. Secondo me, invece, l'orripilante risultato conseguente indurrebbe il programmatore a riformulare il suo codice in modo da renderlo migliore.

PHPLint segnala un errore quando si mescolano tipi di dato diversi dentro a una espressione. Il programmatore deve aggiungere gli operatori di typecast opportuni oppure (meglio) deve riorganizzare il programma in modo che le conversioni implicite non siano più necessarie.

Gli operatori di confronto tra stringhe possono dare risultati inattesi. E' un'altra delle conseguenze del type juggling: nei confronti, se le stringhe sembrano numeri, allora vengono trattate come tali. In questo esempio è evidente che la stringa $a viene prima di $b. Il confronto con gli operatori deboli dice invece che sono uguali:

$a = "03";
$b = "3";
if ( $a == $b )  echo "sono uguali";
if ( $a <  $b )  echo "sono diverse";

==> sono uguali

La ragione di questo strano comportamento è che le due stringhe $a e $b hanno la forma di numeri, pertanto nei confronti vengono trattate entrambe come il numero intero 3. Morale: con le stringhe usare sempre gli operatori di confronto stretto === e !==, e negli altri casi usare la funzione strcmp(). Quindi in generale il confronto tra stringhe della forma

$a OPERATORE  $b
deve essere riscritto nella forma sicura
strcmp($a, $b) OPERATORE  0

Riservare gli operatori di confronto debole == != <= ecc. alle variabili e alle espressioni che sappiamo essere numeri.

PHPLint segnala un errore quando si confrontano le stringhe con gli operatori di confronto debole.

Le espressioni possono contenere assegnamenti. E' una conseguenza dell'adottare il C come prototipo della sintassi. Il C era concepito per programmatori di sistema abituati al linguaggio assembly, e per i quali le astrusità della sintassi del C erano nulla in confronto a quello che dovevano fare altrimenti. Tuttavia certe soluzioni sintattiche complicano l'apprendimento, oscurano il sorgente e danno ben pochi vantaggi. Gli assegnamenti dentro alle espressioni ricadono in questa categoria di soluzioni sintattiche astruse. Eseguire assegnamenti dentro alle espressioni può sembrare sintetico e "fico", ma produce codice oscuro e difficile da debuggare. Nel linguaggio C c'è il salvagente del tipo delle variabili che salva da molti guai, ma in PHP il meccanismo del type juggling maschera questi problemi che poi diventano difficili da risolvere. Paradossalmente, gli assegnamenti dentro alle espressioni, che erano già una grana per i programmatori C, diventano ancora più subdoli per i programmatori PHP. Poi il fatto che l'operatore di assegnamento "=" e l'operatore di confronto per uguale "==" sono molto simili, completa la frittata. Nel prossimo esempio abbiamo due stringhe $a e $b e vogliamo stampare sullo schermo se sono uguali o diverse. Il programmatore ha scritto questo codice:

$a = "pippo";
$b = "pluto";
if ( $a = $b )
    echo "nomi uguali";
else
    echo "nomi diversi";

==> nomi uguali

Purtroppo l'espressione logica dell'if() fa una cosa completamente diversa da quello che vogliamo perché abbiamo scritto "=" (assegnamento) invece che "==" (confronto). In definitiva, l'espressione dentro all'if() sovrascrive $a con il valore di $b; il risultato dell'assegnamento è la stringa non vuota "pluto", ed è quest'ultimo valore che l'istruzione if() deve valutare come "espressione logica"; le regole del type juggling prescrivono che una stringa non vuota equivale a TRUE, per cui in definitiva il programma stampa "nomi uguali" anche se le stringhe sono diverse.

Un linguaggio più rigoroso vieterebbe l'assegnamento dentro all'espressione logica dell'if(EXPR), o perlomeno avvertirebbe che il risultato finale della valutazione della espressione EXPR è una stringa invece che un valore logico.

PROPOSTA:

Il controllo dell'uso dei tipi all'interno delle espressioni salverebbe anche da altri subdoli tranelli. Ad esempio, un giorno dovevo verificare se un certo numero intero $e fosse dispari, ecco il codice:

$n = 12;

if( $n & 1 == 0 )
    echo "pari";
else
    echo "dispari";

==> dispari

Non funziona. Per via dell'ordine di priorità degli operatori, l'espressione dentro all'if() viene valutata come $n & (1 == 0) invece che nel modo che mi aspettavo. Un controllo di tipo mostrerebbe subito che l'operazione di AND logico avviene tra un intero a sinistra e un valore logico TRUE a destra, e quindi darebbe subito un errore. Per il PHP, invece, il FALSE equivale a zero e nulla viene segnalato.

NOTA BENE. Non sto dicendo che il PHP debba diventare un linguaggio fortemente tipizzato, cosa che lo snaturerebbe. Sto dicendo che certi tipi di errore nel codice sono quasi certamente un errore logico che l'interprete può facilmente rilevare e che dovrebbe segnalare nell'interesse del programmatore. Solo così possiamo sperare che il PHP esca dalla nicchia delle "piccole applicazioni WEB" per diventare uno strumento di sviluppo robusto.

PHPLint controlla scrupolosamente l'uso dei tipi dentro alle espressioni e individua tutti i problemi descritti negli esempi di prima. PHPLint ammette gli assegnamenti all'interno delle espressioni, ma il programmatore viene tutelato dal controllo di tipo.

Troppi livelli di priorità degli operatori. Ci sono circa 20 livelli di priorità. Questo costringe il programmatore a consultare frequentemente la documentazione oppure a usare una quantità di parentesi tonde inutili per evitare subdoli errori. Due o tre livelli di priorità basterebbero, lasciando l'uso delle parentesi tonde solo a quei rari casi di espressioni realmente complesse. Ad esempio, si potrebbero suddividere gli operatori nelle categorie "additivi" (come "+" e "|") e "moltiplicativi" (come "*" e ">>").

PHPLint controlla le priorità degli operatori e segnala quando questi operatori vengono applicati su valori di tipo sbagliato, probabile sintomo che l'ordine di valutazione è diverso rispetto a quello che il programmatore si aspettava.

L'accesso ad un elemento inesistente di un array non dà errore. Pertanto è difficile a runtime evidenziare una gestione sbagliata degli indici, come nel seguente esempio:

$a = array("zero", "uno", "due");

echo "I primi dieci numeri sono:";
for($i = 0; $i < 10; $i++)
	echo " ", $a[$i];

==> I primi dieci numeri sono: zero uno due       

In questo esempio all'indice 0 corrisponde l'elemento "zero", all'indice 2 corrisponde "due", ma dall'indice 3 in poi il PHP restituisce NULL come valore e genera un E_NOTICE nei log (quando i notice sono abilitati, s'intende):

Notice:  Undefined offset: 3 in ... on line ...
Notice:  Undefined offset: 4 in ... on line ...
Notice:  Undefined offset: 5 in ... on line ...
Notice:  Undefined offset: 6 in ... on line ...
Notice:  Undefined offset: 7 in ... on line ...
Notice:  Undefined offset: 8 in ... on line ...
Notice:  Undefined offset: 9 in ... on line ...

Siccome poi il NULL viene renderizzato some la stringa vuota, il risultato è la stampa dei numeri da 0 a 2 e nient'altro, senza alcun avviso al programmatore.

Come workaround, io imposto sempre al massimo livello la maschera degli errori error_reporting(E_ALL|E_STRICT) in modo che vengano segnalati anche gli E_NOTICE, cosa utile anche per rilevare in generale l'uso di variabili non inizializzate. Ma ho fatto anche di più: ho installato un error handler che intercetta tutti gli errori e che trasforma anche gli E_NOTICE relativi all'accesso agli elementi di array che non esistono in eccezioni ErrorException. Questo contribuisce a rendere i programmi più solidi o, almeno, blocca i programmi che contengono evidenti errori. E' preferibile un programma morto a un programma sbagliato che continua a girare facendo danni imprevedibili.

La funzione isset($x) ritorna FALSE se $x vale NULL. Eppure NULL è un valore, ed è ben diverso lo status di una variabile "not set" da quello di una variabile che ha il valore NULL. Sarebbe più logico se la funzione isset() fosse il naturale complemento di unset(), dando così a NULL pieno diritto di esistere come valore significativo. Il manuale del PHP e il libro "Programming PHP" in generale lasciano intendere che una variabile non definita e una variabile che ha il valore NULL sono equivalenti, anche se in realtà non è vero. Per quanto riguarda le variabili ordinarie, questo non è un problema: abbiamo già detto che tutte le variabili devono essere definite prima di usarle, per cui non c'è ragione di usare isset().

Questo equivoco comporta invece qualche difficoltà quando sono coinvolti array i cui elementi possono esistere oppure no, e possono avere valori arbitrari incluso NULL. Se gli elementi di un array possono essere NULL, per verificare se un certo elemento dell'array esiste non potremo usare isset() perché questa funzione darebbe FALSE se l'elemento esiste ma vale NULL. Pertanto questo codice è sbagliato:

if( isset($users['profile']) ) { ... }

Il codice corretto per verificare l'esistenza di un elemento dell'array quando questo elemento potrebbe valere NULL è invece il seguente:

if( array_key_exists('profile', $users) ) { ... }

Vedere anche i bug #47032 (http://bugs.php.net/bug.php?id=47032) e #19080 (http://bugs.php.net/bug.php?id=19080) che descrivono questo problema.

Array associativi

Gli array del PHP sono implementati come array associativi, cioè associano un elemento di tipo arbitrario a una data chiave. Questo permette di costruire facilmente collezioni di dati e permette di reperire velocemente un dato a partire dalla chiave. Le chiavi sono di due tipi: numeri interi e stringhe. Lo specchietto qui sotto illustra come vengono trattate le chiavi fornite:



Tipo dell'indice Tipo risultante della chiave
int

Esempio: array(123 => "a")

L'entrata "a" dell'array viene riportato nella chiave intera 123.
string

Esempio: array("123" => "a")

Se la chiave è la rappresentazione equivalente di un numero intero allora viene considerata numero intero, altrimenti rimane stringa. La rappresentazione del numero è equivalente a numero intero se: 1. c'è un segno meno iniziale opzionale; 2. seguono una o più cifre 0...9; 3. il numero intero risultante è rappresentabile nel tipo int, cioè in generale se è compreso nel range che va da −2^31 fino a 2^31−1.
float

Esempio: array(1.23 => "a")

Il numero float viene troncato ad intero, e quindi la chiave risultante è intera. Se il numero float è troppo grande per essere rappresentato come int, il risultato non è definito.
(qualunque altro tipo) Qualsiasi altro tipo di indice non è mappabile in una chiave valida e produce il warning Illegal offset type; l'elemento di indice patologico non viene inserito nell'array.


Il problema è che il programmatore non ha alcun controllo sul tipo della chiave, perché è il PHP a decidere se una chiave verrà rappresentata internamente come int o come string. Infatti, se la chiave fornita "sembra" un numero intero, essa viene trattata come numero intero. In questo esempio definisco un array con diversi elementi. Le chiavi sono quasi tutte stringhe, ma il PHP tradurrà alcune di esse in numeri interi secondo un algoritmo intricato:

$a = array(
    "a" => "a",
    "123" => "123",
    "+123" => "+123",
    "-123" => "-123",
    "0123" => "0123",
    " 123" => " 123",
    "3000000000" => "3000000000",
    "123.0" => "123.0",
    1.9 => "1.9",
    3e9 => "3e9"
);

var_dump($a);
L'output prodotto è il seguente:
array(9) {
  ["a"]=> string(1) "a"
  [123]=> string(3) "123"
  ["+123"]=> string(4) "+123"
  [-123]=> string(4) "-123"
  ["0123"]=> string(4) "0123"
  [" 123"]=> string(4) " 123"
  ["3000000000"]=> string(10) "3000000000"
  ["123.0"]=> string(5) "123.0"
  [1]=> string(3) "1.9",
  [-2147483648]=> string(3) "3e9"
}
Osserviamo alcuni fatti sorprendenti:

In generale il programmatore PHP non è interessato alle questioni dei tipi, per cui a pochi interessa se il tipo dell'indice è la stringa "123" piuttosto che il numero 123, e questo perché nei programmi più semplici non serve interessarsi di queste sottigliezze.

Le cose cambiano se i programmi pretendono di confrontare gli indici in modo esatto, per esempio perché un codice prodotto "001" è diverso dal codice prodotto "1". In questo esempio definisco un array con un prodotto di chiave "001" e poi lo vado a cercare usando la chiave "1". Notare che il codice prodotto "001" viene salvato come stringa nella chiave dell'array dei prodotti:

$prodotti = array("001" => "Proddotto001");

$key = "1";

foreach($prodotti as $k => $v)
    if( $k == $key )
        echo "Trovato prodotto $v\n";

==> Trovato prodotto Proddotto001

Notare che viene trovato un prodotto sbagliato, perché il codice "001" è diverso dal codice "1", ma purtroppo l'operatore di confronto debole == ha sentenziato che le strighe "001" e "1" sono uguali. Un programma fatto in questo modo funzionerà correttamente nel 99,9% dei casi, ed esibirà comportamenti stravaganti che si presentano solo sporadicamente.

Il programmatore potrebbe risolvere usando l'operatore di uguaglianza forte ===. Questo risolverebbe il caso di prima dove la chiave era stringa, ma fallirebbe nel caso di chiave intera. Nel prossimo esempio il prodotto di codice "123" non viene ritrovato:

$prodotti = array("123" => "Proddotto123");

$key = "123";

foreach($prodotti as $k => $v)
    if( $k === $key )
        echo "Trovato prodotto $v\n";

==> (nessun prodotto trovato)

Il prodotto non viene trovato perché dentro all'array la chiave è stata convertita in numero intero, e l'operatore di confronto forte === esegue il confronto rigoroso per tipo e valore. Ne segue che in questo caso il prodotto, che pure c'è nell'array, non viene trovato.

Morale: attenzione alle chiavi degli array associativi. L'unico modo per assicurare che la chiave sia sempre una stringa è di aggiungere una lettera o uno spazio all'inizio della chiave, come in questo esempio:

$prodotti = array("x001" => "Prodotto001", "x123" => "Proddotto123");

Questioni legate all'OOP

Manca un modello di riferimento. Le funzionalità dell'OOP disponibili in PHP vengono sommariamente descritte dal manuale e liquidate in poche righe e al più un paio di esempi. Manca un modello di riferimento preciso, e questo porta spesso ad accesi dibattiti tra gli sviluppatori su questo o quell'aspetto, soprattutto quando vengono coinvolte oscure regole di ereditarietà o particolari combinazioni di attributi e visibilità. Le conseguenze sono che il programmatore ha difficoltà a chiarire l'argomento, non di rado emergono bachi, e risulta difficile scrivere un validatore statico affidabile.

Non sono ammesse le sotto-classi. Se una classe particolarmente complessa necessita di un'altra classe accessoria, è costretta a dichiararla globalmente. Tutte le classi, una volta dichiarate, sono pubbliche, anche se sono concepite solo per l'uso interno di un certo modulo di programma e non sono utilizzabili al di fuori di esso.

PHPLint introduce la keyword in meta-codice private che rende la classe privata al file che la contiene. In questo modo un file può creare le sue classi ad uso interno senza che queste interferiscano con gli altri file. Esempio:

/*. private .*/ class adUsoInterno {
    ...
}

Se un altro file tenta di usare o ridefinisce la classe privata, PHPLint segnala un errore. Per coerenza, le classi private non vengono riportate dal generatore di documentazione PHPLint Documentator.

I metodi privati non si possono ridefinire nelle classi estese. PHP 5 segnala un errore E_STRICT se la classe estesa ridefinisce un metodo privato con un numero diverso di argomenti. Pertanto i nomi dei metodi ad uso interno della classe è bene che non siano troppo banali per non incorrere in collisioni coi nomi di altri metodi delle classi derivate.

Si possono aggiungere dinamicamente proprietà ad un oggetto. L'istruzione $obj->xyz = 123; aggiunge la proprietà xyz all'oggetto $obj, anche se la classe a cui appartiene l'oggetto non l'ha dichiarata esplicitamente. Questo è comodo in certi casi, ma più spesso il programmatore ha semplicemente sbagliato a scrivere il nome della proprietà, come in questo esempio:

class DBQuery {
    public $db_conn = NULL;  # risorsa del DB aperto
    public $curr_res = NULL; # risultato dell'ultima query

    function __construct($db_name)
    {
        $this->db_con = pg_exec("dbname=$db_name");
    }

    ...
}

Visto l'errore? Il costruttore istanzia la proprietà "db_con" invece che quella prevista, che ha una enne in più. L'interprete non lamenta il problema, ma il programma non funzionerà. Consentire di creare nuove proprietà "al volo" induce il programmatore a scrivere codice mal documentato, perché nella dichiarazione della classe dovrebbero essere elencate tutte le proprietà.

PHPLint segnala come errore l'uso di una proprietà non definita, e quindi non permette di aggiungere dinamicamente proprietà ad un oggetto. Se questo è richiesto dalla logica del programma, reimplementare con un array associativo o con una proprietà che sia un array associativo.

In PHP5 l'attributo "static" dei metodi non sempre viene onorato. In questo esempio la classe Prova dichiara il metodo statico st() e il metodo non-statico ns(), e poi facciamo tutte le prove:

class Prova
{
    public static function st()
    { }

    public function ns()
    { }
}

Prova::st();
# Ok, normale chiamata a metodo statico

Prova::ns();
# Chiamata statica a metodo non statico.
# Se E_STRICT è abilitato allora si ottiene un errore non fatale:
# Strict Standards: Non-static method Prova::ns() should not
# be called statically in /home/salsi/prova.php on line 16

$obj = new Prova();

$obj->ns();
# Ok, normale chiamata "dinamica" a metodo non-statico.

$obj->st();
# Chiamata "dinamica" a metodo statico. Errato, ma nessuna segnalazione

Questo strano comportamento è noto e voluto per favorire un passaggio graduale dal PHP4 (dove non esiste l'attributo static) al PHP5. Ai giorni nostri comincia ad essere anacronistico e sarebbe ora di passare alla segnalazione di un errore fatale ogni volta che si richiama un metodo nel modo sbagliato. A quel punto la necessità di avere due operatori distinti "::" e "->" per richiamare un metodo verrebbe a cadere e sarebbe sufficiente mantenere solo uno dei due.

PHPLint segnala errore su chiamata non-statica di metodo statico e su chiamata statica a metodo non-statico. In definitiva, secondo PHPLint i metodi statici vanno chiamati staticamente con l'operatore :: mentre i metodi non-statici vanno chiamati con l'operatore −>. Lo stesso principio si applica alle proprietà statiche e non-statiche.

Il costruttore della classe genitrice non viene chiamato automaticamente dal costruttore della classe estesa. In generale quando si instanzia un oggetto di una classe estesa, il costruttore della classe genitrice viene chiamato automaticamente dal PHP. Tuttavia se la classe figlia definisce un nuovo costruttore, questo nuovo costruttore deve chiamare esplicitamente il costruttore della classe genitrice, come in questo esempio in PHP 5:

class Logger
{
    public /*. resource .*/ $fd;

    function __construct($logfile = "/tmp/logger.txt")
    {
        // qui lo stato viene inizializzato:
        $this->fd = fopen($logfile, "a");
    }

    function log(/*. string .*/ $msg)
    {
        fwrite($this->fd, $msg."\n");
    }

    function __destruct()
    {
        fclose($this->fd);
    }
}

class LoggerWithLabel extends Logger
{
    public /*. string .*/ $label;

    function __construct(/*. string .*/ $label)
    {
        parent::__construct();  # <-- inizializza Logger
        $this->label = $label;
    }

    function log(/*. string .*/ $msg)
    {
        parent::log($this->label . ": " . $msg);
    }
}

$log = new LoggerWithLabel("Ordinamento dati");
$log->log("inizio il lavoro");
// ...

La classe Logger implementa un (banale) logger di messaggi. Il costruttore apre il file di log e salva in una proprietà il corrispondente handle. La classe LoggerWithLabel estende Logger aggiungendo una etichetta ai messaggi. Il costruttore di questa nuova classe DEVE chiamare esplicitamente il costruttore della classe genitrice, che altrimenti non verrebbe inizializzata correttamente.

Se il nuovo costruttore non chiama il vecchio costruttore, lo stato dell'oggetto non viene inizializzato correttamente, con conseguenze imprevedibili; nel nostro caso la chiamata fwrite() in Logger::log() produrrebbe un Warning che avvisa il programmatore, ma in generale non è detto che le cose vadano così bene.

Lo stesso principio si applica ai metodi distruttori, che esistono solo in PHP 5. Se il metodo distruttore della classe estesa non chiama esplicitamente il distruttore della classe genitrice, l'oggetto non viene rilasciato correttamente. Nel nostro esempio la classe estesa eredita il distruttore della classe genitrice, per cui PHP chiama automaticamente il distruttore.

PHPLint impone che il costruttore della classe estesa chiami il costruttore della classe genitrice, e solleva un errore se non lo fa. Idem per i distruttori.

E' consentito ridefinire le proprietà pubbliche e protette. La classe estesa può definire una proprietà con lo stesso nome di un'altra proprietà della classe genitrice. In questo caso il valore della propietà originaria viene completamente oscurato, e risulta così irraggiungibile. Anche i metodi della classe genitrice accederanno alla nuova proprietà, anche se il suo contenuto è incongruo con il suo significato originario.

In questo esempio la classe MyLogger estende la classe Logger che abbiamo visto prima, ma accidentalmente ridefinisce la proprietà $fd. La conseguenza è fatale e la classe Logger non funzionerà:

class MyLogger extends Logger
{
    public $fd;

    // ...
}
PHPLint vieta di ridefinire le proprietà pubbliche e protette. Solo le proprietà private si possono ridefinire.

Le proprietà che non hanno una espressione che assegna un valore iniziale prendono il valore NULL. D'altro canto, non sapendo il PHP di che tipo è una proprietà, non potrebbe fare di meglio. E' a carico del programmatore assicurare che tutte le proprietà abbiano un valore iniziale definito, oppure il programmatore deve assegnare loro un valore nel metodo costruttore.

Critiche poco rilevanti

Queste sono le critiche al PHP che a me sembrano meno rilevanti ma che vengono spesso sollevate e che comunque meritano una discussione.

Expressione complessa con parentesi graffe. Intimidisce il nome dato a questa funzionalità dalla sintassi arcana. Ad esempio, se $x = 123; è una certa variabile, ecco i diversi modi per stamparla, tutti producono lo stesso risultato "123":

echo $x;      echo "$x";
echo ${x};    echo "${x}";
echo $  {x};
              echo "{$x}";

La colonna di sinistra mostra diversi modi per ottenere il valore della variabile $x, mentre la colonna di destra mostra diversi modi per inserire la stessa variabile all'interno di una stringa. Notare la varietà e l'asimmetria delle diverse sintassi. Altri risultati "strani":

echo "{$ x}";   # " 123"    spazi in eccesso dopo $ renderizzati
echo "{ $x}";   # "{ 123}"  spazi in eccesso prima di $ renderizzati
echo "$ {x}";   # "$ {x}"   ok
echo "${ x}";   # "123"     spazio non renderizzato
echo "${x+x}";  # ""        non so cosa vuol dire, ma non dà errore
echo "${x.x}";  # ""        idem

Tutto ciò è veramente utile o è solo una complicazione delle cose semplici?

Sintassi simil-C poco chiara. L'abbondanza di parentesi tonde e graffe ostacola l'apprendimento e oscura la struttura del sorgente. La scelta del C come linguaggio prototipo di tutti i nuovi linguaggi è una usanza purtroppo diventata comune, ma è una scelta poco felice. Vediamo alcuni punti al riguardo:

Manca una convenzione per i nomi delle funzioni. Non c'è una convenzione nei nomi delle funzioni. Gli schemi "verbo_nome()", "verbonome()", "nome_verbo()" e "nomeverbo()" vengono impiegati indifferentemente. Questo obbliga il programmatore a frequenti consultazioni del manuale. In verità, anche se una convenzione sui nomi venisse adottata, credo che io dovrei comunque fare ricorso al manuale, perché 3000 funzioni proprio non me le ricordo!

Non esiste una convenzione per l'ordine degli argomenti delle funzioni. [Idem.]

E' tedioso dover premettere il carattere '$' davanti alle variabili. La vera utilità di questo modo di scrivere le variabili sta nella possibilità di comporre delle stringhe che incorporano delle variabili. Scrivere

"Trovati $r record di $n."
è sicuramente più simpatico che scrivere
"Trovati " . $r . " record di " . $n . "."

L'altro caso di utilità è poter scrivere variabili dal nome variabile, come $$x, una pratica che dovrebbe essere scoraggiata visto che esistono soluzioni migliori.

PROPOSTA. Il tedioso $ davanti ai nomi delle variabili si potrebbe anche abolire, mantenedolo solo per le stringhe letterali come segnaposto a cui segue il nome della variabile da sostituire. In questa ipotetica evoluzione del PHP potremmo scrivere:

N = 10;
for( i=1; i<=N; i++ )
    echo "Record n. $i di $N = ", ...;

Insieme alla proposta che abbiamo avanzato prima di rendere unico lo spazio delle keyword e degli identificatori, questo codice non presenta nessuna ambiguità e sarebbe più chiaro e semplice.

La funzionalità dei "magic quotes" ha prodotto molti siti pieni di back-slash. Quando i magic quotes sono abilitati nel file di configurazione del PHP, l'interprete inserisce automaticamente il back-slash nelle stringhe di input, in modo da prevenire i soliti problemi di SQL injection. Per fortuna questa funzionalità viene disabilitata per default nelle ultime versioni del PHP, e si discute se eliminarla del tutto in futuro. Morale: non usare i magic quote nel nuovo software e vivere felici. Basta assicurare che in php.ini siano presenti queste righe:

magic_quotes_gpc = Off
magic_quotes_runtime = Off

La funzionalità dei register globals può compromettere la sicurezza nei programmi mal scritti. Anche questa funzionalità viene ormai disabilitata per default nelle ultime versioni del PHP. Esistono però dei software un po' vecchiotti che ne fanno uso. Morale: non usare il register globals nel nuovo software e vivere felici. Basta assicurare che nel php.ini sia presente questa riga:

register_globals = Off
PHPLint segnala come errore l'uso del valore di una variabile non inizializzata. Come conseguenza non si possono validare con PHPLint programmi che richiedono la funzionalità dei register_globals. In altri termini, PHPLint riporta come errati i programmi che usano i register_globals, dato che contengono variabili non inizializzate esplicitamente nel codice.

In un assegnamento e nel passaggio di argomenti a funzione, gli oggetti in PHP4 vengono copiati; in PHP5 viene copiato solo il riferimento. Abbiamo già discusso la questione quando abbiamo parlato degli array. Pare che questo cambiamento di semantica nel passaggio a PHP5 non abbia prodotto troppi incidenti, ed è stato un cambiamento doveroso. E' consigliabile che tutti i nuovi programmi vengano concepiti in PHP5.

Alcune parti del PHP non sono thread-safe per cui non girano correttamente su Apache 2.

Il garbage collector (GC) non riconosce i riferimenti circolari (http://bugs.php.net/bug.php?id=33595). Questo problema è comune anche ad altri GC, e comporta che a volte la memoria non più utilizzata non viene rilasciata (ad esempio un oggetto A che fa riferimento a B che fa riferimento di nuovo ad A). In generale è bene che il programmatore eviti strutture dati con riferimenti circolari. In alternativa, il programmatore deve gestire questi casi da programma, rimuovendo quando necessario i riferimenti circolari.

Manca supporto ai caratteri internazionali per l'accesso al file system. Quando in PHP si deve creare o accedere a un file, le librerie del PHP permettono di specificarne il nome attraverso una stringa. Tuttavia il manuale non chiarisce quale dev'essere la codifica dei nomi dei file. L'articolo PHP e caratteri esotici (www.icosaedro.it/articoli/php-i18n.html) affronta più estesamente questo argomento e propone alcune soluzioni.

Manca un framework ufficiale di riferimento per le applicazioni WEB. Il pensiero va alla tecnologia .Net di Microsoft che offre al programmatore di applicazioni WEB una serie di strumenti più astratti per la gestione dell'autenticazione, dei FORM, delle sessioni e altro ancora. Il programmatore PHP solitamente tende a reinventare la ruota, cerca di riutilizzare parti del proprio software, oppure si affida a una serie di pacchetti di terze parti.

Rimedi

Mentre aspettiamo che i volenterosi sviluppatori del PHP ci presentino un giorno una versione riveduta e corretta del nostro linguaggio di programmazione preferito (magari applicando anche qualcuno dei suggerimenti che ho indicato qui), ecco cosa possiamo fare per migliorare i nostri programmi.

Abilitare sempre tutti i messaggi diagnostici. Nel file php.ini deve apparire:

error_reporting = E_ALL | E_STRICT
(E_STRICT è solo per PHP5).

Salvare i messaggi diagnostici in un log file che potremo esaminare con calma; attivare tutte le modalità diagnostiche, ma evitare di inviare messaggi di errore all'utente (l'utente normale ne è solo infastidito, ma sarebbero molto utili al cracker...):

display_errors = Off
display_startup_errors = Off
log_errors = On
warn_plus_overloading = On

Esaminare con regolarità il log file, e lavorare in modo da eliminare tutti i messaggi di errore, inclusi i NOTICE più insignificanti. Il nostro lavoro sarà completo quando l'applicativo WEB sarà in grado di girare senza emettere alcuna lamentela.

Nel php.ini disabilitare le funzionalità pericolose o sconsigliate:

magic_quotes_gpc = Off
magic_quotes_runtime = Off
register_globals = Off

Riporre la massima cura nella logica delle espressioni. Non mischiare tipi di dato diversi. Convertire esplicitamente le stringhe in numeri quando necessario, perché questo migliora anche l'efficienza del programma.

Se durante lo sviluppo sorgono dei dubbi su determinati punti, lasciare sempre un commento chiaramente identificabile, e ripromettersi di rimeditare la questione a mente fresca. Io uso sempre mettere la parola "FIXME" nei commenti. Applicare questa regola in modo sistematico. In Unix basta fare un "grep FIXME programma.php" per vedere lo stato di maturità raggiunto dal sorgente. La mancanza di FIXME è un buon indice.

Tenere sempre conto delle condizioni di errore. Le funzioni della libreria del PHP ritornano spesso un valore speciale per indicare le condizioni di errore, tipicamente FALSE o NULL. Ad esempio, fopen() ritorna una risorsa se ha successo, mentre ritorna FALSE su errore, per cui il test per verificare la condizione di errore è

$f = fopen("pippo", "r");
if( $f === FALSE ) die("apertura fallita del file 'pippo'");

In caso di errore sarebbe meglio fornire una descrizione più dettagliata nel file di log e proporre un messaggio più amichevole all'utente, ad esempio:

$f = @fopen("pippo", "r");
if( $f === FALSE ) {
    echo "<html><body>Errore interno al programma, spiacente!</body></html>";
    trigger_error("fopen(pippo, r): " . (string) error_get_last()['message'], E_USER_ERROR);
    exit(1);
}

Questo lavoro richiede un minuto e fa risparmiare ore di frustrante debugging.

Non abusare dell'operatore "@" per nascondere i problemi! Usare @ solo se seguito da adeguato test della condizione di errore e relativo report su log file.

Sottoporre il sorgente a un validatore sintattico/semantico, come PHPLint. Quando un sorgente supera la validazione di PHPLint ed è stato depurato da tutti i FIXME, ci sono buone possibilità che funzioni a dovere.

Contemplare con compiacimento il proprio sorgente. Stampare le parti più complesse e rileggere attentamente, seguendo la logica del programma passo-passo. Cambiare i nomi di funzioni, variabili, ecc. fino a quando riflettono bene il loro significato. Le funzioni dovrebbero esprimere azioni, le variabili dovrebbero esprimere cose/entità reali. Curare la strutturazione del sorgente e l'indentazione delle istruzioni, adottando uno stile uniforme. Aggiungere linee di spazio per evidenziare gruppi di istruzioni che eseguono un dato compito, magari indicando in un commento cosa fanno, cosa entra e cosa esce; tale gruppo di istruzioni è suscettibile di essere poi trasformato in una funzione. Rendere il programma ben strutturato e modulare spesso produce codice più facilmente riutilizzabile perché se ne possono estrarre determinate parti.

Non cominciare a buttare giù codice senza avere le idee chiare sul da farsi. Ogni progetto, anche se piccolo, deve avere una analisi dei requisiti dettagliata. Il livello di dettaglio deve essere tale da lasciare il programmatore concentrarsi sul "coding" e basta.

Adottare un buon programma per il controllo di versione, come CVS. E' l'unico modo per scoprire come mai un programma che funzionava perfettamente fino a ieri, improvvisamente si è messo a fare le bizze dopo le ultime insignificanti modifiche di oggi. Se il progetto viene sviluppato da diverse persone, è l'unico modo di tenere sotto controllo la situazione evitando derive verso il caos. Esistono interfaccie grafiche di facile uso: usiamole!

Costruire automatismi per il rilascio del programma e il deploy. In Unix ci sono "bash" e "make" con i quali è facile costruire questi piccoli automatismi. Ricordarsi di mettere un numero di versione e di "taggare" il sorgente rilasciato sul CVS.

Mantenere un elenco dei problemi rimasti in sospeso (la bug list). Esistono programmi appositi, ma in mancanza d'altro un semplice file di testo può andare. Mettere anche questo sotto CVS, così da sapere chi, quando e come lo ha modificato.

Beta testing. Lo devono fare dei programmatori, non persone digiune di informatica. Solo a un programmatore può venire in mente di inserire il valore "−1" nel campo "quantità" della maschera di ordinazione di un prodotto e così ottenere un totale fattura negativo. Solo a un programmatore può venire in mente di interrogare lo script inviando valori deliberatamente artefatti. In generale, eseguire test di safety e security. Usare le specifiche dell'analisi dei requisiti e tentare di fare tutto il contrario di quello che è indicato lì: mettere numeri dove il programma si aspetta lettere e viceversa, lasciare vuoti i campi obbligatori e riempire all'inverosimile quelli facoltativi. Richiamare le pagine del sito in ordine arbitrario passando dall'history del browser. Disabilitare cookies e JS: che succede?

Ok, il Cliente vuole il prodotto entro domani e ce lo paga anche poco. Se la priorità è il tempo rispetto alla qualità, adeguarsi a sviluppare codice "basta che vada", ma la garanzia vale una settimana e deve essere controfirmata come clausola vessatoria nel contratto (formula "visto e piaciuto").

Riferimenti

Argomenti correlati


Umberto Salsi
Commenti
Contatto
Mappa
Home / Indice sezione
Still no comments to this page. Use the Comments link above to add your contribute.