Validatie- en uitzonderingsafhandeling van de gebruikersinterface naar de backend

Vroeg of laat in je programmeercarrière krijg je te maken met het dilemma van validatie en afhandeling van uitzonderingen. Dit was ook het geval met mij en mijn team. Een paar jaar geleden bereikten we een punt waarop we architecturale acties moesten ondernemen om tegemoet te komen aan alle uitzonderlijke gevallen die ons vrij grote softwareproject moest verwerken. Hieronder vindt u een lijst met praktijken die we waardeerden en toepassen als het gaat om validatie en afhandeling van uitzonderingen.


Validatie versus uitzonderingsafhandeling

Toen we ons probleem begonnen te bespreken, dook een ding heel snel op. Wat is validatie en wat is afhandeling van uitzonderingen? In een gebruikersregistratieformulier hebben we bijvoorbeeld enkele regels voor het wachtwoord (het moet zowel cijfers als letters bevatten). Als de gebruiker alleen letters invoert, is dat een validatieprobleem of een uitzondering. Moet de gebruikersinterface dat valideren of gewoon doorgeven aan de back-end en eventuele uitzonderingen opvangen die mijn worden gegooid?

We kwamen tot een algemene conclusie dat validatie verwijst naar regels die door het systeem zijn gedefinieerd en die zijn geverifieerd tegen door de gebruiker verstrekte gegevens. Een validatie mag niet gaan om hoe de bedrijfslogica werkt, of hoe het systeem wat dat betreft werkt. Ons besturingssysteem kan bijvoorbeeld zonder enige protesten een wachtwoord verwachten dat bestaat uit gewone letters. We willen echter een combinatie van letters en cijfers afdwingen. Dit is een geval van validatie, een regel die we willen opleggen.

Aan de andere kant zijn uitzonderingen gevallen waarin ons systeem op een onvoorspelbare manier kan functioneren, verkeerd of helemaal niet als bepaalde specifieke gegevens in een verkeerd formaat worden verstrekt. Als in het bovenstaande voorbeeld de gebruikersnaam al op het systeem bestaat, is dit een uitzondering. Onze bedrijfslogica moet in staat zijn om de juiste uitzondering te maken en de gebruikersinterface te vangen en af ​​te handelen zodat de gebruiker een leuk bericht ziet.


Valideren in de gebruikersinterface

Nu we duidelijk hebben gemaakt wat onze doelen zijn, laten we enkele voorbeelden bekijken op basis van hetzelfde idee voor het gebruikersregistratieformulier.

Valideren in JavaScript

Voor de meeste van de huidige browsers is JavaScript een tweede natuur. Er is bijna geen webpagina zonder JavaScript. Een goede oefening is om een ​​aantal basisdingen in JavaScript te valideren.

Laten we zeggen dat we een eenvoudig gebruikersregistratieformulier hebben in index.php, zoals hieronder beschreven.

    Gebruikersregistratie    

Registreer Nieuw Account

Gebruikersnaam:

Wachtwoord:

Bevestigen:

Dit zal iets opleveren dat lijkt op de afbeelding hieronder:


Elk formulier moet valideren dat de tekst die is ingevoerd in de twee wachtwoordvelden gelijk is. Vanzelfsprekend is dit om ervoor te zorgen dat de gebruiker geen fout maakt bij het invoeren van zijn of haar wachtwoord. Met JavaScript is de validatie vrij eenvoudig.

Eerst moeten we een beetje van onze HTML-code bijwerken.

 
Gebruikersnaam:

Wachtwoord:

Bevestigen:

We hebben namen toegevoegd aan de wachtwoordinvoervelden zodat we ze kunnen identificeren. Vervolgens hebben we opgegeven dat bij verzenden het formulier het resultaat van een geroepen functie moet retourneren validatePasswords (). Deze functie is het JavaScript dat we zullen schrijven. Eenvoudige scripts zoals deze kunnen in het HTML-bestand worden bewaard. Andere, meer geavanceerde scripts moeten in hun eigen JavaScript-bestanden worden geplaatst.

 

