Validering och undantagshantering Från användargränssnittet till baksidan

Förr eller senare i din programplanering kommer du att stå inför dilemmaet för validering och undantagshantering. Detta var fallet med mig och mitt lag också. För ett par år sedan nådde vi en punkt när vi var tvungna att ta arkitektoniska åtgärder för att rymma alla exceptionella fall som vårt ganska stora mjukvaruprojekt skulle behöva hantera. Nedan följer en lista över praxis som vi kom för att värdera och tillämpa när det gäller validering och undantagshantering.


Validering mot exceptionell hantering

När vi började diskutera vårt problem uppstod en sak mycket snabbt. Vad är validering och vad är undantagshantering? Till exempel i en användarregistreringsformulär har vi några regler för lösenordet (det måste innehålla både siffror och bokstäver). Om användaren skriver in endast bokstäver, är det en valideringsproblem eller ett undantag. Skulle användargränssnittet validera det, eller bara skicka det till backend och fånga några undantag som jag kastas?

Vi kom fram till en gemensam slutsats att validering hänvisar till regler som definieras av systemet och verifieras mot data som tillhandahålls av användaren. En validering bör inte bry sig om hur affärslogiken fungerar, eller hur systemet för den delen fungerar. Till exempel kan vårt operativsystem förvänta sig, utan några protester, ett lösenord som består av vanliga bokstäver. Vi vill dock upprätthålla en kombination av bokstäver och siffror. Det här är ett fall för validering, en regel som vi vill införa.

Å andra sidan är undantag fall då vårt system kan fungera på ett oförutsedd sätt, felaktigt eller inte alls om någon specifik data tillhandahålls i fel format. Till exempel, i det ovanstående exemplet, om användarnamnet redan finns på systemet, är det ett fall av ett undantag. Vår affärslogik ska kunna klara det lämpliga undantaget och användargränssnittet fångar och hanterar det så att användaren får ett bra meddelande.


Validerar i användargränssnittet

Nu när vi klargjorde vad våra mål är, låt oss se några exempel utifrån samma användarregistreringsformidé.

Validerar i JavaScript

Till de flesta av dagens webbläsare är JavaScript den andra naturen. Det finns nästan ingen webbsida utan någon grad av JavaScript i den. En bra övning är att validera några grundläggande saker i JavaScript.

Låt oss säga att vi har ett enkelt användarregistreringsformulär i index.php, som beskrivet nedan.

    Användarregistrering    

Registrera nytt konto

Användarnamn:

Lösenord:

Bekräfta:

Detta kommer att mata ut något som liknar bilden nedan:


Varje sådan blankett bör validera att texten som anges i de två lösenordsfälten är lika. Självfallet är detta för att säkerställa att användaren inte gör ett misstag när han skriver in sitt lösenord. Med JavaScript är det enkelt att göra valideringen.

Först måste vi uppdatera en liten bit av vår HTML-kod.

 
Användarnamn:

Lösenord:

Bekräfta:

Vi lade till namn till lösenordsinmatningsfälten så att vi kan identifiera dem. Då angav vi att vid inlämning ska formuläret returnera resultatet av en funktion som heter validatePasswords (). Den här funktionen är JavaScript som vi ska skriva. Enkla skript som detta kan hållas i HTML-filen, andra, mer sofistikerade bör gå i sina egna JavaScript-filer.

 

Det enda vi gör här är att jämföra värdena för de två inmatningsfälten med namnet "Lösenord"och"bekräfta". Vi kan referera till formuläret med parametern vi skickar in när vi ringer funktionen. Vi använde"detta"i formulärets onsubmit attribut, så själva formuläret skickas till funktionen.

När värdena är desamma, Sann kommer att returneras och formuläret kommer att skickas, annars kommer ett varningsmeddelande att visas för att berätta för användaren att lösenorden inte matchar.


HTML5-valideringar

Medan vi kan använda JavaScript för att validera de flesta av våra ingångar, finns det fall där vi vill gå på en enklare väg. En viss grad av inmatningsvalidering finns i HTML5, och de flesta webbläsare gärna tillämpar dem. Att använda HTML5-validering är enklare i vissa fall, men det ger mindre flexibilitet.

  Användarregistrering     

Registrera nytt konto

Användarnamn:

Lösenord:

Bekräfta:

E-postadress:

Hemsida:

För att visa flera valideringsfall utvidgade vi vår formulär en liten bit. Vi lade till en e-postadress och en webbplats också. HTML-valideringar ställdes in på tre fält.

  • Textinmatningen Användarnamn behövs bara. Den kommer att validera med en sträng som är längre än noll tecken.
  • E-postadressfältet är av typen "e-post"och när vi anger"nödvändig"attributet, kommer webbläsare att tillämpa en validering till fältet.
  • Slutligen är webbfältet av typen "url". Vi angav också en"mönster"attribut där du kan skriva dina reguljära uttryck som validerar de obligatoriska fälten.

För att göra användaren medveten om tillståndet i fälten använde vi också en liten bit av CSS för att färga gränserna för ingångarna i rött eller grönt, beroende på tillståndet för den önskade valideringen.


Problemet med HTML-validering är att olika webbläsare beter sig olika när du försöker skicka in formuläret. Vissa webbläsare kommer bara att tillämpa CSS för att informera användarna, andra kommer att förhindra inlämning av formuläret helt och hållet. Jag rekommenderar dig att testa dina HTML-valideringar noggrant i olika webbläsare och, om det behövs, också ge en JavaScript-återgång för de webbläsare som inte är tillräckligt smarta.


Validerande i modeller

Hittills vet många om Robert C. Martins rent arkitekturförslag, där MVC-ramverket endast är för presentation och inte för affärslogik.


I huvudsak bör din affärslogik ligga på en separat, väl isolerad plats som är organiserad för att återspegla arkitekturen i din ansökan, medan ramverkets synpunkter och kontroller ska styra leveransen av innehållet till användaren och modellerna kan helt och hållet släppas eller, om det behövs , som endast används för att utföra leveransrelaterade transaktioner. En sådan operation är validering. De flesta ramverk har bra valideringsfunktioner. Det skulle vara synd att inte sätta dina modeller på jobbet och göra en liten validering där.

Vi kommer inte installera flera MVC-webbramar för att visa hur vi validerar våra tidigare former, men här finns två ungefärliga lösningar i Laravel och CakePHP.

Validering i en Laravel-modell

Laravel är utformad så att du har mer tillgång till validering i Controller där du också har direkt tillgång till inmatningen från användaren. Den inbyggda validatorns typ föredrar att användas där. Men det finns förslag på internet att validering i modeller fortfarande är en bra sak att göra i Laravel. Ett komplett exempel och lösning av Jeffrey Way finns i hans Github-arkiv.

Om du föredrar att skriva en egen lösning kan du göra något som liknar modellen nedan.

