La gestione delle informazioni provenienti dai moduli HTML è un'attività che da un lato richiede estrema attenzione e dall'altro tende spesso ad essere noiosa e ripetitiva, come se non bastasse quando ci si trova a raccogliere e validare le informazioni provenienti da form contenenti numerosi campi (tanto più se in precedenza è mancata una pianificazione attenta) gli script lato-server hanno la fastidiosa tendenza a trasformarsi in una serie disordinata di IF...ELSE... annidati.
Se poi il cliente si sbizzarrisce a chiedere modifiche e aggiustamenti dell'ultimo minuto, allora la situazione rischia di diventare davvero tragica.
In questo articolo e in quelli che seguiranno cercheremo di descrivere come, anche in questo caso, la programmazione Object Oriented sia di enorme aiuto nel creare degli script ordinati e facili da mantenere.
Come sempre quando si tratta di OOP bisogna partire da lontano, pianificare con pazienza evitando di gettarsi a capofitto nel codice, ecco perchè in questa prima parte cominceremo a creare delle classi che effettuino proprio quei controlli standard che solitamente siamo costretti ad eseguire attraverso una serie di fastidiose espressioni condizionali: chiameremo queste classi "rules", ovvero regole.
La lettura dell'articolo richiede che si disponga già di una certa confidenza con la programmazione a oggetti, argomento al quale freephp.html.it ha già dedicato numerosi articoli.
I messaggi di errore
Per prima cosa creiamo un file contenente alcuni messaggi di errore in italiano (così ci sarà facile passare da una lingua all'altra).
<?php
/*
File "it_error_const.inc.php"
*/
/*
Errori per campi singoli
*/
define('STR_TOO_LONG', '"%s" non può contenere più di %d caratteri' ) ;
define('STR_TOO_SHORT', '"%s" deve contenere almeno %d caratteri' ) ;
define('STR_BAD_RANGE', '"%s" deve contenere da %d a %d caratteri') ;
define('STR_BAD_LEN', '"%s" deve contenere %d caratteri') ;
/*
Errori per campi multipli (es. combo box)
*/
define('ARR_TOO_LONG', '"%s" non può contenere più di %d elementi' ) ;
define('ARR_TOO_SHORT', '"%s" deve contenere almeno %d elementi' ) ;
define('ARR_BAD_RANGE', '"%s" deve contenere da %d a %d elementi') ;
define('ARR_BAD_LEN', '"%s" deve contenere %d elementi') ;
define('LACK_OF_ELEMENTS', '"%s" non contiene tutti gli elementi richiesti') ;
define('IS_REQUIRED', '"%s" è un campo obbligatorio') ;
define('BAD_PATTERN', 'Il contenuto di "%s" non corrisponde al formato richiesto') ;
define('NOT_NUM', '"%s" non contiene un numero valido') ;
define('NUM_BAD_RANGE', 'il valore di "%s" deve essere compreso tra %d e %d') ;
.
.
.
Come vedremo più avanti questi messaggi verranno utilizzati grazie alla funzione sprintf()
La gestione degli errori
Il file che presenta i messaggi di errore contiene anche una classe generica e astratta per la gestione degli errori: da questa classe erediteranno, direttamente o indirettamente, tutte le altre che andranno a comporre il nostro mini-framework. In PHP4 le classi astratte non esistono (le cose cambiano con PHP5) ma nulla ci impedisce utilizzare una classe qualsiasi come se fosse un modello astratto.
/*
Sempre nel file "it_error_const.inc.php"
*/
.
.
.
/* Classe astratta per la gestione degli errori */
class AbstrErrHandler{
/*
Raccoglie gli errori
*/
var $errors ;
/*
Costruttore */
function AbstrErrHandler()
{
$this->errors = array() ;
}
function isValid()
{
/*
TRUE se 0 errori */
return !(bool)count($this->errors) ;
}
/*
Restituisce il primo errore verificatosi */
function getError()
{
return array_shift($this->errors) ;
}
/* Restituisce tutto lo stack degli errori */
function getErrors()
{
return $this->errors ;
}
}//END AbstrErrHandler
?>
Una regola astratta
Il passo successivo è creare una regola astratta che funga da modello a quelle concrete che applicheremo per validare i campi:
<?php
/*
Include i messaggi e la gestione degli errori
*/
include_once('it_error_const.inc.php') ;
/*
Classe astratta, definisce il modello da cui deriveranno tutte le altre regole
*/
class AbstractRule extends AbstrErrHandler
{
/*
Contiene i messaggi di errore
*/
var $errors = array() ;
/*
costruttore
*/
function AbstractRule()
{
}
/*
Effettua il parsing alla caccia di errori
riceve come argomenti il valore e l'etichetta del campo
*/
function check($val, $label)
{
}
}//end class
?>
La prima regola concreta
La prima regola che creeremo è molto semplice.
<?php
class IsNumericRule extends AbstractRule
{
function isNumericRule(){
parent::AbstractRule() ;
}
function check($val, $label)
{
if( !is_numeric($val) )
{
$this->errors[] = sprintf(NOT_NUM, $label) ;
}
}
}//end class
?>
Ha il solo scopo di verificare che il contenuto di un campo sia di tipo numerico...non potevamo semplicemente ricorrere a is_numeric? Ovviamente sì ma quando abbiamo a che fare con gli oggetti ciò che si perde in immediatezza si guadagna in flessibilità a lungo termine, e ce ne accorgeremo tra poco.
Per un esempio di utilizzo facciamo riferimento alla fine del prossimo paragrafo.
Controllo "range" sui numeri
La seconda regola verifica contemporaneamente che il contenuto di un campo sia numerico e che sia compreso tra certi valori.
<?php
class NumRangeRule extends IsNumericRule
{
/*
il valore massimo e quello minimo
*/
var $min ;
var $max ;
function NumRangeRule($min, $max){
parent::IsNumericRule() ;
$this->min = $min ;
$this->max = $max ;
}
function check($val, $label)
{
parent::check($val, $label) ;
if($val < $this->min || $val > $this->max)
{
$this->errors[] = sprintf(NUM_BAD_RANGE, $label, $this->min, $this->max) ;
}
}
}//end class
?>
Esempio di utilizzo
<?php
$field = new NumRangeRule(10, 20) ;
$field->check(30, 'Num utenti') ;
if(!$field->isValid())
{
echo $field->getError(), "n" ;
}
?>
viene stampato l'errore: "il valore di "Num utenti" deve essere compreso tra 10 e 20".
La classe appena vista eredita le caratteristiche della precedente e questa possibilità di "sommare" le regole diventa molto utile quando dobiamo applicare controlli complessi.
Controllo "range" sulle stringhe
La classe per il controllo sulla lunghezza delle stringhe è un po' lunga quindi per ragioni di spazio vi invitiamo a esaminarla scaricando il file zip che contiene tutti gli esempi. Come è intuibile lo scopo di questa regola è verificare che il contenuto di un campo rispetti una determinata lunghezza.
Controllo "Pattern"
<?php
/*
Verifica la corrispondenza a un pattern
*/
class PatternRule extends AbstractRule
{
var $regex ;
function PatternRule($regex)
{
parent::AbstractRule() ;
$this->regex = $regex ;
}
function check($val, $label)
{
$val = trim($val) ;
if( !preg_match($this->regex, $val) )
{
$this->errors[] = sprintf(BAD_PATTERN, $label) ;
}
}//end function
}//end class
?>
Questa regola standardizza il controllo sul formato di una stringa, è una delle più utili e applicate con maggiore frequenza. Il codice e i commenti sono abbastanza esplicativi, quindi ci limitiamo a mostrare un caso di utilizzo:
<?php
$field = new PatternRule('/^[a-zA-Z0-9_]+$/') ;
$field->check('Fabio#Heller', 'Username') ;
if(!$field->isValid())
{
echo $field->getError(), "n" ;
}
?>
L'errore stampato sarà il seguente: "Il contenuto di "Username" non corrisponde al formato richiesto"
Conclusioni
Le classi appena viste potrebbero apparire da un lato banali, dall'altro strumenti troppo complessi per eseguire delle operazioni così comuni. In realtà i pregi e le funzionalità non vanno valutati considerando il singolo oggetto isolato: i vantaggi si evidenziano nel momento in cui, osservando l'applicazione nel suo complesso, gli oggetti riescono a cooperare tra loro in modo efficiente.
Nelle prossime pagine introdurremo altre classi che rappresentano delle regole di validazione più complesse.
Controllo su campi multipli
Non sempre i dati contenuti nel campo di un form posso essere ricondotti ad un valore semplice, numerico o letterale: esistono elementi (select, insiemi di checkbox) che possono restituire insiemi di dati che PHP traduce in array.
Con questo tipo di valori i controlli devono poter agire in due sensi sull'insieme nel suo complesso e, opzionalmente, su ogni singolo valore presente nell'insieme.
Nel secondo caso possiamo riutilizzare le regole del tipo già visto, mentre per il primo tipo di validazione è necessario crearne di apposite.
Di seguito ne presentiamo uno soltanto, invitandovi a fare riferimento al file "regole.php" (contenuto nel file zip allegato a questa serie di articoli) per un esame più completo.
Fieldset che deve contenere uno o più elementi
<?php
/*
Verifica che il contenuto di un fieldset multiplo contenga uno o più elementi predefiniti
*/
class HasElementsRule extends AbstractRule
{
/*
Gli elementi da cercare
*/
var $elements ;
function HasElementsRule($arrElms)
{
parent::AbstractRule() ;
$this->elements = $arrElms ;
}
function check( $val, $label, $allRequired = false )
{
$result = array_intersect($this->elements, $val) ;
$error = sprintf(LACK_OF_ELEMENTS, $label) ;
if($allRequired)
{
if(count($result) < count($this->elements) )
{
$this->errors[] = $error ;
}
}
elseif( empty($result) )
{
$this->errors[] = $error ;
}
}//END function
}//END class
?>
Esempio di utilizzo
<?php
$field = new HasElementsRule( array('pippo', 'paperino') ) ;
$field->check( array('paperoga', 'archimede'), 'Personaggi Disney', true) ;
if(!$field->isValid())
{
echo $field->getError() ;
}
?>
Nel'esempio precedente otterremo l'errore: "'Personaggi Disney' non contiene tutti gli elementi richiesti".
La classe è abbastanza flessibile da poter richiedere sia la presenza di tutti gli elementi che di uno soltanto, è sufficiente impostare correttamente il terzo parametro del metodo check().
Applicare le regole ai campi
Fino ad ora abbiamo soltanto visto come creare delle regole funzionanti, ora è giunto il momento di esaminarne l'applicazione concreta ai dati provenienti da un form.
Immaginiamo per un momento alle variabili contenute negli array $_GET, $_POST o $_REQUEST come se fossero oggetti, ebbene è proprio a questi oggetti che dovremo applicare una o più regole di validazione.
Le istanze della classe FrmField rappresentano gli oggetti in questione che, in base al buon esito delle regole che applicheremo, risulteranno validi o non validi.
Rappresentazione OOP di un campo singolo
<?php
/*
Classe che rappresenta un campo singolo di un form
*/
class FrmField extends AbstrErrHandler
{
/*
Le regole da applicare
*/
var $rules ;
/*
Il valore contenuto dal campo
*/
var $val ;
/*
Il valore da attribuire di default
*/
var $defVal ;
/*
L'etichetta del campo
*/
var $label ;
/*
Il campo è obbligatorio?
*/
var $req ;
/*
Costruttore
*/
function FrmField($val, $label, $required, $rules = array(), $default = '' )
{
parent::AbstrErrHandler() ;
$this->rules = &$rules ;
$this->label = $label ;
$this->req = $required ;
/*
Se la variabile non esiste viene settata ad un valore di default
*/
if( !isset($val) )
{
$val = $default ;
}
$this->val = &$val ;
$this->defVal = $default ;
}//END Function
function validate()
{
/*
Se il campo è vuoto
*/
if( empty($this->val) )
{
/*
Se è obbligatorio
*/
if($this->req)
{
/*
Se è obbligatorio settiamo un errore
*/
$this->errors[] = sprintf(IS_REQUIRED, $this->label) ;
}
/*
Se vuoto e non obbligatorio inutile fare altri controlli e usciamo
*/
return ;
}//END if empty
/*
Altrimenti procediamo ai controlli successivi
*/
while( list(, $rule) = each($this->rules) )
{
$rule->check($this->val, $this->label) ;
if( !$rule->isValid() )
{
$this->errors[] = $rule->getError() ;
/*
In caso di errori ripristina il valore di default al
posto di quello non corretto
*/
$this->val = $this->defVal ;
return ;
}
}//end while
}//end function validate
}//end class
?>
Esempio di utilizzo
Validiamo un codice di avviamento postale:
<?php
$field = new FrmField('', 'cap', true, array(new StrRangeRule(5,5), new IsNumericRule()) ) ;
$field->validate() ;
if($field->isValid())
{
echo $field->val ;
}
else
{
echo $field->getError() ;
}
?>
Il codice ci mostra l'applicazione di più regole, o filtri, alla variabile in questione: il CAP in questo caso è obbligatorio e, ovviamente, deve essere numerico e di lunghezza 5 caratteri.
Rappresentazione OOP di un campo multiplo
<?php
/*
Classe che rappresenta un campo multiplo (es. select multipla) di un form
*/
class FrmFieldSet extends AbstrErrHandler
{
/*
L'array di valori contenuti nel fieldset
*/
var $val ;
/*
Le regole da applicare all'intero fieldset
*/
var $fieldSetR ;
/*
Le regole da applicare ai singoli valori contenuti
*/
var $fieldR ;
/*
L'etichetta del fieldset
*/
var $label ;
/*
Il valore da attribuire di default
*/
var $defVal ;
/*
Il campo è obbligatorio?
*/
var $req ;
function FrmFieldSet($sentVals, $label, $required, $fieldSetRules = array(), $fieldRules = array(), $default = array() )
{
parent::AbstrErrHandler() ;
/*
Se la variabile non esiste viene settata ad un valore di default
*/
if( !isset($sentVals) )
{
$sentVals = $default ;
}
$this->val = &$sentVals ;
$this->req = $required ;
$this->defVal = $default ;
$this->label = $label ;
$this->fieldSetR = &$fieldSetRules ;
$this->fieldR = &$fieldRules ;
}
/*
Determina se un valore è presente nel fieldset
*/
function selected($what)
{
return in_array($what, $this->val) ;
}
/*
Esegue i controlli sul fieldset
*/
function validate()
{
/*
Se il campo è vuoto
*/
if( empty($this->val) )
{
/*
Se è obbligatorio settiamo un errore
*/
if($this->req)
{
$this->errors[] = sprintf(IS_REQUIRED, $this->label) ;
}
/*
Se vuoto e non obbligatorio inutile fare altri controlli e usciamo
*/
return ;
}//END if empty
/*
Altrimeti ciclo di controlli sul fieldset nel suo complesso
*/
while( list(, $rule) = each( $this->fieldSetR) )
{
$rule->check($this->val, $this->label) ;
if( !$rule->isValid() )
{
$this->errors[] = $rule->getError() ;
/*
In caso di errori ripristina il valore di default al
*/
$this->val = $this->defVal ;
return ;
}
}//end while
/*
ciclo di controlli su ogni campo nel fieldset
*/
while( list(, $rule) = each($this->fieldR) )
{
while( list(, $field) = each($this->val) )
{
$rule->check($field, $this->label) ;
if( !$rule->isValid() )
{
$this->errors[] = $rule->getError() ;
/*
Ripristina il valore di default al
posto di quello non corretto
*/
$this->val = $this->defVal ;
return ;
}//END if
} //END while2
} //END while1
}//end function "validate"
}//END class
?>
Tipi ricorrenti
Visto che esistono tipi di dato ricorrenti, come appunto il CAP, numeri telefonici e indirizzi e-mail possiamo anche pensare di codificare questi tipi in PHP, come negli esempi che seguono.
Tipo CAP
<?php
class CapField extends FrmField
{
function CapField($val, $label, $required, $default='')
{
$rules = array() ;
$rules[] = new PatternRule("/^d{5}$/") ;
parent::FrmField($val, $label, $required, $rules, $default) ;
}
}//end class
?>
In nome della semplicità e in barba alle prestazioni abbiamo riscritto il controllo sul CAP utilizzando un'espressione regolare. Non ci sono limiti al numero di tipi che possiamo descrivere, anche se ovviamente devono essere ricorrenti perchè ne valga la pena, in tutti gli altri casi possiamo servirci delle classi più generiche FrmField e FrmFieldset.
Tipo e-mail
<?php
class MailField extends FrmField
{
function MailField($val, $label, $required, $default='')
{
$rules = array() ;
$rules[] = new PatternRule("/^[a-zA-Z]{1}w{1,10}[-|.|_]{0,1}w{0,10}@w{3,10}.w{0,10}-{0,1}w{0,10}.{0,1}w{2,6}$/i") ;
parent::FrmField($val, $label, $required, $rules, $default) ;
}
}//end class
?>
Tipo telefono
<?php
class PhoneField extends FrmField
{
function PhoneField($val, $label, $required, $default='')
{
$rules = array() ;
$rules[] = new PatternRule("/^+{0,1}d{0,4}[-|| |/]{0,1}d{3,5}[-||/]{0,1}d{5,10}$/U") ;
parent::FrmField($val, $label, $required, $rules, $default) ;
}
}//end class
?>
Un esempio di utilizzo per tutti: verifichiamo un indirizzo e-mail:
<?php
$z = new MailField('s1234@html.it', 'e-mail', true) ;
$z->validate() ;
if($z->isValid())
{
echo $z->val ;
}
else
{
echo $z->getError() ;
}
?>
Dopo aver esaminato tante classi qualcuno potrebbe chiedersi se il gioco valga la candela e se una programmazione a oggetti così esasperata possa comportare dei problemi di prestazioni in PHP, cercheremo di fornire una risposta ad entrambe le questioni nelle prossime pagine.
Nella terza e ultima parte di questo articolo capiremo come utilizzare concretamente le classi esaminate fino ad ora così da rendere più semplice, e al tempo stesso più solida, la validazione dei form html.
Per prima cosa descriviamo l'ultima classe del nostro piccolo framework: SmartForm.
<?php
/*
Disabilitiamo il flushing immediato
(vedere http://it2.php.net/manual/en/ref.outcontrol.php)
*/
ob_implicit_flush(0) ;
class SmartForm extends AbstrErrHandler
{
function SmartForm($triggerVar, &$fields, $crossChecks = array() )
{
$this->fields = &$fields ;
/*
Flag che determina se il form è stato inviato
*/
$this->isSent = isset($_REQUEST[$triggerVar]) ;
/*
Se i dati sono stati inviati
*/
if( $this->isSent )
{
/*
Procedo ai controlli incrociati sui campi
*/
while( list($key1, ) = each($crossChecks) )
{
if( !$crossChecks[$key1]->isValid() )
{
$this->errors[] = $crossChecks[$key1]->getError() ;
}
}//END while
/*
Se ci sono già errori, inutile proseguire
*/
if( !$this->isValid() )
{
return ;
}
/*
Altrimenti procedo alla verifica dei singoli campi
*/
while( list($key2,) = each($fields) )
{
$fields[$key2]->validate() ;
/*
Raccolgo gli eventuali errori
*/
if( !$fields[$key2]->isValid() )
{
$this->errors[] = $fields[$key2]->getError() ;
}
}//END while
}//END if
}//END constructor
/*
Metodo che preleva il template che contiene il form HTML e lo invia all'output
*/
function display($template)
{
/*
Variabili da utilizzare nel template
*/
$FORM = &$this ;
$FIELDS = &$this->fields ;
/*
Attiviamo l'output buffering per ridurre i vari
print contenuti nel template ad un'unica operazione di output
*/
ob_start() ;
include($template) ;
ob_end_flush() ;
}//END display
}//END class
?>
Questa classe rappresenta il cuore del nostro sistema di validazione: utilizza le istanze delle classi che abbiamo definito in precedenza per applicare dei controlli ferrei ad ogni campo inviato.
Riceve come argomenti il nome della variabile-flag che ci dice che il form è stato inviato, l'array di oggetti-campo da validare e un terzo array di cui parleremo tra poco.
Un form d'esempio
Nel box sottostante presentiamo il template di un form ipotetico:
<!-- file testTpl.php -->
<strong>SMART FORMS TEST</strong>
<?php
$errors = $FORM->getErrors() ;
if( !empty($errors) )
{
echo "<br><br>" ;
echo( join("<br>rn" , $errors) ) ;
echo "<br><br>" ;
}
?>
<form action="index.php" method="post" name="testForm">
<strong>Nome:</strong>
<input name="nome" type="text" value="<?php echo $FIELDS['nome']->val ?>" size="20" maxlength="15"><br>
<strong>Cognome:</strong>
<input name="cognome" type="text" value="<?php echo $FIELDS['cognome']->val ?>" size="20" maxlength="20"><br><br>
<strong>Userid (e-mail):</strong>
<input name="userid" type="text" value="<?php echo $FIELDS['userid']->val ?>" size="30" maxlength="40"><br>
<strong>Password: </strong>
<input name="pw1" type="password" value="<?php echo $FIELDS['pw1']->val ?>" size="10" maxlength="10"><br>
<strong>Ripeti password:</strong>
<input name="pw2" type="password" value="<?php echo $FIELDS['pw2']->val ?>" size="10" maxlength="10"><br><br>
<input name="invia" type="submit" value="Invia">
</form>
E la pagina PHP che lo utilizza
<?php
/*
Inclusione delle varie classi
*/
include_once('./it_error_const.inc.php') ;
include_once('./regole.php') ;
include_once('./tipi.php') ;
include_once('./form_handler.php') ;
/*
In produzione sopprimiamo gli invevitabili notice sui campi che ancora non esistono
*/
error_reporting(E_ALL ^ E_NOTICE) ;
/*
Un controllo incrociato sui campi password
*/
class CrossPassword extends AbstrErrHandler
{
function CrossPassword($pass1, $pass2, $error)
{
if($pass1 != $pass2)
{
$this->errors[] = $error ;
}
}//END CrossPassword
}
/*
Array degli oggetti campo
*/
$fields = array() ;
/*
Array degli oggetti controllo incrociato
*/
$cross = array() ;
/*
Definizione dei campi
*/
$fields['nome'] = new FrmField( $_REQUEST['nome'], 'Nome', false, array( new PatternRule("/^w{3,15}$/i") ) ) ;
$fields['cognome'] = new FrmField( $_REQUEST['cognome'], 'Cognome', false, array( new PatternRule("/^w{3,20}$/i") ) ) ;
$fields['userid'] = new MailField( $_REQUEST['userid'], 'Userid', true) ;
$fields['pw1'] = new FrmField( $_REQUEST['pw1'], 'Password 1', true, array( new StrRangeRule(8, 10) ) ) ;
$fields['pw2'] = new FrmField( $_REQUEST['pw2'], 'Password 2', true, array( new StrRangeRule(8, 10) ) ) ;
/*
Definizione dei controlli incrociati
*/
$cross[] = new CrossPassword($_REQUEST['pw1'], $_REQUEST['pw2'], 'I campi password non contengono gli stessi valori') ;
$form = new SmartForm('invia', $fields, $cross) ;
/*
Se il form non è stato inviato o è inviato ma ci sono errori
*/
if( !$form->isSent || !$form->isValid() )
{
/*
Mostra il form ed eventuali errori
*/
$form->display('testTpl.php') ;
}
//Se invece è tutto ok
//elabora i dati inviati
echo 'Dati inviati:<br><br>' ;
var_export($_POST) ;
?>
Il codice commentato dovrebbe essere sufficientemente esplicativo, tuttavia è necessario spendere due parole sui controlli incrociati, cioè sui controlli che coinvolgono confronti tra più campi del form: la soluzione più naturale è che queste regole debbano essere specificate separatamente e che attengano all'oggetto form anzichè ai singoli oggetti campo, ecco il perchè del terzo argomento presente nel costruttore "$crossChecks".
Ed ecco il risultato definitivo dei nostre "fatiche Object Oriented".
Conclusioni
Abbiamo utilizzato la programmazione a oggetti per ottenere una procedura standard e riutilizzabile per la validazione dei form HTML, come sempre, quando si ha a che fare con l'OOP, la fase di progettazione è la più impegnativa tuttavia il tempo speso nello studio delle classi successivamente viene ampiamente recuperato nell' applicazione concreta di quanto realizzato.