Het enige dat we hier doen is de waarden van de twee invoervelden met de naam "wachtwoord"en"bevestigen"We kunnen naar het formulier verwijzen door de parameter die we verzenden wanneer we de functie aanroepen."deze"in de vorm onsubmit attribuut, dus het formulier zelf wordt naar de functie verzonden.

Wanneer de waarden hetzelfde zijn, waar wordt teruggestuurd en het formulier wordt verzonden, anders wordt een waarschuwingsbericht weergegeven met de melding dat de wachtwoorden niet overeenkomen.


HTML5-validaties

Hoewel we JavaScript kunnen gebruiken om de meeste van onze invoer te valideren, zijn er gevallen waarin we een eenvoudiger pad willen volgen. Enige mate van invoervalidatie is beschikbaar in HTML5 en de meeste browsers passen ze graag toe. Het gebruik van HTML5-validatie is in sommige gevallen eenvoudiger, maar biedt minder flexibiliteit.

  Gebruikersregistratie     

Registreer Nieuw Account

Gebruikersnaam:

Wachtwoord:

Bevestigen:

E-mailadres:

Website:

Om verschillende validatiezaken te demonstreren, hebben we ons formulier een beetje uitgebreid. We hebben ook een e-mailadres en een website toegevoegd. HTML-validaties zijn ingesteld op drie velden.

  • De tekstinvoer gebruikersnaam is eenvoudigweg vereist. Het wordt gevalideerd met een tekenreeks die langer is dan nul tekens.
  • Het veld voor het e-mailadres is van het type "e-mail"en wanneer we de"verplicht"attribuut, browsers zullen een validatie op het veld toepassen.
  • Ten slotte is het websiteveld van het type "url". We hebben ook een"patroon"attribuut waar u uw reguliere expressies kunt schrijven die de vereiste velden valideren.

Om de gebruiker bewust te maken van de status van de velden, hebben we ook een klein beetje CSS gebruikt om de randen van de invoer in rood of groen te kleuren, afhankelijk van de status van de vereiste validatie.


Het probleem met HTML-validaties is dat verschillende browsers zich anders gedragen wanneer u het formulier probeert in te dienen. Sommige browsers passen de CSS alleen toe om de gebruikers te informeren, anderen zullen voorkomen dat het formulier volledig wordt ingediend. Ik raad u aan om uw HTML-validaties grondig te testen in verschillende browsers en indien nodig ook een JavaScript-fallback te bieden voor browsers die niet slim genoeg zijn.


Valideren in modellen

Inmiddels weten veel mensen over het voorstel voor schone architectuur van Robert C. Martin, waarin het MVC-raamwerk alleen bedoeld is voor presentatie en niet voor bedrijfslogica.


In wezen moet uw bedrijfslogica zich bevinden op een afzonderlijke, goed geïsoleerde plaats, georganiseerd om de architectuur van uw toepassing weer te geven, terwijl de weergaven en controllers van het framework de levering van de inhoud aan de gebruiker moeten besturen en modellen volledig kunnen worden verwijderd of, indien nodig , alleen gebruikt om met de levering verband houdende bewerkingen uit te voeren. Een dergelijke bewerking is validatie. De meeste frameworks hebben geweldige validatiefuncties. Het zou zonde zijn om uw modellen niet aan het werk te zetten en daar een kleine validatie uit te voeren.

We zullen geen verschillende MVC-webraamwerken installeren om te demonstreren hoe onze vorige formulieren kunnen worden gevalideerd, maar hier zijn twee benaderende oplossingen in Laravel en CakePHP.

Validatie in een Laravel-model

Laravel is zo ontworpen dat je meer toegang hebt tot validatie in de Controller, waar je ook directe toegang hebt tot de invoer van de gebruiker. De ingebouwde validator wordt hier het liefst gebruikt. Er zijn echter op het internet suggesties dat valideren in modellen nog steeds een goede zaak is om te doen in Laravel. Een compleet voorbeeld en een oplossing van Jeffrey Way zijn te vinden op zijn Github-repository.

