Validazione e gestione delle eccezioni dall'interfaccia utente al back-end

Prima o poi nella tua carriera di programmatore ti troverai di fronte al dilemma della validazione e della gestione delle eccezioni. Questo è stato il caso anche per me e il mio team. Un paio di anni fa abbiamo raggiunto un punto in cui dovevamo intraprendere azioni architettoniche per accogliere tutti i casi eccezionali che il nostro progetto software abbastanza grande doveva gestire. Di seguito è riportato un elenco di pratiche che abbiamo imparato a valutare e applicare quando si tratta di convalida e gestione delle eccezioni.


Validazione e gestione delle eccezioni

Quando abbiamo iniziato a discutere del nostro problema, una cosa è emersa molto rapidamente. Che cos'è la validazione e cos'è la gestione delle eccezioni? Ad esempio in un modulo di registrazione utente, abbiamo alcune regole per la password (deve contenere sia numeri che lettere). Se l'utente inserisce solo lettere, è un problema di convalida o un'eccezione. L'interfaccia utente dovrebbe convalidarlo o semplicemente passarlo al back-end e rilevare eventuali eccezioni?

Abbiamo raggiunto una conclusione comune che la convalida si riferisce a regole definite dal sistema e verificate rispetto ai dati forniti dall'utente. Una convalida non dovrebbe preoccuparsi di come funziona la logica di business o di come funziona il sistema per quella materia. Ad esempio, il nostro sistema operativo può prevedere, senza proteste, una password composta da lettere semplici. Tuttavia vogliamo imporre una combinazione di lettere e numeri. Questo è un caso di convalida, una regola che vogliamo imporre.

D'altra parte, le eccezioni sono casi in cui il nostro sistema può funzionare in modo imprevedibile, in modo errato o per nulla se alcuni dati specifici sono forniti in un formato sbagliato. Ad esempio, nell'esempio precedente, se il nome utente esiste già nel sistema, si tratta di un'eccezione. La nostra logica di business dovrebbe essere in grado di lanciare l'eccezione appropriata e l'interfaccia utente catturarla e gestirla in modo che l'utente possa vedere un bel messaggio.


Convalida nell'interfaccia utente

Ora che abbiamo chiarito quali sono i nostri obiettivi, vediamo alcuni esempi basati sulla stessa idea del modulo di registrazione utente.

Convalida in JavaScript

Per la maggior parte dei browser di oggi, JavaScript è una seconda natura. Non c'è quasi nessuna pagina web senza un certo grado di JavaScript. Una buona pratica consiste nel convalidare alcune cose basilari in JavaScript.

Diciamo che abbiamo un semplice modulo di registrazione utente in index.php, come descritto sotto.

    Registrazione Utente    

Registra un nuovo account

Nome utente:

Parola d'ordine:

Confermare:

Questo produrrà qualcosa di simile all'immagine qui sotto:


Ogni modulo deve convalidare che il testo inserito nei due campi password è uguale. Ovviamente questo è per garantire che l'utente non commetta errori durante la digitazione della propria password. Con JavaScript, fare la validazione è abbastanza semplice.

Per prima cosa dobbiamo aggiornare un po 'del nostro codice HTML.

 
Nome utente:

Parola d'ordine:

Confermare:

Abbiamo aggiunto nomi ai campi di immissione della password in modo che possiamo identificarli. Quindi abbiamo specificato che al momento dell'invio il modulo dovrebbe restituire il risultato di una funzione chiamata validatePasswords (). Questa funzione è il JavaScript che scriveremo. Script semplici come questo possono essere mantenuti nel file HTML, altri più sofisticati dovrebbero andare nei loro file JavaScript.

 

L'unica cosa che facciamo qui è confrontare i valori dei due campi di input chiamati "parola d'ordine" e "Confermare"Possiamo fare riferimento al modulo tramite il parametro che inviamo quando si chiama la funzione."Questo"nella forma onSubmit attributo, quindi il modulo stesso viene inviato alla funzione.

Quando i valori sono gli stessi, vero verrà restituito e verrà inviato il modulo, in caso contrario verrà visualizzato un messaggio di avviso che informa l'utente che le password non corrispondono.


Validazioni HTML5

Mentre possiamo usare JavaScript per convalidare la maggior parte dei nostri input, ci sono casi in cui vogliamo andare su un percorso più facile. È disponibile un certo grado di convalida dell'input in HTML5 e la maggior parte dei browser è felice di applicarli. L'utilizzo della convalida HTML5 è più semplice in alcuni casi, sebbene offra meno flessibilità.

  Registrazione Utente     

