Címkék: #Érvényesítés (Validation) #Laravel #Laravel 9 #Mintaillesztés (Pattern matching) #Nézet (View) #Többnyelvűsítés (Localization) #Űrlap (Form)
Milyen egyedi szabályok képzelhetők el a komplex példánk kapcsán?
Az első szabályt megmutatom, hogy hogyan kell megoldani egy korábbi módszer szerint, a 2. szabályt pedig megnézzük egy nagyon friss Laravel keretrendszer 9.18-as verziószám szerinti újítás alapján.
Azért először ezt mutatom meg, mert ha ezt átlátjuk, utána már sokkal könnyebben fogjuk tudni alkalmazni a 9.18-as újítást is. Kezdjük meg a munkát egy Rule (szabály) objektum létrehozásával:
php artisan make:rule LegalAgeRule
Az utasítás kiadásának hatására létre is jött egy új fájl az app / Rules mappában (a mappa is új amúgy): LegalAgeRule.php
Ha megnézzük ezt a fájlt, akkor láthatunk benne három metódust:
De térjünk is vissza a mi példánkra és próbáljuk ki az iménti dolgokat a gyakorlatban. Állítsunk be az osztályon belül egy változót, aminek majd a konstruktoron keresztül adunk kezdőértéket.
private $legalAge;
public function __construct($age)
{
$this->legalAge = $age;
}
A passes metódussal fogjuk vizsgálni, hogy a bemenetként kapott születési dátum az régebben volt-e mint az imént beállított "legális életkor", tehát hogy a születési dátum alapján elmúlt-e mondjuk 18 éves az utas...
A dátumok kezelése általában problémás szokott lenni a programozásnál, mivel ahány nyelv és nemzet, annyiféleképpen használják és formázzák a dátumaikat. Ezért nagyon gyakori, hogy a keretrendszerek (mint a Laravel is) és a kiegészítő csomagjaik próbálnak segítséget nyújtani a dátumok kezelésében. A Carbon is egy ilyen csomag (részletes használatáról itt nézhetünk meg rengeteg példát). Importáljuk be ezt a csomagot a fájl tetején:
use Carbon\Carbon;
Majd jöhet az alkalmazása is:
public function passes($attribute, $value)
{
$formattedValue = new Carbon($value);
$legalAge = Carbon::now()->subYears($this->legalAge);
return $formattedValue < $legalAge;
}
Kód értelmezése: a kapott értéket egy új Carbon objektumnak adjuk át. Utána a konstruktorban definiált legális életkornál megnézzük, hogy mi a referencia dátum (például, ha 18 év van beállítva neki és ma 2022. augusztus 10. van, akkor a referenciadátum az ebből a dátumból kivonva a 18 év lesz, tehát 2002. augusztus 10.), amit végül összehasonlítunk a felhasználótól kapott születési dátummal annak eldöntésére, hogy elmúlt-e a felhasználónk 18 éves.
Végül következhet az üzenet definiálása, amit akkor kap meg a felhasználó, ha elbukott a validáción (vagyis nincs még 18 éves):
public function message()
{
return 'Az utasnak legalább ' . $this->legalAge . ' évesnek kell lennie!';
}
Itt egy sima szövegösszefűzéssel üzenjük meg a felhasználónak (leendő utasunknak), hogy még nem múlt el 18 éves, ezért nem utazhat velünk.
... és ennyi! Elvileg a szabály megfogalmazásával készen is vagyunk, már csak hozzá kell fűznünk az utas létrehozásának szerver oldali validációjához, ami pedig a PassengersController store() metódusában történik meg. Megjegyzés: az utasoknál már sok kliens oldali validációt csináltunk (views / passengers / create nézet), de innen a PassengerController store() metódusából még hiányzik is ez a szerver oldali validáció. Sebaj, rögtön pótoljuk ezt a hiányosságot és a kliens oldali validációnak megfelelően először definiáljuk a szerver oldali szabályokat itt:
$request->validate([
'utasneve' => 'required|min:10|max:50',
'age' => 'required|numeric|min:6|max:99',
'email' => 'required|email|max:100',
'phone' => 'required|max:11',
'repulojarata' => 'required|integer|exists:flights,id',
]);
Definiáltuk is itt az alapvető szabályokat, magyarázni már talán nem is kell őket és most attól is eltekintek, hogy mindegyik szabályhoz saját hibaüzenetet adjak meg, hiszen azt mindenki meg tudja tenni az előző blogbejegyzésem alapján (ami viszont fontos, hogy a views / luggage / create nézetből másoljuk át a views / passengers / create nézetbe a szerver oldali hibák megjelenítését: @if ($errors->any()) résszel kezdődő elágazás). Megjegyzésként talán még újra annyit hozzáfűznék, hogy ha ezeket a szerver oldali szabályokat akarjuk tesztelni helyes működés szempontjából, akkor addig érdemes kivenni a kliens oldali ellenőrzéseket és ha meggyőződtünk a szerver oldali szabályok működéséről, akkor vissza lehet tenni a kliens oldali ellenőrzéseket is.
Ez eddig teljesen jó, de még nem használtuk fel azt a funkcionalitást, amin korábban dolgoztunk, az egyedien létrehozott legális korhatár szabály alkalmazását. A születési dátum mező viszont még hiányzik a folyamatunkból, nézzük meg, hogy mit kell csinálni hozzá:
views / passengers / create.blade.php -ban helyezzük el a form-on belül. Megjegyzés: tudom, hogy van már egy életkor (age) mezőnk is, amelyek összefüggésben vannak olyan szempontból, hogy ha valaki beállítja, hogy 2000-ben született, de 50 éves kort ad meg magának, akkor az ellentmondáshoz vezet, de ilyeneket szoktak alkalmazni marketingkutatásoknál is, úgynevezett ellenőrző kérdésként.
<label for="birthdate">Születési dátum:</label> <input type="date" name="birthdate" id="birthdate" class="form-control mt-2 mb-4"
required>
A PassengersController store() metódusának releváns bővítése kiemelve a születési dátumos részekkel:
$request->validate([
'utasneve' => 'required|min:10|max:50',
'age' => 'required|numeric|min:6|max:99',
'email' => 'required|email|max:100',
'phone' => 'required|max:11',
'repulojarata' => 'required|integer|exists:flights,id',
'birthdate' => ['required', new LegalAgeRule(18)]
]);
Passenger::create([
'name' => request('utasneve'),
'flight_id' => request('repulojarata'),
'age' => request('age'),
'email' => request('email'),
'phone' => request('phone'),
'birthdate' => request('birthdate'),
]);
Megjegyzés: Hol lehet ez az egyedi szabály létrehozás még hasznos? Például egy alkoholokkal kereskedő webshopnál rögtön az elején, amikor meg kell adni a születési dátumot, akkor csak akkor engedjük tovább a látogatót, ha ("kiszámoltuk" és) elmúlt már 18 éves. A konstruktoros értékadásnál egy sima paraméterátadással a 18-at módosíthatjuk 21-re is, ha mondjuk az USA-ban van a webshopunk célközönsége.
Természetesen importáljuk is be a fájl elején a LegalAgeRule-t, mert csak akkor fog működni. Majd jöhet egy tesztkitöltés:
És vissza is kapjuk a Mentés gomb megnyomása után hibaüzenetként az űrlap felett, hogy az utasnak legalább 18 évesnek kell lennie!
A Laravel keretrendszer szereti, ha logikusan építjük fel az alkalmazásunkat és az építőkockákból utána mi magunk is hatékonyabban tudunk építkezni. Emiatt válasszuk szét a szerver oldalon a validációt és az eltárolási folyamatot, így a validáció utána újrafelhasználhatóvá fog válni, ha ugyanazokat a szabályokat akarjuk használni a store(...) és az update(...) metódusok esetében, ami valljuk meg, eléggé valószínű. Hozzunk létre egy PassengerStoreUpdateRequest-et:
php artisan make:request PassengerStoreUpdateRequest
Ennek az utasításnak a hatására az app / Http mappában létrejön a Requests mappa és benne az új fájlunkkal: PassengerStoreUpdateRequest.php. Ebbe fogjuk áthelyezni a validációs szabályainkat. Két alapértelmezett metódust tartalmaz, az authorize() a felhasználói engedélyekkel kapcsolatos, de mivel nekünk még nincsenek regisztrált / bejelentkezett felhasználóink, ezzel csak annyit foglalkozzunk, hogy a return után true-val térünk vissza, nem false értékkel, különben mindig 403-as HTTP hibakódot kapnánk (This action is unauthorized.). Viszont ott van a rules() metódus, amibe betesszük a validációs szabályainkat a PassengersController store() metódusából. Látható, hogy ez csak egy tömbbel tér vissza, úgyhogy ezt kell idemásolnunk majd (a LegalAgeRule importálásával együtt):
public function rules()
{
return [
'utasneve' => 'required|min:10|max:50',
'age' => 'required|numeric|min:6|max:99',
'email' => 'required|email|max:100',
'phone' => 'required|max:11',
'repulojarata' => 'required|integer|exists:flights,id',
'birthdate' => ['required', new LegalAgeRule(18)]
];
}
Így viszont a PassengersController store() metódusából ugye kivettük a validációt, amit mégiscsak vissza kellene kötni valahogy. Így tehetjük meg:
public function store(PassengerStoreUpdateRequest $request)
{
$validated = $request->validated();
Passenger::create([ ...
Kiemeltem tehát a store() metódus paraméterét (amit természetesen a fájl elején importálni is kell!), valamint a 3. sorban látható validálást.
Tesztelés után láthatjuk, hogy a kódunk szerencsére ugyanúgy működik. A kódújraszervezésnek köszönhetően pedig hozzájutottunk egy olyan kódegységhez, ami könnyedén újrafelhasználható most már akár a PassengersController update() metódusában is.
Ez volt tehát a teljes példa, amit a Laravel keretrendszer 9.18-as verzióújításának köszönhetően rövidebben is meg tudtunk volna oldani, de ezt most a 2. példánkban fogjuk megtenni.
A példát kétféleképpen is megoldjuk, utána mindenki eldöntheti, hogy melyik használatát tartja célravezetőbbnek, logikusabbnak, hasznosabbnak.
Bővítsük ki a PassengerStoreUpdateRequest osztály rules() metódusában a phone-ra vonatkozó szabályt:
'phone' => 'required|max:11|regex:/^\d{2}\/\d{3}\-\d{4}$/'
Itt az utolsó szabály a lényeges most, ami a mintaillesztésről szól (ez ugyanaz, amint a kliens oldalon a telefonszám bemeneti mező pattern attribútumában is meghatároztunk). Megjegyzés: előfordulhat a mintaillesztésnél, hogy a | karakter is fontos szerepet játszik a mintában, ilyenkor a bemeneti adathoz tartozó szabályok sorozatát inkább tömbként definiáljuk ('phone' => ['required', 'max:11', 'regex_szabály']), és ne pedig így szövegesen | karakterrel elválasztva. Ez a háttérben a PHP preg_match metódust használja, amivel rá is kanyarodok rögtön a következő alfejezetre...
Hozzunk létre egy új szabályt (Rule) tartalmazó osztályt, ahogy korábban is tettük, de egy kis kiegészítéssel:
php artisan make:rule PhoneNumberFormatInvokableRule --invokable
Létre is jön az új fájl a már meglévő app / Rules mappánkban. Viszont az --invokable kapcsoló miatt ez már más szerkezettel jött létre. Nincsenek benne a __construct(), passes(), message() metódusok, mindössze egy __invoke() metódusunk van, azonban majd látni fogjuk, hogy ezzel "ki tudjuk váltani" az imént említett három másik metódust is.
Amit az egyszerűbb megoldásnál említettem (preg_match), azt itt már expliciten mi fogjuk használni:
public function __invoke($attribute, $value, $fail)
{
if( ! preg_match('/^\d{2}\/\d{3}\-\d{4}$/', $value)) {
$fail('A telefonszám formátuma nem megfelelő.');
}
}
Kódmagyarázat: egy feltételvizsgálatot tartalmaz a kód, ami azt vizsgálja, hogy a megadott minta illeszkedik-e a kapott telefonszám bemeneti adatra, ha igen, akkor nyilván nincsen baj, emiatt nekünk azzal kell foglalkoznunk, hogy mi van, ha nem passzol (ezért van a preg_match metódus előtt egy negálás), akkor futunk rá a hiba kiváltására és a hibaüzenet kiírására.
Majd a PassengerStoreUpdateRequest osztály rules() metódusában a phone-ra vonatkozó szabályt:
'phone' => ['required', 'max:11', new PhoneNumberFormatInvokableRule()],
Ezt az osztályt természetesen importáljuk is a fájl tetején.
Természetesen itt is működik a többnyelvűsítés a hibaüzenetek megjelenítésénél. Nézzük is meg a példát: adjunk hozzá egy új kulcs-érték párost a lang / en / validation.php fájl visszatérési tömbjéhez:
'phone_format' => 'The phone number format is invalid.',
A PhoneNumberFormatInvokeableRule osztályban az __invoke() metódusban lévő feltételben lévő sort módosítsuk:
if( ! preg_match('/^\d{2}\/\d{3}\-\d{4}$/', $value)) {
$fail('validation.phone_format')->translate();
}
A $fail Closure paramétere a nyelvi fájlnév (validation) után ponttal elválasztva a tömbben lévő kulcs neve (phone_format). Ezután teszteljük és próbáljuk ki, hogy a szerver oldali validáció a megfelelő (angol nyelvű) hibaüzenettel tér-e vissza, ha hibázunk a kitöltéskor a telefonszám formátum kapcsán.
Másik nyelv teszteléséhez hozzuk létre a lang / hu / validation.php fájlt és adjuk hozzá ugyanazt a kulcsot, de más (magyar) értékkel:
<?php
return [
'phone_format' => 'A telefonszám formátuma nem megfelelő.',
];
Most elmentés után, ha azt szeretnénk, hogy a magyar hibaüzenetet írja ki, akkor a config / app.php fájlban a 'locale' kulcs értékét kell átírnunk 'hu'-ra (vagy majd vissza 'en'-re, attól függően, hogy melyiket akarjuk tesztelni), és így tudjuk tesztelgetni a többnyelvű hibaüzenet kiíratását.
A tesztelés sikeréről való megbizonyosodás után érdemes visszatenni a kliens oldali validációt, ami a telefonszám bemeneti mezőnél a pattern attribútum és értékének beállítása lesz.
Ebben a bejegyzésben áttekintettem az egyedi validációs szabályok alkalmazását, példákkal illusztrálva. A két bővebb példa az űrlapok leginkább problémás részeire fókuszált: egy dátum típusú mezőre és egy mintaillesztési probléma megoldására. A 2. példa összetett részében rávilágítottam egy olyan újításra is, ami a Laravel keretrendszer 9.18-as verziószámánál jelent meg. Ennek segítségével nagyon egyszerűen kezelhető az egyedi érvényesítési szabályok alkalmazása, akár többnyelvűsítéssel is.
A bejegyzéshez tartozó Github commit-ben minden részlet benne van, még az is, amit esetleg kódújraszervezéssel átstrukturáltam (ezeket nem töröltem, csak kommenteltem). Továbbá például a passengers / create nézetben a bemeneti mezők alapértékeinek (value) beállításai is megvannak, amelyekre itt már külön nem hívtam fel a figyelmet.
Továbblépésként azt gondolom, hogy érdemes lenne tesztelni a validációt automatikusan, úgyhogy a Laravel Dusk használata ismét előtérbe kerülhet.