Als u liever uw eigen oplossing schrijft, kunt u iets doen dat lijkt op het onderstaande model.

class UserACL breidt Eloquent uit private $ rules = array ('userName' => 'required | alpha | min: 5', 'password' => 'required | min: 6', 'confirm' => 'required | min: 6 ',' email '=>' verplicht | email ',' website '=>' url '); privé $ fouten; public function validate ($ data) $ validator = Validator :: make ($ data, $ this-> rules); if ($ validator-> failed ()) $ this-> errors = $ validator-> errors; return false;  return true;  public function errors () return $ this-> errors; 

U kunt dit vanaf uw controller gebruiken door simpelweg de. Te maken UserACL object en oproep valideren erop. Je hebt waarschijnlijk de "registreren"methode ook op dit model, en de registreren zal de reeds gevalideerde gegevens gewoon delegeren naar uw bedrijfslogica.

Validatie in een CakePHP-model

CakePHP promoot ook validatie in modellen. Het heeft uitgebreide validatiefunctionaliteit op modelniveau. Hier gaat het over hoe een validatie voor onze vorm eruit zou zien in CakePHP.

class UserACL breidt AppModel uit public $ validate = ['userName' => ['rule' => ['minLength', 5], 'required' => true, 'allowEmpty' => false, 'on' => 'create ',' message '=>' Gebruikersnaam moet minimaal 5 tekens lang zijn. ' ], 'wachtwoord' => ['rule' => ['equalsTo', 'confirm'], 'message' => 'De twee wachtwoorden komen niet overeen. Voer ze alstublieft opnieuw in. ' ]]; public function equalsTo ($ checkedField, $ otherField = null) $ value = $ this-> getFieldValue ($ checkedField); return $ value === $ this-> data [$ this-> name] [$ otherField];  private functie getFieldValue ($ fieldName) return array_values ​​($ otherField) [0]; 

We hebben de regels slechts gedeeltelijk geïllustreerd. Het volstaat om de kracht van validatie in het model te benadrukken. CakePHP is hier bijzonder goed in. Het heeft een groot aantal ingebouwde validatiefuncties zoals "minimale lengte"in het voorbeeld en op verschillende manieren om feedback te geven aan de gebruiker. Nog meer, concepten als"verplicht"of"allowEmpty"zijn eigenlijk geen validatieregels, Cake zal hiernaar kijken bij het genereren van uw weergave en HTML-validaties ook plaatsen in velden die met deze parameters zijn gemarkeerd.De regels zijn echter geweldig en kunnen eenvoudig worden uitgebreid door eenvoudig methoden in de modelklasse te maken zoals wij deden. vergelijk de twee wachtwoordvelden.Tenslotte kunt u altijd het bericht opgeven dat u naar de views wilt verzenden in geval van een validatiefout Meer over CakePHP-validatie in het kookboek.

Validatie in het algemeen op modelniveau heeft zijn voordelen. Elk framework biedt gemakkelijke toegang tot de invoervelden en creëert het mechanisme om de gebruiker op de hoogte te stellen in geval van validatiefouten. Geen behoefte aan try-catch statements of andere verfijnde stappen. Validatie aan de serverkant zorgt er ook voor dat de gegevens gevalideerd worden, wat er ook gebeurt. De gebruiker kan onze software niet meer misleiden zoals met HTML of JavaScript. Uiteraard wordt elke validering aan de serverzijde geleverd met de kosten van een netwerkomloop en rekenkracht aan de kant van de provider in plaats van aan de kant van de klant.


Uitzonderingen op de bedrijfslogica genereren

De laatste stap bij het controleren van gegevens voordat deze aan het systeem wordt toegewezen, ligt op het niveau van onze bedrijfslogica. Informatie die dit deel van het systeem bereikt, moet voldoende ontsmet zijn om bruikbaar te zijn. De bedrijfslogica moet alleen controleren op zaken die daarvoor van cruciaal belang zijn. Het toevoegen van een bestaande gebruiker is bijvoorbeeld een geval wanneer we een uitzondering genereren. De lengte van de gebruiker controleren op ten minste vijf tekens moet op dit niveau niet gebeuren. We kunnen veilig aannemen dat dergelijke beperkingen op hogere niveaus werden afgedwongen.

