Komplex példa 8. rész - Kódújraszerverzés - az útvonalak

Attila | 2022. 04. 15. 20:54 | Olvasási idő: 5 perc

Címkék: #Controller #CRUD #Laravel #Laravel 9 #Routing #Tinker #Űrlap (Form) #Üzleti logika (Business logic)

Jelenleg az útvonalaink eléggé "beégetettek", illetve statikusak. Ha valamelyik megváltozik, akkor nem frissül a többi helyen. Ezt fel tudjuk úgy oldani, hogy gyakorlatilag az útvonalak regisztrációjakor egy "alias" nevet rendelünk hozzájuk, így később csak itt kell megváltoztatnunk azért, hogy az összes hivatkozott helyen megváltozzon. Megvizsgálom a paraméter nélküli és a paraméteres útvonalakat is, sőt még az opcionális paraméterek kezelésére is mutatok példát.
routing_named_routes

Elnevezett útvonalak

Az útvonalakat a felhasználó képes elérni a böngészőjében vagy úgy, hogy ő maga gépeli be őket, vagy valamilyen linken keresztül jut el hozzájuk, esetleg - ahogy nemrég tanultuk - egy űrlap elküldésével éri el az adott útvonalat (van még persze több más mód is, de ennyi példa egyelőre talán elég nekünk).

Az alkalmazásunk jelenlegi állapotában ezek az útvonalak még eléggé statikusak, beégetettek: tehát ha mondjuk meggondoljuk magunkat a korábbi elképzelésünkhöz képest, és más útvonalon szeretnénk elérni valamilyen erőforrást, akkor azt minden olyan helyen meg kell változtatni, ahol hivatkoztunk rá: például a weboldalunk menüstruktúrájában, az űrlapunk (form) action attribútumában, az oldalon elhelyezett egyéb linkeken stb. Ez, azontúl, hogy rettentő kényelmetlen (mármint mindenhol egyesével megváltoztatni), meglehetősen nagy hibalehetőséget is magával vonz, amit pedig ugye mi nem szeretnénk. Hiszen bárhol megfeledkezhetünk róla, hogy ott, azon a helyen is meg kellett volna változtatni a beégetett linket és emiatt a weboldalunk hibás működést eredményezhetne...

Emiatt találták ki az "elnevezett útvonalakat", amikor gyakorlatilag egy "alias" nevet adunk az útvonalunknak és erre az alias névre hivatkozunk minden olyan helyen ahol használni szeretnénk... ha pedig meg akarjuk változtatni az útvonalat, akkor elég ezt megtenni egyetlen egy helyen, ott ahol azt regisztráltuk, például a routes/web.php fájlban. Az így elnevezett útvonalak neveinek mindenképpen egyedinek kell lennie, tehát például nem regisztrálhatunk két "homepage" nevű útvonalat.

Nézzünk meg ehhez egy nagyon egyszerű példát: adjunk nevet a főoldalunknak, utána pedig vizsgáljuk meg azt, hogy hogyan kell hivatkozni rá.

Route::get('/', function () {
    return view('welcome');
})->name('homepage');

Itt azt a nevet adtuk neki, hogy homepage, így tudunk majd hivatkozni rá bárhol, ahol szeretnénk. A hivatkozáshoz a route() segédmetódust tudjuk használni. Jelenleg én a főoldalamra egy helyen hivatkozok, mégpedig a menümben (layout nézet). Úgyhogy átírom ott ezt az útvonalat az a tag href attribútumában a / jel helyett használjuk ezt az idézőjelek között:

Ez egy nagyon egyszerűen "használható" útvonal volt, igazából még bonyolítottunk is picit az életünkön, hogy a / jel helyett ilyen hosszan hivatkoztunk a főoldalra... de nézzük meg inkább az elmúlt bejegyzések során megismert 7 RESTful útvonalat és velük együtt a Controller metódusaikat.


RESTful útvonalak és metódusok csoportosítása

Első pillantásra is már észrevehetjük, hogy melyik két csoportba sorolhatjuk az útvonalainkat:

  1. paraméter nélküliek (nincs az útvonalban wildcard karaktersorozat): create, store, index
  2. paraméterrel rendelkezőek (van az útvonalban wildcard karaktersorozat): show, edit, update, destroy

Ezeket használtuk már korábban is, most nézzük meg, hogy hogyan alakítható át a regisztrálásuk és a használatuk a gyakorlatban. Kezdjük az egyszerűbbekkel, a paraméter nélküliekkel. A gyakorlás során az AirlinesController-hez kapcsolódó útvonalakat és metódusokat fogom használni és átalakítani.