Registra un nuovo account

Nome utente:

Parola d'ordine:

Confermare:

Indirizzo email:

Sito web:

Per dimostrare diversi casi di convalida, abbiamo esteso un po 'la nostra forma. Abbiamo aggiunto anche un indirizzo email e un sito web. Le convalide HTML sono state impostate su tre campi.

  • L'input di testo nome utente è semplicemente richiesto. Verrà convalidato con qualsiasi stringa più lunga di zero caratteri.
  • Il campo dell'indirizzo email è di tipo "e-mail"e quando specificiamo il"necessario"attributo, i browser applicheranno una convalida al campo.
  • Infine, il campo del sito web è di tipo "url"Abbiamo anche specificato un"modello"attributo in cui puoi scrivere le tue espressioni regolari che convalidano i campi richiesti.

Per rendere l'utente consapevole dello stato dei campi, abbiamo anche usato un po 'di CSS per colorare i bordi degli input in rosso o verde, a seconda dello stato della convalida richiesta.


Il problema con le convalide HTML è che i diversi browser si comportano diversamente quando si tenta di inviare il modulo. Alcuni browser applicheranno semplicemente il CSS per informare gli utenti, altri impediranno l'invio del modulo del tutto. Ti consiglio di testare accuratamente le convalide HTML in diversi browser e, se necessario, fornire anche un fallback JavaScript per quei browser che non sono abbastanza intelligenti.


Convalida in modelli

Ormai molte persone conoscono la proposta di architettura pulita di Robert C. Martin, in cui il framework MVC è solo per presentazione e non per logica aziendale.


In sostanza, la logica aziendale dovrebbe risiedere in un luogo separato e ben isolato, organizzato per riflettere l'architettura dell'applicazione, mentre le viste ei controllori del framework dovrebbero controllare la consegna del contenuto all'utente ei modelli potrebbero essere eliminati del tutto o, se necessario , utilizzato solo per eseguire operazioni relative alla consegna. Una di queste operazioni è la convalida. La maggior parte dei framework ha ottime funzioni di validazione. Sarebbe un peccato non mettere i tuoi modelli al lavoro e fare un po 'di convalida lì.

Non installeremo diversi framework web MVC per dimostrare come convalidare i moduli precedenti, ma qui ci sono due soluzioni approssimative in Laravel e CakePHP.

Validazione in un modello Laravel

Laravel è progettato in modo da avere un maggiore accesso alla convalida nel Controller, dove si ha anche accesso diretto all'input dell'utente. Il tipo di validatore integrato preferisce essere utilizzato lì. Tuttavia ci sono suggerimenti su Internet che la convalida nei modelli è ancora una buona cosa da fare a Laravel. Un esempio completo e una soluzione di Jeffrey Way possono essere trovati sul suo repository Github.

Se preferisci scrivere la tua soluzione, potresti fare qualcosa di simile al modello qui sotto.

classe UserACL estende Eloquent private $ rules = array ('userName' => 'richiesto | alpha | min: 5', 'password' => 'richiesto | min: 6', 'confirm' => 'required | min: 6 ',' email '=>' required | email ',' website '=>' url '); errori $ privati; funzione pubblica convalida ($ data) $ validator = Validator :: make ($ data, $ this-> rules); se ($ validator-> fail ()) $ this-> errors = $ validator-> errori; restituisce falso;  return true;  errori di funzione pubblica () return $ this-> errors; 

Puoi usarlo dal tuo controller semplicemente creando il UserACL oggetto e chiamata si convalidano su di esso. Probabilmente avrai il "Registrare"metodo anche su questo modello e il Registrare delegherà solo i dati già convalidati alla tua logica aziendale.

Validazione in un modello CakePHP

CakePHP promuove la convalida anche nei modelli. Ha una vasta funzionalità di validazione a livello di modello. Ecco come appare una convalida per il nostro modulo in CakePHP.

class UserACL estende AppModel public $ validate = ['userName' => ['rule' => ['minLength', 5], 'required' => true, 'allowEmpty' => false, 'on' => 'crea ',' message '=>' Il nome utente deve contenere almeno 5 caratteri. ' ], 'password' => ['rule' => ['equalsTo', 'confirm'], 'message' => 'Le due password non corrispondono. Si prega di reinserirli. ' ]]; public function equalsTo ($ checkedField, $ otherField = null) $ value = $ this-> getFieldValue ($ checkedField); return $ value === $ this-> data [$ this-> name] [$ otherField];  funzione privata getFieldValue ($ fieldName) return array_values ​​($ otherField) [0]; 

