Komplex példa 7. rész - A 7 RESTful útvonal és vezérlő metódus: edit, update, destroy

Attila | 2022. 04. 13. 17:45 | Olvasási idő: 4 perc

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

Haladunk tovább a 7 RESTful útvonal és vezérlő metódus bemutatásán és itt átvesszük a megmaradt hármat: az edit és az update páros kerülnek sorra, majd legvégül a destroy.
7_RESTful_edit_update_delete

A create és store páros működéséhez nagyon hasonló az edit és az update. Míg előbbieknél egy új sor beszúrását eredményezi a folyamat befejezése, addig utóbbiaknál egy már meglévő sor mezőinek frissítése fog megtörténni. Most is követni fogom azt a folyamatot, aminél az adatbázis kapcsolattípusok megvalósítására mutatok példákat mind az edit, mind az update esetében. Kezdjük az egyszerűbbekkel, majd haladunk a bonyolultabbak irányába...


Egy egyszerű szöveges mező frissítése - edit

Ahogy említettem, ez hasonlít a create-hez annyiban, hogy egy űrlapot fog visszaadni a felhasználónak, de nem üresen, hanem a már létező példány (és kapcsolata(i)) mezőértékeivel kitöltött bemeneti elemekkel. Mindezeket a már jól ismert gyakorlati komplex példánkon keresztül fogom bemutatni. Legelőször arra van szükségünk, hogy lehetővé tegyük majd a felhasználóknak, hogy eljuthassanak arra az oldalra, ahol majd az űrlapon szerkeszteni tudják a meglévő erőforrást. Megint a légitársaságokkal fogom szemléltetni az első példát. A weboldalon a légitársaságok menüponton kerülnek felsorolásra a légitársaságok nevei, amelyek kattinthatóak és ha rájuk kattint a felhasználó, akkor gyakorlatilag a show útvonalat, vezérlő metódust éri el és a nézetet kapja vissza eredményül. Bővítsük ki ezt úgy, hogy minden légitársaság neve mellett ott lesz egy másik link is, amivel szerkeszthetővé tesszük az adott légitársaságot. Tehát az airlines/index nézetet bővítsük így:

Látható, hogy a felsorolási pontok egy linkkel lettek kibővítve, ami a megfelelő útvonalra irányítja majd a felhasználót. Ha gondoljátok, akkor nyugodtan lehet használni a Route::resource('airlines', AirlineController::class) összefoglaló útvonalat a meglévő 4 bővítése helyett és akkor egy sorban meglesz gyakorlatilag mind a 7 útvonal. Én most még azért a gyakorlás miatt egyesével végigmutatom mind a 7-et, úgyhogy következhet az edit-es útvonal a web.php-ban:

Route::get('/airlines/{airline}/edit', [AirlinesController::class, 'edit']);

Majd következhet az AirlinesController edit metódusának megvalósítása, ami egy űrlapot ad vissza, viszont már kitöltött mezőkkel:

public function edit(Airline $airline)
{
  return view('airlines.edit', [
    'airline' => $airline
  ]);
}

A szerkeszteni kívánt légitársaság adatai így átküldésre kerülnek az airlines mappában lévő edit nézetnek. A nézet tartalmát másolhatjuk a create nézetből, de egyelőre a "select" részt töröljük ki belőle, azzal majd a későbbiekben bővítjük ki, amikor a több-többes kapcsolatok szerkesztését mutatom be.

A form felett frissítsük a szöveget, mert most már ez nem új légitársaság létrehozásáról szól, hanem egy meglévő frissítéséről. Több dolgot is módosítanunk kell a form-on belül és majd a kezdő tag-ben is. Kezdjük a légitársaság nevét tartalmazó input mezővel:

Kiemeltem, hogy a value attribútumra kell figyelni, annak segítségével tudunk alapértelmezett értéket adni az input mezőnek. A submit gomb feliratát (value attribútum értékét) is módosítsuk "Frissítés"-re.

A form kezdő tag-ben állítsuk be az action-be az update útvonalra:

Ezután következhet a legérdekesebb módosítás, ugyanis az űrlap elküldésére csak kétféle lehetőségünk van: GET vagy POST. Ha a most szükséges PUT-ot vagy PATCH-et írnánk (vagy a későbbiekben a törlésnél használandó DELETE-et), akkor az nem működne, ezért egy kis trükkel kell élnünk:

@method('PUT')

Ezt a sort adjuk hozzá a form-on belül (hasonlóan, mint ahogy a @csrf -et már hozzáadtuk). Ha most elmentjük a nézetet és behozzuk a böngészőben, majd jobb egérgombbal megvizsgálva a form bármely részét (hogy alul a kódnál az látszódjon), akkor a következőt kell látnunk:


Kiemeltem a fontos részeket a képen, látható, hogy a form belső elemei (szöveges mező és a gomb value értékei) megfelelően frissültek, valamint bekerült egy rejtett (hidden) mező is, ami arra utal majd a rendszernek, hogy ez igazából nem egy sima POST küldés, hanem PUT lesz majd és így tudjuk elküldeni a form action-jében látható útvonalra.


Egy egyszerű szöveges mező frissítése - update

Következhet az adatokat fogadó oldal megvalósítása, vagyis az update útvonal és Controller action. Az útvonal így néz ki:

Route::put('/airlines/{airline}', [AirlinesController::class, 'update']);

A hozzá tartozó metódus a Controller-ben, pedig hasonlít a store metódus magjához, de most nem egy újat kell generálnunk, hanem egy már meglévő példány tagváltozóit kell frissítenünk.

public function update(Request $request, Airline $airline)
{
  $airline->update([
    'name' => request('legitarsasagneve')
  ]);

  return redirect('airlines');
}

Látható, hogy a (2.) paraméterül megkapott $airline példánynak az update() metódusát hívjuk meg, amiben frissítjük a légitársaság nevét. Végül pedig visszairányítjuk a felhasználót a légitársaságokat listázó oldalra.


1-n kapcsolat frissítése - edit

Mivel korábban a kapcsolattípust az utason (passenger) keresztül mutattam be, maradjunk most is ennél a verziónál, legalább tudjuk gyakorolni az imént tanultakat is. Az utasok útvonalait már létrehoztuk egyetlen sorral, így ott már nem kell foglalkoznunk a hozzáadással, az edit és az update metódusa viszont még üres, mint ahogy az edit nézet is hiányzik... de haladjunk szépen sorban: a passengers index nézetébe tegyük bele a linket, ami elvezet az utas szerkesztéséhez:

Kiemeltem a linket a képen. Utána az edit metódust nézzük meg a PassengersController-ben:

public function edit(Passenger $passenger)
{
  return view('passengers.edit', [
    'passenger' => $passenger,
    'flights' => Flight::orderBy('name')->get()
  ]);
}

Ha megnézzük, akkor láthatjuk, hogy itt is nagyon hasonlít ez a create metódusra, viszont paraméterül megkapja az utas egy példányát, amit utána tovább is küldünk itt az edit nézetnek. De ez még nem létezik, úgyhogy hozzuk létre, a tartalmát pedig átmásolhatjuk a create nézetből, a különbségekre pedig ismét felhívom a figyelmet. Az űrlap feletti szöveg és a form nyitó tag, valamint a metódus a form-on belül változzon meg a kép alapján:

Az űrlap gombjának feliratát változtassuk meg "Mentés"-ről "Frissítés"-re. A sima szöveges beviteli mezőnél ugyanúgy kell eljárnunk, mint a légitársaságos példánál tettük: value attribútumba kerüljön bele az utas neve ($passenger->name). Ami viszont érdekesebb az a select tag lesz, illetve az option -je. Mivel azt szeretnénk, hogy a már meglévő utasnál a lenyíló lista ne a legelső elemére ugorjon (ne az legyen kiválasztva), hanem az, amelyik ténylegesen annak az utasnak a repülőjárata, ezért egy feltételvizsgálatot kell elhelyeznünk az option nyitó címkéjébe: azt kell vizsgálnunk, hogy az adott légitársaság azonosítója megegyezik-e az utasnál eltárolt légitársaság azonosítóval (külső kulcs). Ha megegyezik, akkor írjuk oda az option-be, hogy "selected", vagyis kiválasztott.

