Komplex példa 6. rész - A 7 RESTful útvonal és vezérlő metódus: create, store

Attila | 2022. 04. 05. 09:49 | Olvasási idő: 5 perc

Címkék: #Adatbázis (Database) #Bootstrap #Controller #CRUD #CSS #Eloquent #Laravel #Laravel 9 #Nézet (View) #Routing #Űrlap (Form)

Folytatjuk az ismerkedést a 7 RESTful útvonallal és vezérlő metódusaikkal. A show-n és az index-en az előző (tisztán Laravel-es, lásd még React JS) bejegyzésben túlestünk. Most haladunk tovább és a create, store útvonalakra, metódusokra fogunk koncentrálni. Ezekkel lehet majd új dolgot (példányt és adatsort) létrehozni.
7_RESTful_create_store

Aki lemaradt volna az áttekintő bejegyzésről, az mindenképp ott kezdje az ismerkedést: https://attila.gludovatz.hu/posts/komplex-pelda-5-resz-a-7-restful-utvonal-es-vezerlo-metodus

Mi pedig térjünk is rá rögtön a create ágra (útvonal és vezérlő metódusa együttesen), aminek a megvalósítása, optimális esetben meg kell, hogy előzze a store ágat.

Már most fontos megjegyezni, hogy mivel itt már átlépünk arra a veszélyes területre, ahol a "felhasználókkal kommunikálunk", emiatt gondoljunk a biztonságra. Tekintsünk úgy minden bemeneti értékre, mintha az ördögtől érkezett volna... ez persze nem azt jelenti, hogy hagyjuk figyelmen kívül, hanem csak azt, hogy járjunk el az érkező bemenetekkel kapcsolatban nagyon körültekintően, törekedve a biztonságra. Az ilyen bemeneti adatokat mindenképpen ellenőrizni kell kliens (HTML5 és Javascript eszközeink lesznek erre) és szerver oldalon is (itt majd a Laravel segít nekünk a validációban, ellenőrzésben). A validáció témakörét a későbbiekben fogom ismertetni, most egyelőre elég annyit tudnunk róla, hogy majd kliens és szerver oldalon is ellenőrizni fogunk.


Create

Szerencsére, a korábban áttekintett táblázatunk most is rendelkezésre áll. Ebből kiolvasható, hogy a create útvonalat felhasználói oldalról egy GET HTTP metóduson keresztül érhető el. Ez annyit fog jelenteni, hogy a felhasználó vissza fog kapni eredményül egy olyan űrlapot (HTML form), ahol beírhatja a különböző bemeneti mezőkbe, hogy az új elem (példány & adatsor) adattagjai milyen értékekkel rendelkezzen. Például megadhatja a légitársaság vagy egy utas nevét, esetleg a repülőjáratok paramétereit, hogy mennyibe kerül, mi a járatszám stb. Gyakorlati oldalon kezdjük valami egyszerűvel, aminél csak a nevet kell megadni és nincsen semmilyen idegen kulcs az adattáblájában: a légitársaságok (airlines). Hozzuk létre a web.php-ban a hozzá kapcsolódó két újabb (a show és index már megvolt korábban) útvonalat:

Route::get('/airlines/create', [AirlinesController::class, 'create']);
Route::post('/airlines', [AirlinesController::class, 'store']);

Importáljuk is be az AirlinesController-t a fájl tetején: use App\Http\Controllers\AirlinesController;