Abbiamo solo esemplificato parzialmente le regole. È sufficiente evidenziare il potere di convalida nel modello. CakePHP è particolarmente bravo in questo. Ha un gran numero di funzioni di convalida incorporate come "minLength"nell'esempio e in vari modi per fornire feedback all'utente. Ancora di più, concetti come"necessario" o "allowEmpty"non sono in realtà regole di validazione, Cake guarderà questi quando genererà la tua vista e inserirà le convalide HTML anche sui campi contrassegnati con questi parametri.Tuttavia le regole sono grandiose e possono essere facilmente estese semplicemente creando metodi sulla classe del modello come abbiamo fatto per confrontare i due campi password.Infine, è sempre possibile specificare il messaggio che si desidera inviare alle viste in caso di errore di convalida.Ulteriori informazioni sulla convalida CakePHP nel ricettario.

La validazione in generale a livello di modello ha i suoi vantaggi. Ogni framework fornisce un facile accesso ai campi di input e crea il meccanismo per notificare l'utente in caso di errore di convalida. Non c'è bisogno di dichiarazioni try-catch o di altri passaggi sofisticati. La convalida sul lato server assicura anche che i dati vengano convalidati, indipendentemente da cosa. L'utente non può ingannare il nostro software come con HTML o JavaScript. Naturalmente, ogni convalida lato server viene fornita con il costo di un round-trip di rete e la potenza di calcolo dal lato del provider invece che dal lato del cliente.


Lanciare le eccezioni dalla logica aziendale

Il passaggio finale nel controllo dei dati prima di inviarli al sistema è al livello della nostra logica aziendale. Le informazioni che raggiungono questa parte del sistema dovrebbero essere sufficientemente igienizzate per essere utilizzabili. La logica aziendale dovrebbe controllare solo i casi che sono critici per questo. Ad esempio, aggiungere un utente già esistente è un caso in cui si genera un'eccezione. Il controllo della lunghezza dell'utente di almeno cinque caratteri non dovrebbe avvenire a questo livello. Possiamo tranquillamente presumere che tali limiti siano stati applicati a livelli più alti.

D'altra parte, il confronto tra le due password è una questione di discussione. Ad esempio, se solo crittografiamo e salviamo la password vicino all'utente in un database, potremmo rilasciare il controllo e assumere che i livelli precedenti abbiano verificato che le password siano uguali. Tuttavia, se creiamo un utente reale sul sistema operativo utilizzando un'API o uno strumento CLI che richiede effettivamente un nome utente, una password e una conferma della password, potremmo prendere anche la seconda voce e inviarla a uno strumento CLI. Lascia che si ri-convalidi se le password corrispondono ed essere pronto a lanciare un'eccezione se non lo fanno. In questo modo abbiamo modellato la nostra logica aziendale in modo che corrisponda al comportamento del sistema operativo reale.

Lanciare le eccezioni da PHP

Lanciare eccezioni da PHP è molto semplice. Creiamo la nostra classe di controllo degli accessi utente e dimostriamo come implementare una funzionalità di aggiunta degli utenti.

class UserControlTest estende PHPUnit_Framework_TestCase function testBehavior () $ this-> assertTrue (true); 

Mi piace sempre iniziare con qualcosa di semplice che mi fa andare avanti. Creare uno stupido test è un ottimo modo per farlo. Mi costringe anche a pensare a ciò che voglio attuare. Un test chiamato UserControlTest significa che ho pensato che avrò bisogno di un UserControl classe per implementare il mio metodo.

require_once __DIR__. '/ ... /UserControl.php'; classe UserControlTest estende PHPUnit_Framework_TestCase / ** * @expectedException Exception * @expectedExceptionMessage L'utente non può essere vuoto * / function testEmptyUsernameWillThrowException () $ userControl = new UserControl (); $ userControl-> add (");

Il prossimo test da scrivere è un caso degenerativo. Non testeremo per una lunghezza specifica dell'utente, ma vogliamo essere sicuri di non voler aggiungere un utente vuoto. A volte è facile perdere il contenuto di una variabile dalla vista al business, su tutti quei livelli della nostra applicazione. Ovviamente questo codice fallirà, perché non abbiamo ancora una classe.

Avviso PHP: require_once ([percorso lungo-qui] / Test / ... /UserControl.php): impossibile aprire lo stream: Nessun file o directory in [percorso lungo-qui] /Test/UserControlTest.php sulla riga 2