Aan de andere kant is het vergelijken van de twee wachtwoorden een kwestie van discussie. Als we het wachtwoord bijvoorbeeld dicht bij de gebruiker in een database coderen en opslaan, kunnen we de controle laten vallen en ervan uitgaan dat eerdere lagen ervoor hebben gezorgd dat de wachtwoorden gelijk zijn. Als we echter een echte gebruiker in het besturingssysteem maken met behulp van een API of een CLI-tool waarvoor eigenlijk een gebruikersnaam, wachtwoord en wachtwoord moet worden bevestigd, willen we misschien ook de tweede vermelding maken en deze naar een CLI-tool sturen. Laat het opnieuw valideren als de wachtwoorden overeenkomen en klaar zijn om een ​​uitzondering te gooien als ze dat niet doen. Op deze manier hebben we onze bedrijfslogica gemodelleerd om overeen te komen met hoe het echte besturingssysteem zich gedraagt.

Uitzonderingen van PHP gooien

Het gooien van uitzonderingen op PHP is heel eenvoudig. Laten we onze toegangscontrolecategorie voor gebruikers maken en demonstreren hoe we een functionaliteit voor gebruikersaanvulling kunnen implementeren.

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

Ik vind het altijd leuk om met iets eenvoudigs te beginnen dat me op gang brengt. Het maken van een stomme test is een geweldige manier om dit te doen. Het dwingt me ook om na te denken over wat ik wil implementeren. Een test met de naam UserControlTest betekent dat ik dacht dat ik een a nodig heb UserControl klasse om mijn methode te implementeren.

require_once __DIR__. '/ ... /UserControl.php'; class UserControlTest breidt PHPUnit_Framework_TestCase uit / ** * @expectedException Exception * @expectedExceptionMessage Gebruiker kan niet leeg zijn * / function testEmptyUsernameWillThrowException () $ userControl = new UserControl (); $ userControl-> add (");

De volgende test om te schrijven is een degeneratief geval. We testen niet voor een specifieke gebruikerslengte, maar we willen er zeker van zijn dat we geen lege gebruiker willen toevoegen. Het is soms gemakkelijk om de inhoud van een variabele te verliezen van weergave tot bedrijf, over al die lagen van onze applicatie. Deze code zal uiteraard falen, omdat we nog geen klas hebben.

PHP Waarschuwing: require_once ([long-path-here] / Test / ... /UserControl.php): kan de stream niet openen: geen bestand of map in [long-path-here] /Test/UserControlTest.php on line 2

Laten we de klas maken en onze tests uitvoeren. Nu hebben we nog een probleem.

PHP Fatale fout: aanroep op ongedefinieerde methode UserControl :: add ()

Maar we kunnen dat ook in slechts een paar seconden oplossen.

class UserControl public function add ($ gebruikersnaam) 

Nu kunnen we een mooie testfout hebben en ons het hele verhaal van onze code vertellen.

1) UserControlTest :: testEmptyUsernameWillThrowException Mislukt dat de uitzondering van het type "Uitzondering" wordt gegenereerd.

Eindelijk kunnen we wat echte codering doen.

public function add ($ username) if (! $ username) throw new Exception (); 

Dat betekent dat de verwachting voor de uitzondering verstrijkt, maar zonder een bericht op te geven dat de test nog steeds mislukt.

1) UserControlTest :: testEmptyUsernameWillThrowException Mislukt dat uitzonderingsboodschap "bevat" Gebruiker kan niet leeg zijn ".

Tijd om het bericht van de uitzondering te schrijven

public function add ($ username) if (! $ username) throw new Exception ('Gebruiker mag niet leeg zijn!'); 