Tipp: vigyázzunk, hogy hova illesztjük be ezeket az új útvonalakat! Mert ha például a korábban létrehozott show útvonal után szúrnánk be, akkor sosem tudnánk elérni ezt a create-es útvonalat. De miért is ...? Az oka ennek az, hogy mindkettő (show és create is) GET HTTP metódus szerint működik és a web.php-ban az útvonalak regisztrációja fentről lefelé haladva működik. Emiatt a show útvonala: /airlines/{airline} a wildard (joker) karakterek miatt elfedné a create /airlines/create útvonalát és egy bosszantó 404-es hibakódot kapnánk vissza, amikor be akarnánk tölteni az /airlines/create útvonalat. Az elfedésből adódóan a Laravel azt hinné, hogy mi a show útvonalát akarjuk meghívni az {airline} helyére beillesztett "create" paraméterrel, ilyen azonosítójú (id-jú) sor pedig természetesen nem létezik az airlines adattáblában, ezért egy 404-es kódot kapnánk vissza. Úgyhogy figyeljünk mindig arra, hogy a wildcard-os útvonalakat mindig előzze meg a specifikusabb útvonal regisztráció, nehogy ez kellemetlenségeket okozzon a későbbiekben.

A kapcsolódó Controller már megvan, de a create és a store metódusok még hiányoznak belőlük, úgyhogy adjuk hozzá ezeket (példának használható a korábbi bejegyzésben létrehozott LuggageController-ben lévő create és store metódusok):

public function create()
{
  //
}
public function store(Request $request)
{
  //
}

Látható, hogy a create-nek nincsen paramétere, mivel ez majd csak egy nézetet ad vissza a felhasználónak, amin az űrlap szerepel. A store-nak viszont van paramétere, ez már egy olyan objektumot tartalmaz, ami a felhasználói bemeneti értékeket fogja tartalmazni az űrlap kitöltése és elküldése után. Adjuk hozzá a create-hez a nézetes visszatérést:

return view('airlines.create');

Ez a nézetünk még nincs meg (resources/views/airlines/create.blade.php), úgyhogy valósítsuk meg: a "keretes szerkezet" (layout) itt is adott, úgyhogy azt más nézetekből áttehetjük ide is. Aztán kicsit ismét elő kell vennünk az eddig kevésbé csillogtatott erényeinket ahhoz, hogy ízléses űrlapot rakjunk össze, ami a felhasználó szempontjából elfogadható, használata kényelmes.

<h1>Új légitársaság létrehozása</h1>
<form action="/airlines" method="POST">
  <label for="name">Légitársaság neve:</label>
  <input type="text" name="name" id="name">
  <input type="submit" value="Mentés">
</form>

Ez a működési célnak megfelel, mivel a form kezdő tag-jében meghatározzuk az eredmény elküldésének HTTP metódusát: POST és hogy melyik útvonalra küldjük: /airlines Így biztosan a store felé fogjuk irányítani az űrlapon keresztül elküldött bemeneti értéket (légitársaság neve).

Viszont elég "csúnyácska" lett így... úgyhogy annak a célnak sajnos nem felel meg, hogy a felhasználónak ez elfogadható lesz majd. Én használnék egy kliens oldali keretrendszert, amivel szebbé varázsolható lesz a kinézete az űrlapnak. Telepítsük a Bootstrap keretrendszert (bármilyen másik is szabadon használható: Tailwind, Bulma, Materialize stb., viszont majd annak megfelelően alakítsuk ki az űrlapot, amelyiket kiválasztottuk): a telepítés itt annyiból áll, hogy a layout.blade.php fájlunkban a head részben a saját CSS fájljaink beemelése elé illesszük be a Bootstrap oldaláról kinyert kódot (legfrissebb verzió 2022. áprilisában: 5.1.3):

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">