A kiemelésen látszódik, hogy hogyan változott az option tag nyitóeleme.


1-n kapcsolat frissítése - update

Végül következhet a frissítés a Controller update action-jében:

public function update(Request $request, Passenger $passenger)
{
  $passenger->update([
    'name' => request('utasneve'),
    'flight_id' => request('repulojarata')
  ]);

  return redirect('passengers');
}

Így már működik is az utas nevének és repülőjáratának is a frissítése. Teszteljük le néhány utassal!

Megjegyzés: ha az edit űrlapon a szöves input mező name attribútumát "utasneve"-ről átírom "name"-re, a select name attribútumát pedig "utasneve"-ről átírom "flight_id"-ra (tehát úgy változtatom meg a bemeneti mezők értékét, ahogy hívjuk őket a passengers adattáblában is), akkor a következő egyszerűsítéssel is élhetünk az iménti update metóduson belül:

public function update(Request $request, Passenger $passenger)
{
  $passenger->update(request()->all());
  return redirect('passengers');
}

Hiszen a request()->all() metódushívás egy tömbbel tér vissza, amely így alkalmas arra, hogy minden felhasználói bemenetként érkező értéket lekérjünk, majd átadjuk az utas példány frissítő metódusának. Azonban ez veszélyes is lehet, ha a kapcsolódó Model osztályban a $fillable helyett az ellentetjét, a $guarded tagváltozót használom üres tömbként. Ekkor ugyanis az adatbázis mezők közül egyetlen érték sem lenne védett és a request()->all() esetén a rosszindulatú felhasználó akár az id mező értékét is tudná módosítani úgy, hogy az űrlap kódjában kliens oldalon elhelyez egy id nevű bemeneti mezőt, aminek az értékét is beállítja egy számára kívánt értékre és akkor esetleg olyan adatsort tudna módosítani (create esetén is ugyanez igaz, csak ott beszúrni), amihez nem lehetne joga. Biztonságosabb ezért a $fillable-t használni, és a request()->all() helyett pedig ebben az esetben ezt:

request(['name', 'flight_id'])

Így is működni fog és így nem is veszélyes.


n-n kapcsolat frissítése - edit

Ehhez ismét vegyük elő a légitársaságok - városok kapcsolatot és kezdjük a szerkesztési űrlap összerakásával. Ehhez az airlines / create nézetből ismét másoljuk át a select tag elemeit és az imént használt @if direktívát alkalmazzuk itt is: tehát amely légitársaságok össze vannak kapcsolva az adott városokkal, akkor azok legyenek kiválasztva (selected).

Megjegyzés: az @if - @endif Blade direktíva helyettesíthető a @selected direktívával, ami egy feltételvizsgálatként funkcionál és ha igaz, akkor kiírja, hogy "selected". Jelen példa így működhet az @if - @endif páros helyett:

@selected ( $airline->cities->pluck("id")->contains($city->id) )

Így csak azok lesznek kiválasztva a select-ben, amelyek tényleg az adott légitársasághoz tartoznak. A feltételvizsgálatban lévő utasítás elsőre talán bonyolultnak tűnhet, ezért érdemes lehet segítségül hívni a Tinker-t, amikor akár csak egy picit is elbizonytalanodunk, hogy jó-e az, amit csinálunk:

php artisan tinker

Majd "építsük fel" vagy "fűzzük össze", amit szeretnénk (a sorok után megjegyzésbe odaírtam, hogy mi történik az utasítás kiadása után):

$airline = Airline::find(1)->first(); // olyan azonosítószámot válasszunk, ahol tudjuk, hogy van kapcsolat az airline_city táblában
$airline->cities; // nálam az 1-es légitársaság a légitársaság 6 várossal van összekapcsolva, itt ezeket meg is kapom egy gyűjteményként
$airline->cities->pluck("id"); // nekünk viszont csak a városok id azonosítóira van szükségünk
$airline->cities->pluck("id")->contains(4); // a 4-es várossal nincs kapcsolatban, ezért ez false értéket ad vissza
$airline->cities->pluck("id")->contains(5); // a 5-ös várossal kapcsolatban van, úgyhogy ez true értéket ad vissza