klass UserACL utökar Eloquent private $ rules = array ('userName' => 'krävs | alfa | min: 5', 'lösenord' => 'krävs | min: 6', 'bekräfta' => 'krävs | min: 6 ',' email '=>' krävs 'email', 'website' => 'url'); privata $ fel Validering av offentliga funktioner ($ data) $ validator = Validator :: make ($ data, $ this-> rules); om ($ validator-> misslyckas ()) $ this-> errors = $ validator-> errors; returnera false;  returnera sant;  offentliga funktionsfel () returnera $ this-> errors; 

Du kan använda detta från din kontroller genom att helt enkelt skapa UserACL objekt och samtal validera på det. Du kommer förmodligen att ha "Registrera"metod också på denna modell, och Registrera kommer bara att delegera de redan validerade uppgifterna till din affärslogik.

Validering i en CakePHP-modell

CakePHP främjar validering i modeller också. Den har omfattande valideringsfunktionalitet på modellnivå. Här handlar det om hur en validering för vår form skulle se ut i CakePHP.

klass UserACL utökar AppModel public $ validate = ['userName' => ['rule' => ['minLength', 5], 'required' => true, 'allowEmpty' => false, 'on' => 'skapa ',' message '=>' Användarnamnet måste vara minst 5 tecken långt. ' ], 'password' => ['rule' => ['equalsTo', 'confirm'], 'message' => 'De två lösenorden matchar inte. Vänligen skriv in dem igen. ' ]]; public function equalsTo ($ checkedField, $ otherField = null) $ value = $ this-> getFieldValue ($ checkedField); returnera $ värde === $ this-> data [$ this-> name] [$ otherField];  privat funktion getFieldValue ($ fieldName) return array_values ​​($ otherField) [0]; 

Vi exemplifierade endast reglerna delvis. Det räcker att lyfta fram valideringsförmågan i modellen. CakePHP är särskilt bra på detta. Det har ett stort antal inbyggda valideringsfunktioner som "MINLENGTH"i exemplet och olika sätt att ge feedback till användaren. Ännu mer begrepp som"nödvändig"eller"allowEmpty"är inte faktiska valideringsregler. Kakan kommer att se på dessa när du genererar din syn och lägger HTML-valideringar också på fält markerade med dessa parametrar. Regler är dock stora och kan enkelt utökas genom att helt enkelt skapa metoder på modellklassen som vi gjorde Jämför de två lösenordsfälten. Slutligen kan du alltid ange det meddelande du vill skicka till vyerna vid valideringsfel. Mer om CakePHP-validering i kokboken.

Validering i allmänhet på modellnivå har sina fördelar. Varje ram ger enkel tillgång till inmatningsfälten och skapar mekanismen för att anmäla användaren vid valideringsfel. Inget behov av provtagningsrapporter eller andra avancerade steg. Validering på serverns sida säkerställer också att data blir validerad, oavsett vad. Användaren kan inte lura vår programvara mer som med HTML eller JavaScript. Naturligtvis kommer varje serversida-validering med kostnaden för en nätverkssamtal och datorkraft på leverantörens sida istället för kundens sida.


Kasta undantag från affärslogiken

Det slutgiltiga steget i kontroll av data innan vi förbinder det med systemet är i nivå med vår affärslogik. Information som når denna del av systemet bör saneras tillräckligt för att kunna användas. Affärslogiken ska endast kontrollera om det är kritiskt för fallen. Om du till exempel lägger till en användare som redan finns finns ett fall när vi gör ett undantag. Kontroll av användarens längd för att vara minst fem tecken borde inte hända på denna nivå. Vi kan säkert anta att sådana begränsningar verkställdes på högre nivåer.

Å andra sidan är att jämföra de två lösenorden en diskussionsfråga. Om vi ​​till exempel bara krypterar och sparar lösenordet nära användaren i en databas, kan vi släppa kontrollen och anta tidigare lag såg till att lösenorden är lika. Om vi ​​emellertid skapar en riktig användare på operativsystemet med hjälp av ett API eller ett CLI-verktyg som faktiskt kräver ett användarnamn, lösenord och lösenordsbekräftelse, kanske vi vill ta den andra posten också och skicka den till ett CLI-verktyg. Låt det valideras om lösenorden matchar och är redo att kasta ett undantag om de inte gör det. På så sätt modellerade vi vår affärslogik för att matcha hur det verkliga operativsystemet fungerar.

Kasta undantag från PHP

Att kasta undantag från PHP är väldigt enkelt. Låt oss skapa vår användarkontrollklass och visa hur du implementerar en användaradditionsfunktionalitet.

klass UserControlTest utökar PHPUnit_Framework_TestCase funktionstestBehavior () $ this-> assertTrue (true); 

Jag gillar alltid att börja med något enkelt som får mig att gå. Att skapa ett dumt test är ett bra sätt att göra det. Det tvingar mig också att tänka på vad jag vill genomföra. Ett test som heter UserControlTest betyder att jag trodde att jag behöver en Usercontrol klass för att genomföra min metod.

require_once __DIR__. '/ ... /UserControl.php'; klass UserControlTest utökar PHPUnit_Framework_TestCase / ** * @expectedException Exception * @expectedExceptionMessage Användaren kan inte vara tom * / funktion testEmptyUsernameWillThrowException () $ userControl = nytt UserControl (); $ userControl-> add (");

Nästa test att skriva är ett degenerativt fall. Vi kommer inte att testa för en viss användarlängd, men vi vill se till att vi inte vill lägga till en tom användare. Det är ibland lätt att förlora innehållet i en variabel från visning till företag, över alla lager i vår ansökan. Denna kod kommer naturligtvis att misslyckas, eftersom vi inte har någon klass ännu.

PHP Varning: require_once ([långvägen-här] / Test / ... /UserControl.php): misslyckades med att öppna ström: Ingen sådan fil eller katalog i [långvägen-här] /Test/UserControlTest.php på rad 2

Låt oss skapa klassen och köra våra tester. Nu har vi ett annat problem.

PHP Dödligt fel: Ring till odefinierad metod UserControl :: add ()

Men vi kan också fixa det på bara några sekunder.

klass UserControl public function add ($ användarnamn) 

Nu kan vi få ett bra testfel som berättar hela historien om vår kod.

1) UserControlTest :: testEmptyUsernameWillThrowException Misslyckades hävdar att undantaget av typen "Undantag" kastas.

Slutligen kan vi göra viss kodning.

public function add ($ användarnamn) if (! $ användarnamn) kasta ny Undantag (); 

Det gör förväntan på undantagskortet, men utan att ange ett meddelande kommer testet fortfarande att misslyckas.

1) UserControlTest :: testEmptyUsernameWillThrowException Misslyckades med att hävda att undantagsmeddelandet "innehåller" Användaren kan inte vara tom ".