Dat maakt onze test geslaagd. Zoals u kunt zien, controleert PHPUnit of het verwachte uitzonderingsbericht is opgenomen in de werkelijk gegenereerde uitzondering. Dit is handig omdat het ons in staat stelt om dynamisch berichten te construeren en alleen te controleren op het stabiele deel. Een bekend voorbeeld is wanneer u een fout gooit met een basistekst en aan het eind de reden voor die uitzondering opgeeft. Redenen worden doorgaans verstrekt door bibliotheken of applicaties van derde partijen.

/ ** * @expectedException Exception * @expectedExceptionMessage Kan gebruiker George * / function testWillNotAddAnAlreadyExistingUser () $ command = \ Mockery :: mock ('SystemCommand') niet toevoegen; $ command-> shouldReceive ('execute') -> once () -> with ('adduser George') -> andReturn (false); $ command-> shouldReceive ('getFailureMessage') -> once () -> andReturn ('Gebruiker bestaat al op het systeem.'); $ userControl = nieuwe UserControl ($ opdracht); $ UserControl-> toe te voegen ( 'George'); 

Door fouten te gooien op dubbele gebruikers kunnen we deze berichtconstructie nog een stap verder verkennen. De bovenstaande test creëert een mock die een systeemcommando zal simuleren, het zal mislukken en op verzoek zal het een mooi foutbericht retourneren. We zullen dit commando injecteren naar de UserControl klasse voor intern gebruik.

class UserControl private $ systemCommand; openbare functie __construct (SystemCommand $ systemCommand = null) $ this-> systemCommand = $ systemCommand? : new SystemCommand ();  public function add ($ username) if (! $ username) throw new Exception ('Gebruiker kan niet leeg zijn!');  class SystemCommand 

Injecteren van de a SystemCommand instantie was vrij eenvoudig. We hebben ook een gemaakt SystemCommand klasse in onze test om syntaxisproblemen te vermijden. We zullen het niet implementeren. De reikwijdte overschrijdt het onderwerp van deze tutorial. We hebben echter nog een testfoutbericht.

1) UserControlTest :: testWillNotAddAnAlreadyreadyistingUser Kan niet beweren dat uitzondering van het type "Uitzondering" wordt gegenereerd.

Yep. We geven geen uitzonderingen. De logica om de systeemopdracht aan te roepen en de gebruiker toe te voegen ontbreekt.

public function add ($ username) if (! $ username) throw new Exception ('Gebruiker mag niet leeg zijn!');  if (! $ this-> systemCommand-> execute (sprintf ('adduser% s', $ username))) throw new Exception (sprintf ('Kan gebruiker% s niet toevoegen. Reden:% s', $ gebruikersnaam, $ this-> systemCommand-> getFailureMessage ())); 

Nu, die aanpassingen aan de toevoegen() methode kan het lukken. We proberen onze opdracht op het systeem uit te voeren, wat er ook gebeurt, en als het systeem zegt dat het de gebruiker om welke reden dan ook niet kan toevoegen, gooien we een uitzondering. Het bericht van deze uitzondering zal voor een deel hardgecodeerd zijn, met de naam van de gebruiker bijgevoegd en vervolgens de reden van de systeembesturing aan het einde aaneengesloten. Zoals u kunt zien, maakt deze code onze test door.

Aangepaste uitzonderingen

Het gooien van uitzonderingen met verschillende berichten is in de meeste gevallen genoeg. Als je echter een complexer systeem hebt, moet je ook deze uitzonderingen vangen en op basis daarvan verschillende acties ondernemen. Het analyseren van het bericht van een uitzondering en alleen daarop actie ondernemen kan leiden tot vervelende problemen. Ten eerste maken strings deel uit van de gebruikersinterface, presentatie, en ze hebben een vluchtige aard. Het baseren van logica op steeds veranderende strings zal leiden tot nachtmerrie op het gebied van afhankelijkheid. Ten tweede, het bellen van een GetMessage () methode op de gevangen uitzondering elke keer is ook een vreemde manier om te beslissen wat te doen.

Met dit alles in gedachten is het creëren van onze eigen uitzonderingen de volgende logische stap om te nemen.