Gyűjteményeket (adattárolókat) nagyon sokszor kell kezelnünk a programozás során. Emiatt a Laravel biztosít a számunkra rengeteg segédfüggvényt, amelyekkel megkönnyíti a munkát velük. Ilyen a pluck és a contains is, előbbi a gyűjteményben lévő példányok adott (itt id) paramétereit gyűjti ki, míg a contains megvizsgálja, hogy az eredményül kapott újabb gyűjtemény tartalmaz-e egy adott elemet (itt 4-est és 5-öst nézzük meg) és annak megfelelően, hogy tartalmazza-e visszatér nekünk false/true, tehát hamis vagy igaz értékkel. Nekünk pedig pontosan ez kellett az edit űrlapon lévő option kapcsán. A gyűjteményekről részletesen itt lehet informálódni, de majd mi is tanulunk még róluk: https://laravel.com/docs/9.x/collections


n-n kapcsolat frissítése - update

Az update metódust mindössze egyetlen utasítással kell bővítenünk a return utasítás előtt és már működik is a frissítése a kapcsolatoknak is:

$airline->cities()->sync(request('cities'));


Egy elem törlése - destroy

Végül nézzük meg, hogy hogyan lehet egy meglévő elemet törölni, mondjuk egy légitársaságot. Adjuk hozzá a 7 közül az utolsó útvonalat:

Route::delete( '/airlines/{airline}', [AirlinesController::class, 'destroy']);

Ehhez szükség lesz az AirlinesController-ben a destroy metódusra:

public function destroy(Airline $airline)
{
  $airline->delete();
  return redirect('airlines');
}

Ez eddig oké, na de hogy juthatnánk el erre az útvonalra? Hiszen azt mondtam korábban az űrlapok elküldésénél, hogy csak GET és POST metódus létezik. Ugyanúgy kell ezt is manipulálni, mint a PUT-nál tettük. Hozzuk létre az űrlapot mondjuk a légitársaságok show nézetén belül.

Ezekután, ha behozzuk valamelyik légitársaság show nézetét, akkor már megjelenik a Törlés gomb és könnyedén (már-már túl könnyedén, hiszen nincsen ismételt rákérdezés sem a törlésről) tudjuk törölni az adott légitársaságot.

Mi a helyzet akkor, amikor olyan légitársaságot akarunk törölni, aminek van(nak) kapcsolódó városa(i)?

Ez már a kapcsolat beállításától függ, hogy hogyan adtuk meg a migrációs fájlban az onDelete metódust (részletesebben az itteni megjegyzésben olvasható, hogy milyen onDelete beállítások vannak és hogyan működnek). Itt látható például az airline_city tábla létrehozó metódusa: benne kiemeltem, hogy mi történjen az ebben a táblában lévő sorral vagy sorokkal, ha törlésre kerül az airlines táblában lévő adatsor (cascade, tehát továbbgyűrűzés fog történni ebben az esetben):

Ha keresünk egy olyan légitársaságot, aminek vannak városai, akkor ezek a kapcsolatok is törlődni fognak a kapcsolótáblából (airline_city).

A bejegyzésben található forráskód módosítások az itteni Github commit-ben találhatók meg.


Gyakorlás

Ahhoz, hogy jól megismerjük a 7 RESTful útvonal és vezérlő metódus, valamint a hozzájuk kapcsolódó nézetek megvalósítását, az utóbbi blogbejegyzések elegendőek. Azonban mindez mit sem érne, ha nem gyakorolnánk be a használatukat. Úgyhogy szánjunk időt arra, hogy a meglévő egyéb erőforrásaink (repülőjáratok, városok, utasok, poggyászok stb.) kódjait is kiegészítsük úgy, hogy legyen meg mindegyikhez a teljesértékű működés (index, show, create, store, edit, update, destroy).