Tid för att skriva Exceptionens meddelande

public function add ($ användarnamn) if (! $ användarnamn) släng ny Undantag ('Användaren kan inte vara tom!'); 

Nu gör det vårt testpass. Som du kan observera verifierar PHPUnit att det förväntade undantagsmeddelandet finns i det faktiskt kastade undantaget. Detta är användbart eftersom det tillåter oss att dynamiskt konstruera meddelanden och bara kolla efter den stabila delen. Ett vanligt exempel är när du kastar ett fel med en bastext och i slutet anger du orsaken till det undantaget. Skäl är vanligtvis tillhandahållna av tredje parts bibliotek eller ansökan.

/ ** * @expectedException Exception * @expectedExceptionMessage Kan inte lägga till användare George * / funktion testWillNotAddAnAlreadyExistingUser () $ command = \ Mockery :: mock ('SystemCommand'); $ command-> shouldReceive ('execute') -> en gång () -> med ('adduser George') -> andReturn (false); $ command-> shouldReceive ('getFailureMessage') -> en gång () -> ochReturn ('Användaren finns redan på systemet.'); $ userControl = nytt UserControl ($ command); $ UserControl-> lägga (George '); 

Kasta fel på dubbla användare kommer att tillåta oss att utforska detta meddelande konstruktion ett steg längre. Testet ovan skapar en mock som simulerar ett systemkommando, det kommer att misslyckas och på begäran kommer det att returnera ett bra felmeddelande. Vi kommer att injicera detta kommando till Usercontrol klass för internt bruk.

klass UserControl privat $ systemCommand; allmän funktion __construct (SystemCommand $ systemCommand = null) $ this-> systemCommand = $ systemCommand? : nytt systemkommando ();  public function add ($ användarnamn) if (! $ användarnamn) släng ny Undantag ('Användaren kan inte vara tom!');  klass SystemCommand 

Injektion av a SystemCommand Exempel var ganska lätt. Vi skapade också en SystemCommand klass i vårt test för att undvika syntaxproblem. Vi kommer inte att genomföra det. Dess omfattning överstiger denna handledningens ämne. Vi har dock ett annat testfelmeddelande.

1) UserControlTest :: testWillNotAddAnAlreadyExistingUser Failed att hävda att undantaget av typen "Undantag" kastas.

Japp. Vi kastar inga undantag. Logiken att ringa till systemkommandot och försöka lägga till användaren saknas.

public function add ($ användarnamn) if (! $ användarnamn) släng ny Undantag ('Användaren kan inte vara tom!');  om ! $ this-> systemCommand-> exekvera (sprintf ('adduser% s', $ användarnamn))) släng ny Undantag (sprintf ('Kan inte lägga till användare% s. Anledning:% s', $ användarnamn, $ this-> systemCommand-> getFailureMessage ())); 

Nu, de ändringar av Lägg till() Metod kan göra tricket. Vi försöker att utföra vårt kommando på systemet, oavsett vad, och om systemet säger att det inte kan lägga till användaren av vilken anledning som helst, kastar vi ett undantag. Meddelandet om detta undantag kommer att vara delkodat, med användarens namn bifogade och sedan orsaket av systemkommandot sammanfogade i slutet. Som du kan se gör denna kod vårt testkort.

Anpassade undantag