/ ** * @expectedException ExceptionCannotAddUser * @expectedExceptionMessage Kan gebruiker George * / function testWillNotAddAnAlreadyExistingUser () $ command = \ Mockery :: mock ('SystemCommand') niet toevoegen; $ command-> shouldReceive ('execute') -> once () -> with ('adduser George') -> andReturn (false); $ command-> shouldReceive ('getFailureMessage') -> once () -> andReturn ('Gebruiker bestaat al op het systeem.'); $ userControl = nieuwe UserControl ($ opdracht); $ UserControl-> toe te voegen ( 'George'); 

We hebben onze test aangepast om onze eigen aangepaste uitzondering te verwachten, ExceptionCannotAddUser. De rest van de test is ongewijzigd.

class ExceptionCannotAddUser breidt Exception uit public function __construct ($ userName, $ reason) $ message = sprintf ('Can not add user% s. Reason:% s', $ userName, $ reason); parent :: __ construct ($ message, 13, null); 

De klasse die onze aangepaste uitzondering implementeert, lijkt op elke andere klasse, maar moet worden uitgebreid Uitzondering. Het gebruik van aangepaste uitzonderingen biedt ons ook een geweldige plek om alle presentatie-gerelateerde stringmanipulatie uit te voeren. Door de aaneenschakeling hier te verplaatsen, hebben we ook de presentatie van de bedrijfslogica geëlimineerd en het beginsel van één enkele verantwoordelijkheid gerespecteerd.

public function add ($ username) if (! $ username) throw new Exception ('Gebruiker mag niet leeg zijn!');  if (! $ this-> systemCommand-> execute (sprintf ('adduser% s', $ username))) throw new ExceptionCannotAddUser ($ gebruikersnaam, $ this-> systemCommand-> getFailureMessage ()); 

Het gooien van onze eigen uitzondering is gewoon een kwestie van het oude veranderen "gooien"geef opdracht aan nieuwe en verzenden in twee parameters in plaats van het opstellen van het bericht hier. Natuurlijk zijn alle tests voorbijgaand.

PHPUnit 3.7.28 door Sebastian Bergmann ... Tijd: 18 ms, Geheugen: 3.00Mb OK (2 tests, 4 waarderingen) Klaar.

Uitzonderingen vangen in uw MVC

Uitzonderingen moeten op een bepaald moment worden vastgelegd, tenzij u wilt dat uw gebruiker ze ziet zoals ze zijn. Als u een MVC-framework gebruikt, wilt u waarschijnlijk uitzonderingen in de controller of het model opnemen. Nadat de uitzondering is vastgelegd, wordt deze omgezet in een bericht naar de gebruiker en weergegeven in uw weergave. Een veelgebruikte manier om dit te bereiken, is door een "tryAction ($ actie)"methode in de basiscontroller of het model van uw toepassing en noem dit altijd met de huidige actie. In die methode kunt u de blokkeringslogica en een mooie berichtgeneratie gebruiken die past bij uw kader.

Als u geen webraamwerk of een webinterface gebruikt, moet uw presentatielaag zorgen voor het vangen en transformeren van deze uitzonderingen.

Als u een bibliotheek ontwikkelt, vallen uw uitzonderingen onder de verantwoordelijkheid van uw klanten.


Laatste gedachten

Dat is het. We doorkruisten alle lagen van onze applicatie. We zijn gevalideerd in JavaScript, HTML en in onze modellen. We hebben uitzonderingen op onze bedrijfslogica gegooid en deze zelfs gemaakt met onze eigen uitzonderingen. Deze benadering van validatie en afhandeling van uitzonderingen kan zonder ernstige problemen van kleine tot grote projecten worden toegepast. Als uw validatielogica echter erg complex wordt en verschillende delen van uw project gebruikmaken van overlappende delen van de logica, kunt u overwegen alle validaties uit te voeren die op een bepaald niveau kunnen worden uitgevoerd naar een validatieservice of validatieprovider. Deze niveaus kunnen omvatten, maar zijn niet beperkt tot, JavaScript-validator, back-end PHP-validator, externe validator voor externe communicatie enzovoort.

Bedankt voor het lezen. Fijne dag.