Itt is van egy névkonvenció, amit be kell tartanunk: hasonlóan, mint a nézetek megadásánál, ezekre az útvonal nevekre is hivatkozzunk úgy, hogy melyik erőforrás melyik metódusához tartozik, így bármikor rápillantunk, tudjuk, hogy hol kell keresni, mi a célja az útvonal elérésének.


1. Paraméter nélküli útvonalak

A paraméter nélküli útvonalak esetén tehát a következő névkonvenciókat tartsuk be:

  • create -> name('airlines.create')
  • store -> name('airlines.store')
  • index -> name('airlines.index')

Generáljunk az útvonalakból URL-eket. Hol használjuk ezeket?

  • A légitársaságot létrehozó űrlaphoz vezető linket a légitársaságok kilistázásánál használjuk biztosan, úgyhogy változtassuk meg az a tag href attribútumát erre:
  • A légitársaságot eltároló metódushoz vezető útvonalat a create nézetben használtuk, méghozzá a form tag action attribútumában, módosítsuk azt erre:
  • Eddig mondhatnánk azt, hogy "de hát ezeket csak egyetlen helyen használtuk eddig, nem is feltétlenül kellett volna elneveznünk az útvonalakat" és erre azt is mondhatnánk, hogy igazunk is van, de most jön az index-hez tartozó útvonal, amelyet már több helyen is használtunk... és bár én mindig azt javaslom, hogy tartsuk be a névkonvenciókat akkor, ha azt szeretnénk, hogy a keretrendszer segítsen nekünk, de előfordulhat, hogy mégis eltérnénk tőle valamilyen okból. Ebben az esetben biztos hasznos lehet számunkra az útvonal elnevezése, és később az egyszerűbb módosítása.
  • Az index útvonalra való hivatkozást már több helyen használtuk: a menüstruktúránkban:

  • valamint a store, update és a destroy Controller metódusok visszatérésekor:
return redirect(route('airlines.index'));


2. Paraméterrel rendelkező útvonalak

A paraméterrel rendelkező útvonalaknál valamilyen felhasználói bemenetre (input-ra) van szükségünk az útvonal eléréséhez és a kérés feldolgozásához. (Az is egy input, ha nem ad meg semmit a felhasználó, lásd még az "opcionális paramétereket" később.) A paraméterek az útvonal regisztrációjánál a wildcard helyeken vannak, amik aztán utána a Controller metódus paramétereként értelmezhetők és használhatók. Alább rögtön felsorolom, hogy az útvonalaknál milyen paramétereket lehet és érdemes használnunk.

Hogyan lehet ezeknél URL-eket generálni?

A route() segédmetódus itt is használható, viszont nem elég neki egy paraméter, hanem szükség van egy másodikra is, amelyben felsoroljuk a paramétereket... itt a RESTful útvonalaknál csak 1-1 paraméter adódik és főleg az erőforrás id-ja az, de saját, teljesen egyedi útvonalak definiálásánál és regisztrálásánál előfordulhat, hogy több paramétert is definiálunk az útvonalban: ekkor tömbként kell meghatározni a route() segédmetódus második paraméterét, amelyben kulcs-érték párokkal felsorolva tudunk az egyes paramétereknek értéket adni. Itt most elég a RESTful útvonalakra és vezérlő metódusokra koncentrálnunk még:

  • show, edit: ahhoz, hogy ezeket az útvonalakat elérjük az index nézetet kell módosítanunk, mivel mindkét beégetett link itt található. Hogy jobban látható legyen a változtatás, ezért meghagytam (megjegyzésben) az ábrán azt, hogy mikből csináltunk miket:

  • update, destroy: ezeket már nem linkekként érjük el, hanem a megfelelő form elemek action attribútumait kell megváltoztatnunk.
    • update: az edit nézetben lévő form-ot (action-jét) kell módosítani így:

    • destroy: a show nézetben lévő form-ot (action-jét) kell módosítani így:

Milyen paraméterek lehetségesek a wildcard helyeken? Javasolt-e a használatuk? Mire kell még pluszban figyelnünk?
  • Amit ne használjunk: {id}
    • Közös munkánk legelején még az id-t használtuk paraméterként, azonban ez magával vonz egy olyan utasítást a vezérlő metódus elején, hogy vagy a find($id) vagy méginkább a findOrFail($id) metódussal le kellett kérnünk az aktuális erőforrás adattáblájának a megfelelő azonosítójú (id) sorát. Például ha az útvonal ez: GET HTTP metódus szerinti /airlines/{id} --> akkor ez a show vezérlő metódust azonosítja, és ennek legelején rögtön le kellene kérnünk az airlines táblából a releváns sort, ami ugye egy felesleges kódsor lenne, hiszen használhatnánk ehelyett a következőt...
  • Amit eddig is használtunk: erőforrás neve (angolul, egyes számban), például: {airline}, {flight}, {user} stb.
    • Ha ezt használjuk az útvonalnál, akkor a vezérlő metódus paraméterében már rögtön írhatjuk az erőforrás Model osztály neve után a példány nevét is (pl.: Airline $airline), amit utána a metódus magjában már úgy használhatunk, hogy az adott azonosítójú példány adataival tudunk dolgozni. A háttérben igazából ez is az id-t használja azonosításra (kikeresésre), hiszen az útvonalban például azt adhatjuk át, hogy 'airlines/123', akkor ez a 123-as azonosítójú légitársaságot fogja jelenteni a program számára.
  • Lehetőségünk van még arra is, hogy ne az id alapján azonosítsuk az erőforrást, például egy felhasználót a neve vagy az e-mail címe alapján azonosíthatunk egyedien, például: {user:name}, {user:email}, {airline:name} stb. De fontos itt hangsúlyozni az utolsó szót, hogy egyedien. Ugyanis, ha olyan paraméter szerint szeretnénk lekérni felhasználót, ami nem egy egyedi értéket tartalmaz, akkor a működés inkonzisztenssé válhat. Adatbázistábla szinten tehát ennek a mezőnek (users tábla name vagy email attribútuma, vagy az airlines tábla name attribútuma) kulcsnak kell lennie. Ez a táblakényszer biztosítja számunkra, hogy az oszlopban lévő minden érték egyedi lesz, ami által minden adatsor a táblában egyedien azonosítható lesz. Megjegyzés: a mi komplex példánkban az airlines tábla name attribútuma nem kulcs mező, úgyhogy arra nem is feltétlenül igaz az, hogy egyedileg azonosítana minden egyes sort a táblában. Próbáljuk is ki emiatt, hogy miként jelentkezik a probléma, amit említettem, utána pedig orvosoljuk ezt úgy, hogy kulccsá alakítjuk a name mezőjét.

Új útvonal legyen a korábbi show helyett (a régit tegyük kommentbe és másoljuk le újként):

Route::get('/airlines/{airline:name}',[AirlinesController::class, 'show'])->name('airlines.show');

Ekkor viszont még, ha így néz ki a show metódusunk, akkor nem igazán fog működni, rossz eredményt kapunk vissza, mint amit mi elvárunk:

public function show(Airline $airline)
{
  dd($airline);

A metódus további részét változatlanul hagyhatjuk, hiszen az már megfelelően működik, ezzel csak teszteljük, hogy ténylegesen név alapján is le tudjuk-e kérni a légitársaságot.

De mi van akkor, ha ugyanolyan nevű légitársaságaink vannak? Itt szemléltetem:

Mivel az adatbázistábla engedi, lehet ugyanolyan nevű légitársaságunk. Ha viszont most fent a címsorba ezt ütjük be: http://127.0.0.1:8000/airlines/Austrian akkor bár a keresés jól működik, de természetesen találatként csak egyet fogunk kapni... ez pedig, ahogy említettem is, inkonzisztenciához vezetne. Így néz ki nálam az eredmény a böngészőben (nyissuk le a kis háromszöget az attributes kulcs mellett):


(A másik id-jú légitársaság nem jelenik meg, természetesen.) Ahogy említettem, a probléma megoldása csak az lehet, hogy ha csak kulcs mezőre használjuk ezt a paraméteres útvonal megoldást. Ehhez a jelen példánkban vissza kellene alakítani egyedire minden légitársaság nevét, utána pedig egy új migrációs fájlban definiálni, hogy az airlines tábla name attribútumát egyedivé tesszük (kulcs táblakényszert alkalmazunk rá). Ezután már biztonsággal működhet a "nem id-alapú" útvonal paraméterezésnél a keresés és találat.

Én viszont maradnék az id (elsődleges kulcs) szerinti keresési és találati megoldásnál, mivel az sokkal gördülékenyebben tud működni ebben a szituációban, illetve, semmi sem kényszerít jelen szituációban arra, hogy név szerint kellene ezeket lekérni. Ha persze erre volna szükségem, akkor a fenti tanácsokat kellene megfogadnom és utána a műveleteket kellene végrehajtanom.


Példák, javaslatok: több, esetleg opcionális paraméter megadása az útvonalakban és vezérlő metódusokban

Természetesen abszolút előfordulhat, hogy olyan saját útvonalat definiálunk, amelyben több paraméter is szerepel, mutatok erre is példát:

Route::get('/flights/{active}/{from}/{to}', [FlightsController::class, 'getActiveFlightsFromTo']);

Mivel a repülőjáratoknál némivel több opció kérhető le, mint a légitársaságoknál, ezért inkább erre mutatok példát. Ezzel az útvonallal lehetne lekérni az aktív/passzív repülőjáratokat egy adott dátum intervallumon belül. Ekkor a felhasználó így hívhatná meg ezt az útvonalat manuálisan (esetleg egy form input mezőiből felparaméterezve):

127.0.0.1:8000/flights/1/2022-01-01/2022-04-16

Ezzel a paraméterérték megadással az idei év aktív repülőjáratait szeretné lekérni. Az útvonal kérés feldolgozásának oldalán ugye a FlightsController felé irányítjuk a felhasználót, ahogy azt tettük a RESTful metódusok esetében is. Viszont ne konkrétan ott oldjuk meg a kérés feldolgozását a getActiveFlightsFromTo nevű metódusban, hanem hívjuk segítségül az üzleti logikát biztosító Model osztályt, ami nem csak az adatkapcsolatok menedzseléséért felel, hanem ide érdemes definiálni azokat a függvényeket, metódusokat is, amelyek a Flight osztállyal, objektumaival kapcsolatosak. A getActiveFlightsFromTo metódus például így nézhet ki a FlightsController-ben:

public function getActiveFlightsFromTo($active, $from, $to)
{
  return Flight::getFilteredFlights($active, $from, $to);
}

A getFilteredFlights kapcsolódó metódus pedig a Flight Model osztályban:

public static function getFilteredFlights($active, $from, $to)
{
  return Flight::where('active', $active)->whereBetween('created_at', [$from, $to])->get();
}

Kérdezhetnénk, hogy van-e értelme "kiszervezni" a megvalósítást a Model osztályra? A válasz az, hogy egyértelműen igen. Mivel bármikor előfordulhat, hogy máshol is le szeretnénk majd kérni az aktív/passzív repülőjáratokat adott dátumintervallumban, ekkor pedig az MVC kialakítása miatt nem nagyon (vagy csak nagyon "nyakatekerten") tudnánk a FlightsController osztály adott metódusához hozzáférni. Az üzleti logikai metódusokat mindig a Model osztályokban tároljuk.

Mi lenne akkor, ha például csak az adott "dátumtól" (from) paramétert ismerjük és mindig a mai napig szeretnénk lekérni a repülőjáratokat?

A megoldás az, hogy nem kell ehhez új útvonalat és metódusokat regisztrálnunk, hanem csak kicsit módosítanunk kell a meglévőeket, mert lehetőségünk van opcionális paramétereket definiálni az útvonalaknál. Nézzük meg az átalakításokat:

Route::get('/flights/{active}/{from}/{to?}', [FlightsController::class, 'getActiveFlightsFromTo']);

Gyakorlatilag egy kérdőjelet kell odatenni a "to" paraméter végére. Mindez a kapcsolódó Controller metódusban ez kell a helyes működéshez a 3. paraméternél:

public function getActiveFlightsFromTo($active, $from, $to = null)

Míg a Flight Model osztály getFilteredFlights metódusa így néz ki:

public static function getFilteredFlights($active, $from, $to = null)
{
  if($to == null) $to = Carbon::now()->format('Y-m-d');
  
  return Flight::where('active', $active)->whereBetween('created_at', [$from, $to])->get();
}

Tehát látszódik, hogy a return utasítás nem változott.

Mindezt a Tinker-ben letesztelhetjük: php artisan tinker

Flight::getFilteredFlights(1, '2022-01-01', '2022-02-16');
Flight::getFilteredFlights(0, '2022-03-31');

Az első utasítás az első aktív repülőjárattal tér vissza, ami 2022. január 1. után és 2022. február 16. között lett létrehozva. A második utasítás az első "passzív" (tehát valamilyen szempontból inaktív) repülőjáratot mutatja 2022. március 1. után.

Ugyanígy persze manuálisan is letesztelhetjük, ha a böngészőnk címsorába lekérjük a következő útvonalakat:

Ezek az útvonalak persze csak példák... úgy teszteljük őket, hogy a mi adatbázisunk szerint látható eredményt hozzanak.


Egy általános megállapítás a végére

Mindezek persze mind alapértelmezetten is igazak akkor, ha a 7 RESTful útvonalat 1 resource útvonalként hoztuk létre. Mindezt ellenőrizhetjük a Terminal-ban, amikor kiadjuk a következő utasítást:

php artisan route:list

Itt láthatók, a "manuálisan" létrehozott AirlinesController-hez kapcsolódó útvonalaink:

Az utolsó előtti "oszlopban" látszódik az útvonal neve. Ahogy pedig említettem, mindezek megvannak a luggage és a passengers esetében is, hiszen ezeket így regisztráltuk:

Route::resource('luggage', LuggageController::class);
Route::resource('passengers', PassengersController::class);

A bejegyzés kódolási módosításai az itteni Github commit-ben találhatók meg.

Gyakorlásként javaslom, hogy a többi útvonalat és a hozzájuk kapcsolódó kódokat is módosítsátok. Így tud rögzülni a használata.