Bootstrap-hez van egy nagyon hasznos dokumentáció, amelyet szabadon használhatunk (itt konkrétan a form-okra koncentrálva: https://getbootstrap.com/docs/5.1/forms/overview/). Mi is ezt fogjuk felhasználni, készítsünk el egy ilyen űrlapot egyetlen bemeneti mezővel:

<form class="border border-light p-5" action="/airlines" method="POST">
  <label for="legitarsasagneve">Légitársaság neve:</label>
  <input type="text" id="legitarsasagneve" name="legitarsasagneve" class="form-control mt-2 mb-4" placeholder="Légitársaság neve" required>
  <input class="btn btn-info btn-block" type="submit" value="Mentés">
</form>

Így néz ki az eredmény:

A Mentés gombot nem is engedi megnyomni, amíg nincs kitöltve a légitársaság neve, itt tehát látszódik, hogy működik a kliens oldali validáció, mivel az a mező kötelezően kitöltendő. Ha beírunk valamit a mezőbe és rányomunk a mentés gombra, akkor 419-es hibakódot kapunk. Ez szintén egy Laravel-es biztonsági mechanizmus miatt van. A rendszer védi az oldalt a Cross-Site Request Forgery (CSRF) támadástól. További infók róla itt és itt. A támadás elleni védelemhez nekünk egy token-t kell generálnunk, ami nagy segítséget nyújt a védekezésben. Adjuk hozzá a form nyitó tag-je után ezt: @csrf

Ha most újratöltjük az oldalunkat és jobb egérgombbal megvizsgáljuk a bemeneti mezőnk feliratát (Légitársaság neve:), akkor az előtte lévő sorban felfedezhetünk egy új, rejtett mezőt, ami a token-t, vagyis a véletlen karaktersorozatot tartalmazza:

Ha ezekután próbáljuk meg újra elküldeni a bemeneti értékünket a mentés gomb megnyomásával, már nem fogunk 419-es hibakódot kapni, viszont mivel még nincs meg a store metódusunk, ezért csak egy üres, fehér oldalt kapunk eredményül, de vegyük észre, hogy az URL a böngészőben már jó lesz: /airlines

Még mielőtt rátérnék a store-ra: a légitársaság létrehozásának útvonalát még kényelmetlenül érjük el, úgyhogy a kapcsolódó index nézetbe tegyünk egy linket:

<p><a href="/airlines/create">Új légitársaság létrehozása</a></p>


Store

A store-hoz kapcsolódó útvonal már készen van a web.php-ban, de azért ha rápillantunk és újra értelmezzük, abból nem lehet baj. Ez tehát a POST HTTP metódus szerinti /airlines útvonalat fogja jelképezni. Egy útvonalat ugye egyszer érdemes regisztrálni és nekünk már volt egy /airlines útvonalunk, ami az index Controller metódushoz tartozott, az ott viszont GET-es volt... így a különbség látható, mert a két érték (HTTP metódus és az útvonal maga) párban jár, együttesen kell nézni őket és így kell vizsgálni a különbözőséget az útvonalak regisztrációja között.

Minden olyan útvonal, ami GET-es, azt a felhasználó kéri le a böngészője címsorába beírva a címet, vagy pedig egy linkre rákattintva. A POST, PUT/PATCH, DELETE HTTP metódusokat az űrlapok elküldésével tudjuk végrehajtani (nem azért írok nagy betűvel, mert "kiabálok", hanem mert a HTTP metódusok neveit így szokás írni).

Az útvonalon keresztül tehát elérjük a már meglévő store Controller metódust, de ott még nem csináltunk semmit, ezért is kap a felhasználó üres lapot, amikor elküldi a Mentés gomb megnyomásával az űrlapot. Implementáljuk tehát a store metódus magját. Vegyük észre rögtön, hogy a metódusnak van kapott paramétere, ami az űrlap kitöltött mezőinek értékét tartalmazza, de teszteljük is ezt le: írjunk be annyit a metódusban, hogy ...

dd($request);
// dd(request()->all());

Az első sor visszaadja a $request objektum minden elemét, beleértve a request adattag (kis háromszöget nyissuk le) paraméter elemeit. Ott találjuk meg a _token és a legitarsasagneve paraméter értékét. Ebből így egy picit mindig "bonyolult" lenne kinyerni a számunkra leginkább releváns paramétereket, úgyhogy a Laravel biztosít a számunkra egy request() segédmetódust, ami az iménti kódrészlet második sorában található: cseréljük meg a kommentezést és frissítsük az oldalt az adatok újraküldését leokézva. Így most már látható lesz, hogy csak a releváns részek fognak megjelenni ezáltal, úgyhogy ez nagyon hasznos most a számunkra.

A tényleges légitársaság létrehozáshoz már a jól bevált módszert fogjuk alkalmazni, amit a Tinker-ben korábban már többször meg is tettünk.

$airline = new Airline();
$airline->name = request('legitarsasagneve');
$airline->save();

return redirect('airlines');

Itt az első sorban példányosítjuk az Airline Model osztályt, utána felparaméterezzük a name adattagját a request() segédmetódus paraméterezésével, majd utána elmentjük az adatbázisba. Végül a redirect() segédmetódus segítségével átirányítjuk a felhasználót az /airlines (GET-es) útvonalra, ahol látni fogjuk a légitársaságok listáját rögtön, benne a legújabban létrehozott légitársasággal.

Mivel működik a kódunk, egy refactoring-ot (kód újraszervezést) is végrehajthatunk. Az itteni 3 sor igazából 1-be besűríthető:

Airline::create([
  'name' => request('legitarsasagneve')
]);

Ezzel is működni fog, de ne feledjük el, azért fogja ezt engedni a rendszer, mert az Airline Model osztályban beállítottuk a $fillable attribútumban, hogy a name mezőt engedjük kitölteni az adattáblában.


1-n adatkapcsolat megjelenítése az űrlapon

Most nézzünk egy kicsit nehezebb példát, amikor már nem csak egy nevet kell megadni, hanem ki kell választani hozzá az (adattábla és Eloquent) kapcsolaton keresztül a lehetséges társakat. Legyen ez a példa az utasról, vagyis a Passenger-ről. Őket még nem jelenítettük meg a weboldalunkon, úgyhogy mindent meg kell csinálnunk velük kapcsolatban. Kezdjük a layout fájlunkban a menüpontot először.

Viszont itt már majd alkalmazzuk a resource-t, mint összefoglaló útvonalat. A web.php-ba írjuk be ezt:

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

Ezzel az útvonal regisztrációval a korábban részletezett és a fenti képen is látható mind a 7 útvonalat tudjuk helyettesíteni. Ez így nagyon kényelmes!

Viszont még nincs meg a PassengersController-ünk, úgyhogy ezt hozzuk létre mind a 7 REST útvonalával együtt, amit az -r kapcsoló biztosít nekünk:

php artisan make:controller PassengersController -r

Az index és a show metódusok magját (valamint a kapcsolódó nézeteiket) hasonlóan kell megvalósítani mint a Flight, Airline, Luggage stb. erőforrások esetében, úgyhogy ezt most már itt nem fogom részletezni, inkább majd a különbözőségre koncentrálok. Az index metódus magja legyen ez:

return view('passengers.index', [
  'passengers' => Passenger::orderBy('name')->get()
]);

A hozzá tartozó nézet pedig legyen ez:

A show metódus magja legyen ez (a show paramétere pedig ez: Passenger $passenger):

return view('passengers.show', [
  'passengerName' => $passenger->name,
  'flightNumber' => $passenger->flight->number,
  'luggage' => $passenger->luggage->first(),
]);

Így majd meg tudjuk jeleníteni a show nézetben az utas adatait, a hozzá tartozó repülőjáratot és az ő csomagját is. Tegyük meg mindezt így a show nézetben:

Következhet a create metódus magja. Itt most a poggyászával nem foglalkozunk, hiszen azt a Luggage create metódusában kellene definiálni, hogy melyik utasokhoz tartozhat. Mi most csak arra koncentrálunk itt, hogy az adott utas melyik repülőjárathoz tartozhat. Ezt fogjuk lekérni és átadni a create űrlapnak.

return view('passengers.create', [
  'flights' => Flight::orderBy('number')->get()
]);

A create nézet pedig nézzen ki így (egy input mezővel az utas nevéhez, egy választólistával pedig a repülőjárathoz):


1-n kapcsolat elmentése a store metódussal

Passenger::create([
  'name' => request('utasneve'),
  'flight_id' => request('repulojarata')
]);

return redirect('passengers');

A mentés így már működik is... hacsak nem kapunk hibát arra, hogy a Passenger osztály mezőit nem lehet így kitölteni. Ekkor adjuk hozzá a következő sort a Passenger Model osztályhoz:

protected $fillable = ['name', 'flight_id'];


"Több a többes" (n-n-es) kapcsolat bővítéséhez az űrlap

Ilyen példa kapcsolatunk is van: a városok és a légitársaságok között (airline_city adattábla bővítése!) Eddig a városokhoz nem készítettük el a "felvételi űrlapot", a légitársaságokhoz viszont igen. Úgyhogy bővítsük azt a városokkal. Az AirlinesController create metódusát módosítsuk ennek megfelelően:

return view('airlines.create', [
  'cities' => City::orderBy('name')->get()
]);

A create nézetet is bővítsük a form-on belül:


Felhívnám a figyelmet a multiple kulcsszóra és a select name attribútumára, ami egy tömb azért, hogy több elemet is ki tudjunk jelölni benne (cities[]). Ennek hatására tudunk majd több értéket is kijelölni a listában (egymás alatt SHIFT gomb lenyomásával, vagy pedig a CTRL-t nyomva tartva tudunk több elemet is kijelölni). Itt látható az eredménye:

Így példáulaz adott nevű légitársasághoz hozzá tudok adni több elemet is.


"Több a többes" (n-n-es) kapcsolat bővítéséhez a store mentés

A store metódus magjánál segítségül hívhatjuk a korábbi Tinker-es példáink megvalósítását. Itt most még alkalmazhatjuk az attach() metódust, mivel egy új légitársaságot viszünk fel, aminek még semelyik városban nincsen telephelye. A későbbiekben a szerkesztés során érdemes lesz majd elgondolkodni azon, hogy melyik metódust lenne érdemes alkalmazni (attach-detach, sync, syncWithoutDetaching)... de ezen a hídon majd akkor megyünk át, ha odaérünk.

Airline::create([
  'name' => request('legitarsasagneve')
])->cities()->attach(request('cities'));

Így az új légitársasághoz hozzárendelhetjük a városokat is telephelyként.


Összegzés, következtetések

Látható volt az előző bejegyzésben, hogy poggyászokkal készítettük el az útvonalakat és a vezérlő metódusokat, a blogbejegyzések képeinél látható, hogy repülőjáratok vannak, most pedig mi légitársaságokkal, utasokkal és  akár más egyéb "erőforrásokkal" végrehajthattunk volna ugyanazoknak a RESTful útvonalaknak és vezérlő metódusoknak a megvalósítását. Így már könnyen észrevehető és tanulságként leszűrhető, hogy igazából csak az "erőforrást" magát kell "cserélgetni" és a CRUD műveleteket könnyedén tudjuk majd végrehajtani.

A bejegyzésben található kódváltozások Github kibővített (előzővel együtt érdemes nézni) commit-je itt található.

Megjegyzés: ha nagyon sok ilyen erőforrásunk van, akkor már kicsit unalmassá is válik ezeknek a létrehozása, úgyhogy vannak olyan kész megoldások, amelyek még egyszerűbbé tudják tenni (sok egyéb mellett) a CRUD műveleteknek is a kezelését. Lásd: Voyager admin panel.


Egy kis "műsorajánló" a továbbiakról: folytatjuk a RESTful útvonalak és vezérlő metódusok megismerését, hátravan még az edit, update, destroy. Majd megnézzük az űrlapok validációját is részletesebben, később pedig folytatjuk a teszteléses témát az űrlapokon keresztül.