Creiamo la classe e eseguiamo i nostri test. Ora abbiamo un altro problema.

PHP Errore irreversibile: chiamata a un metodo non definito UserControl :: add ()

Ma possiamo aggiustarlo anche in un paio di secondi.

class UserControl funzione pubblica add ($ username) 

Ora possiamo avere un buon test fallito raccontandoci l'intera storia del nostro codice.

1) UserControlTest :: testEmptyUsernameWillThrowException Non riuscito affermando che l'eccezione del tipo "Exception" è stata generata.

Finalmente possiamo fare un po 'di codice reale.

funzione pubblica add ($ username) if (! $ username) lancia nuova Exception (); 

Ciò rende superata l'aspettativa per l'eccezione, ma senza specificare un messaggio il test fallirà ancora.

1) UserControlTest :: testEmptyUsernameWillThrowException Impossibile affermare che il messaggio di eccezione "contiene" L'utente non può essere vuoto ".

È ora di scrivere il messaggio dell'eccezione

funzione pubblica add ($ username) if (! $ username) lancia nuova eccezione ('L'utente non può essere vuoto!'); 

Ora, questo fa passare il test. Come è possibile osservare, PHPUnit verifica che il messaggio di eccezione previsto sia contenuto nell'eccezione effettivamente generata. Questo è utile perché ci consente di costruire dinamicamente i messaggi e di controllare solo la parte stabile. Un esempio comune è quando si genera un errore con un testo di base e alla fine si specifica il motivo per tale eccezione. I motivi vengono solitamente forniti da librerie o applicazioni di terze parti.

/ ** * @expectedException Exception * @expectedExceptionMessage Impossibile aggiungere l'utente George * / function testWillNotAddAnAlreadyExistingUser () $ command = \ Mockery :: mock ('SystemCommand'); $ command-> shouldReceive ('execute') -> once () -> con ('adduser George') -> andReturn (false); $ command-> shouldReceive ('getFailureMessage') -> once () -> andReturn ('Utente esiste già sul sistema.'); $ userControl = new UserControl ($ command); $ UserControl-> add ( 'George'); 

Gli errori di lancio su utenti duplicati ci permetteranno di esplorare questo messaggio costruendo un ulteriore passo avanti. Il test precedente crea una simulazione che simulerà un comando di sistema, fallirà e, su richiesta, restituirà un bel messaggio di errore. Inseriremo questo comando al UserControl classe per uso interno.

class UserControl private $ systemCommand; funzione pubblica __construct (SystemCommand $ systemCommand = null) $ this-> systemCommand = $ systemCommand? : nuovo SystemCommand ();  funzione pubblica add ($ username) if (! $ username) lancia nuova eccezione ('L'utente non può essere vuoto!');  classCommand di sistema 

Iniezione di a SystemCommand l'istanza è stata abbastanza facile. Abbiamo anche creato un SystemCommand classe all'interno del nostro test solo per evitare problemi di sintassi. Non lo implementeremo. Il suo ambito supera l'argomento di questo tutorial. Tuttavia, abbiamo un altro messaggio di errore di test.

1) UserControlTest :: testWillNotAddAnAlreadyExistingUser Fallito affermando che l'eccezione del tipo "Exception" è stata lanciata.

Sì. Non stiamo gettando eccezioni. Manca la logica per chiamare il comando di sistema e provare ad aggiungere l'utente.

funzione pubblica add ($ username) if (! $ username) lancia nuova eccezione ('L'utente non può essere vuoto!');  if (! $ this-> systemCommand-> execute (sprintf ('adduser% s', $ username))) throw new Exception (sprintf ('Impossibile aggiungere l'utente% s. Motivo:% s', $ username, $ this-> systemCommand-> getFailureMessage ())); 

Ora, quelle modifiche al Inserisci() il metodo può fare il trucco. Cerchiamo di eseguire il nostro comando sul sistema, non importa cosa, e se il sistema dice che non è possibile aggiungere l'utente per qualsiasi motivo lanciamo un'eccezione. Il messaggio di questa eccezione sarà in parte codificato, con il nome dell'utente collegato e quindi il motivo dal comando di sistema concatenato alla fine. Come puoi vedere, questo codice fa passare il test.

Eccezioni personalizzate