Kasta undantag med olika meddelanden är nog i de flesta fall. Men när du har ett mer komplext system måste du också fånga dessa undantag och ta olika åtgärder utifrån dem. Att analysera ett undantags meddelande och endast vidta åtgärder kan leda till några irriterande problem. Först är strängar en del av användargränssnittet, presentationen, och de har en flyktig natur. Att basera logik på ständigt föränderliga strängar leder till beredskaps mardröm. För det andra, ringer en getMessage () Metod på det fångna undantaget varje gång är också ett konstigt sätt att bestämma vad man ska göra nästa.

Med alla dessa i åtanke är det att skapa egna undantag det nästa logiska steget att ta.

/ ** * @expectedException ExceptionCannotAddUser * @expectedExceptionMessage Kan inte lägga till användaren George * / funktion testWillNotAddAnAlreadyExistingUser () $ command = \ Mockery :: mock ('SystemCommand'); $ command-> shouldReceive ('execute') -> en gång () -> med ('adduser George') -> andReturn (false); $ command-> shouldReceive ('getFailureMessage') -> en gång () -> ochReturn ('Användaren finns redan på systemet.'); $ userControl = nytt UserControl ($ command); $ UserControl-> lägga (George '); 

Vi modifierade vårt test för att förvänta oss vårt eget anpassade undantag, ExceptionCannotAddUser. Resten av testet är oförändrat.

klass ExceptionCannotAddUser utökar undantag public function __construct ($ userName, $ reason) $ message = sprintf ('Kan inte lägga till användare% s. Orsak:% s', $ användarnamn, $ orsak); förälder :: __ konstruera ($ meddelande, 13, null); 

Den klass som genomför vårt anpassade undantag är som någon annan klass, men den måste förlängas Undantag. Med hjälp av anpassade undantag ger oss också en bra plats att göra all presentation relaterad strängmanipulation. Förflyttning av sammanfattningen här eliminerade vi också presentationen från affärslogiken och respekterade principen om ensam ansvar.

public function add ($ användarnamn) if (! $ användarnamn) släng ny Undantag ('Användaren kan inte vara tom!');  om (! $ this-> systemCommand-> execute (sprintf ('adduser% s', $ användarnamn))) släng ny ExceptionCannotAddUser ($ användarnamn, $ this-> systemCommand-> getFailureMessage ()); 

Att kasta vårt eget undantag handlar bara om att ändra den gamla "kasta"kommandot till den nya och skicka in två parametrar istället för att skriva meddelandet här. Naturligtvis passerar alla tester.

PHPUnit 3.7.28 av Sebastian Bergmann ... Tid: 18 ms, Minne: 3,00Mb OK (2 test, 4 påståenden) Klar.

Fångande undantag i din MVC

Undantag måste fångas vid någon tidpunkt, om inte du vill att användaren ska se dem som de är. Om du använder en MVC-ram kan du förmodligen fånga undantag i kontrollenheten eller modellen. När undantaget är fångat omvandlas det till ett meddelande till användaren och görs inom din syn. Ett vanligt sätt att uppnå detta är att skapa en "tryAction ($ action)"-metoden i din applikations basstyrenhet eller -modell och alltid ringa den med den aktuella åtgärden. Med den metoden kan du fånga logiken och fina meddelanden som passar din ram.

Om du inte använder ett webbramverk eller ett webbgränssnitt för den delen ska ditt presentationsskikt ta hand om att fånga och omforma dessa undantag.

Om du utvecklar ett bibliotek kommer dina undantag att ta dina kunder.


Slutgiltiga tankar

Det är allt. Vi korsade alla lager i vår ansökan. Vi valideras i JavaScript, HTML och i våra modeller. Vi har kastat och fångat undantag från vår affärslogik och skapade till och med egna egna undantag. Denna metod för validering och undantagshantering kan tillämpas från små till stora projekt utan några allvarliga problem. Men om din valideringslogik blir väldigt komplex och olika delar av ditt projekt använder överlappande delar av logiken kan du överväga att utvinna alla valideringar som kan göras på en viss nivå till en valideringsservice eller valideringsleverantör. Dessa nivåer kan innehålla, men behöver inte begränsas till JavaScript-validator, backend PHP-validator, tredjeparts kommunikationsvaliderare och så vidare.

Tack för att du läste. Ha en bra dag.