Lanciare eccezioni con diversi messaggi è sufficiente nella maggior parte dei casi. Tuttavia, quando si dispone di un sistema più complesso, è necessario rilevare anche queste eccezioni e intraprendere diverse azioni basate su di esse. Analizzare il messaggio di un'eccezione e agire solo su questo può portare a problemi fastidiosi. Innanzitutto, le stringhe fanno parte dell'interfaccia utente, la presentazione e hanno una natura volatile. Basare la logica su stringhe in continua evoluzione porterà ad un incubo nella gestione delle dipendenze. In secondo luogo, chiamando a getMessage () metodo sull'eccezione catturata ogni volta è anche uno strano modo per decidere cosa fare dopo.

Con tutti questi in mente, creare le nostre eccezioni è il prossimo passo logico da compiere.

/ ** * @expectedException ExceptionCannotAddUser * @expectedExceptionMessage Impossibile aggiungere l'utente George * / function testWillNotAddAnAlreadyExistingUser () $ command = \ Mockery :: mock ('SystemCommand'); $ command-> shouldReceive ('execute') -> once () -> con ('adduser George') -> andReturn (false); $ command-> shouldReceive ('getFailureMessage') -> once () -> andReturn ('Utente esiste già sul sistema.'); $ userControl = new UserControl ($ command); $ UserControl-> add ( 'George'); 

Abbiamo modificato il nostro test per aspettarci la nostra eccezione personalizzata, ExceptionCannotAddUser. Il resto del test è invariato.

class ExceptionCannotAddUser estende Exception public function __construct ($ userName, $ reason) $ message = sprintf ('Impossibile aggiungere l'utente% s. Motivo:% s', $ userName, $ reason); genitore :: __ costrutto ($ message, 13, null); 

La classe che implementa la nostra eccezione personalizzata è come qualsiasi altra classe, ma deve estendersi Eccezione. L'utilizzo di eccezioni personalizzate ci fornisce anche il posto ideale per eseguire tutte le manipolazioni relative alle stringhe relative alla presentazione. Spostando qui la concatenazione, abbiamo anche eliminato la presentazione dalla logica aziendale e rispettato il principio della responsabilità unica.

funzione pubblica add ($ username) if (! $ username) lancia nuova eccezione ('L'utente non può essere vuoto!');  if (! $ this-> systemCommand-> execute (sprintf ('adduser% s', $ username))) throw new ExceptionCannotAddUser ($ username, $ this-> systemCommand-> getFailureMessage ()); 

Lanciare la nostra eccezione è solo questione di cambiare il vecchio "gettare"Comanda al nuovo e inviando due parametri invece di comporre il messaggio qui. Ovviamente tutti i test stanno passando.

PHPUnit 3.7.28 di Sebastian Bergmann ... Tempo: 18 ms, Memoria: 3.00Mb OK (2 test, 4 asserzioni) Fatto.

Cattura le eccezioni nel tuo MVC

Le eccezioni devono essere prese ad un certo punto, a meno che tu non voglia che il tuo utente le veda così come sono. Se si utilizza un framework MVC, probabilmente si desidera rilevare eccezioni nel controller o nel modello. Dopo che l'eccezione è stata rilevata, viene trasformata in un messaggio per l'utente e visualizzata nella vista. Un modo comune per raggiungere questo obiettivo è creare un "tryAction ($ action)"metodo nel controller di base o nel modello base della tua applicazione e chiamalo sempre con l'azione corrente. In quel metodo puoi fare la logica catching e la generazione del messaggio per adattarsi al tuo framework.

Se non si utilizza un framework Web o un'interfaccia Web, lo strato di presentazione deve occuparsi di catturare e trasformare queste eccezioni.

Se sviluppi una biblioteca, la cattura delle eccezioni sarà responsabilità dei tuoi clienti.


Pensieri finali

Questo è tutto. Abbiamo attraversato tutti gli strati della nostra applicazione. Abbiamo convalidato in JavaScript, HTML e nei nostri modelli. Abbiamo gettato e preso eccezioni dalla nostra logica di business e persino creato le nostre eccezioni personalizzate. Questo approccio alla validazione e alla gestione delle eccezioni può essere applicato da piccoli a grandi progetti senza gravi problemi. Tuttavia, se la logica di convalida diventa molto complessa e le diverse parti del progetto utilizzano parti di logica sovrapposte, è possibile prendere in considerazione l'estrazione di tutte le convalide che possono essere eseguite a un livello specifico per un servizio di convalida o un provider di convalida. Questi livelli possono includere, ma non devono essere limitati al validatore JavaScript, al validatore PHP backend, al validatore di comunicazione di terze parti e così via.

Grazie per aver letto. Buona giornata.