Komplex példa 1. rész - adatok lekérése és megjelenítése

Attila | 2022. 03. 15. 10:11 | Olvasási idő: 5 perc

Címkék: #Adatbázis (Database) #Blade #Controller #Eloquent #HTML #Laravel #Laravel 9 #MVC #MySQL #Nézet (View) #PHP #Routing #Tinker

Ebben a blogbejegyzésben megkezdjük egy kompex, több elemből álló webalkalmazás összeállítását. A projekt bonyolultsága abban rejlik, hogy az MVC tervezési minta részeit és kapcsolatait gyakorlati oldalról fogjuk megismerni és kipróbálni. Elsőként az adatok lekérését, majd megjelenítését hajtjuk végre.
laravel-komplex-pelda-1

A repülőjáratos (flights) példánkból indulunk ki és ezt fogjuk építgetni úgy, hogy közben rengeteg mindennel fogunk megismerkedni. Természetesen a nézeteknél összeállított sablonunkat is fel fogjuk használni, szóval, aki elkezdte a 9-es verziójú Laravel projekt összeállítását, az felhasználhatja az ott meglévő dolgainkat (illetve az a projekt fejlesztése is folytatható az itteniekkel). Akinek esetleg a fentiek hiányoznának, az itt tudja pótolni:

  1. Adatmodell, migráció, példák az adatbázisban: Flight Model és kapcsolódó migrációs fájlja, valamint adatbázis oldalról az adattáblája és a sorai (kód részről tehát ezek kellenek: app/Models/Flight.php és database/migrations/[időbélyeg]_create_flights_table.php; míg adatbázis oldalról a flights adattábla és benne a példasorok)
  2. Nézetek: Sablon integráció

Ha ezeket végigcsináltuk, akkor a következő állapotúnak kell lennie a projektünknek: Github commit. A flights adattáblám tartalma pedig így néz ki:

Nyilván, ha valaki több adatsort, vagy másféle járatszámokkal vitt fel adatokat a táblába, akkor neki egy picit különbözhet a táblája tartalma, de ez a munkánk során nem lényeges, tekintsünk ezekre úgy, mint csak példaadatok.


Adatlekérés és korlátozott megjelenítés

Ez tehát a kezdő állapot és ha az adataink megvannak, akkor meg kell jelenítenünk az adatainkat a weboldalon. Először azonban ne valamelyik nézetünkbe illesszük be őket, hanem az útvonal regisztrációs fájlunkban (routes/web.php) vizsgáljuk meg az adatlekérésüket és simán jelenítsük meg őket egy gyűjteményben. Mivel a Tinker-t már használtuk, amiről ugye azt írtam, hogy programozottan tudjuk megszólítani az alkalmazásunk aktuális állapotát, ez azért is hasznos nekünk, mert az ott írt kódok a tényleges programozói környezetben is használhatóak!

Route::get('/flights', function () {
  $flights = App\Models\Flight::get();
  return $flights;
});

Ha ezt a kódot beírjuk és leteszteljük a böngészőnkben a http://127.0.0.1:8000/flights webcímet, akkor ugye egy JSON típusú eredményt kapunk vissza (nálam) három elemmel a gyűjteményben, ami ugyanazt mutatja, mintha Tinker-ben simán kiadtuk volna az App\Models\Flight::get(); utasítást.

Ez így rendben is van, de a lehető legritkábban kell az összes repülőjáratot ilyen formában visszaadnunk a felhasználónak: a get() függvény előtt lekérhető a take() és paramétereként, hogy hány járatot szeretnénk megkapni:

$flights = App\Models\Flight::take(2)->get();

Így már nem három, csak két repülőjárat fog látszódni. Amire mindenképpen szeretném felhívni a figyelmet: a Model osztály neve után két kettősponttal (::) statikus metódus hívása történik meg, amiről annyit érdemes tudni, hogy (ez az Objektumorientált Programozás témaköréhez kapcsolódik) ilyen statikus metódusok akkor hívhatók is meghívhatók, ha az osztályból nem hoztunk létre példányt. Viszont a további metódushívások már nyíllal (->) vannak hozzáfűzve az osztály nevéhez. A fenti utasításban tehát nincs szó példányosításról, csak a Model osztály segítségével lekérünk az adatbázisból kettő darab adatsort, amit visszaadunk egy gyűjteményként és ezt kapja meg a $flights változónk (PHP-ban ugye van ilyen szabadságunk, hogy egy változónak sokféle dolgot eredményül adhatunk), itt most egy gyűjteményt adtunk meg neki, ami listaként vagy tömbként is felfogható és kezelhető úgy.

Ha például csak az aktív repülőjáratokból szeretnék kettőt lekérni, akkor így tehetném meg (ismét felhívom a figyelmet rá: az osztály neve után kettőspontokkal történik meg a where metódus hívása, majd a további metódusok hozzáfűzése már nyilakkal):

App\Models\Flight::where('active', 1)->take(2)->get();

Bővítsük ezt még egy kicsit, mert előfordulhat például, hogy az adattáblánk tele van adatokkal és lapozást (pagination) kell beletenni, hogy egyszerre a listának csak része legyen látható. Ez gyakorlatilag ilyen könnyen megvalósítható:

$flights = App\Models\Flight::paginate(2);

Ha most frissítjük a böngészőnket (http://127.0.0.1:8000/flights), akkor láthatjuk, hogy az összes (nálam 3 darab) repülőjárat kilistázásra került, azonban további adattagokat is tartalmaz a visszakapott gyűjtemény, azon túl, hogy az adatbázisban lévő adatok bekerülnek a gyűjtemény data nevű adattagjába, lesz nekünk még itt current_page (aktuális oldal: 1), from (oldalszámtól: 1-től), to (oldalszámig: 2-ig), total (összesen 3), per_page (ennyi darab repülőjárat jelenjen meg oldalanként, amit mi kettőre állítottunk), sőt még előre/hátra léptető linkekkel is el van látva ez a gyűjtemény. Mindössze egyetlen utasítás hozzáfűzésével tehát egy kvázi teljeskörű lapozhatóságot adtunk hozzá a gyűjteményünkhöz, amelyet ezekután már a nézetben könnyebben meg tudunk jeleníteni. Ezeket mind a Laravel szolgáltatja nekünk, nem kell ismernünk hozzá a belső működését. Itt látható a gyűjteményről egy pillanatkép:

Mielőtt azonban hozzáadnánk egy nézetnek az adatokat, próbáljunk ki még néhány dolgot: valószínűleg, ha a repülőjáratok érdekelnek minket, akkor először a legutóbbiakat szeretnénk látni és nem mindig az elsőtől szeretnénk indulni, érdemes emiatt módosítani a lekérdezést:

$flights = App\Models\Flight::latest('created_at')->get();

Azon persze elgondolkodhatunk, hogy a latest() metódusnak melyik mezőt adjuk paraméteréül, én most itt az adattábla 'created_at' mezőjét adtam át, mert tudom, hogy ebben biztosan vannak adatok, amelyek mentén "időben csökkenő sorrendben" tudja mutatni az adatokat a visszaadott gyűjteményben. Ha a latest() metódust paraméter nélkül hagyom, akkor az id mező szerint fogja csökkenő sorrendben mutatni az adatokat. A későbbiekben persze megváltozathatjuk ezt aszerint, hogy mondjuk a járatokat felszállás vagy a leszállás ideje szerint szeretnénk mutatni, mindez csak rajtunk múlik és egyszerűen tudjuk módosítani.


Adatátküldés és dinamikus megjelenítés

Az útvonal végpontunk végső formája legyen ez a tesztelések után (zárójeles részek, kiegészítve magyarázatokkal) és az adatátadással a nézetnek:

Route::get('/flights', function () {
  // $flights = App\Models\Flight::get(); // MySQL-es megfelelő: SELECT * FROM flights;
  // $flights = App\Models\Flight::where('active', 1)->take(2)->get(); // SELECT * FROM flights WHERE active = 1 LIMIT 2;
  // $flights = App\Models\Flight::paginate(2); // lapozhatóság: 2 repülőgépjárat egy lapon
  // $flights = App\Models\Flight::latest('created_at')->get(); // SELECT * FROM flights ORDER BY created_at DESC;
  // return $flights;

  return view('flights', [
    'flights' => App\Models\Flight::latest()->get()
  ]);        
});

Ezzel tehát id szerint csökkenő sorrendben átadtuk az összes repülőjáratunkat a nézetnek, de a flights nézetünk még nem létezik. Még mielőtt ezt létrehoznánk, a layout.blade.php fájlban bővítsük a menünk szerkezetét egy "Flights" nevű menüponttal (ugyanúgy, ahogy korábban a Contact menüponttal tettük), hogy könnyebben elérhessük majd az új nézetünket:

Ha már itt voltam, akkor az előző két menüpont szövegét és változtattam magyarra (Kezdőoldal, Kapcsolat). A többi menüpontot pedig töröltem, hiszen azok úgysem kellenek nekünk a továbbiakban. Emellett nyilván más módosítások is elvégezhetünk a sablonként használt layout fájlunkban, ezt mindenkinek a saját ízlésére, belátására bízom.

Hozzuk ezt létre a flights nézetet resources/views/flights.blade.php helyen és néven. Tartalmát első körben kimásolhatjuk a contact.blade.php fájlból, mivel ugyanúgy a layout-ot szeretnénk majd kiterjeszteni, illetve annak a 'content' szekciójába helyezzük el dinamikusan az adatmegjelenítésünket.

Bővítsük a 4. sor után egy felsorolással, alatta az eredménye:

Látható, hogy a sima, "pöttyös" felsorolás (ul-li) nem sima felsorolás, hanem a sablonunk szerinti kinézetet kapta meg. Az látható, hogy a járatszám linkje még nem vezet sehova. Ennek megoldásával fogjuk folytatni a munkát, ahol Controller-t és wildcard karaktert is fogunk használni hozzá. De mindenekelőtt az útvonalat regisztráljuk a webalkalmazásunkba (routes/web.php):

Route::get('/flights/{flight}',[App\Http\Controllers\FlightsController::class, 'show']);

Láthatjuk, hogy hosszú elérési úttal hivatkozunk a FlightsController osztályra. Ha esetleg többször is használnánk ezt az osztályt, akkor érdemesebb a fájl tetején importálni és akkor utána már elég csupán FlightsController-ként hivatkozni rá.

Hozzuk létre a FlightsController-t:

php artisan make:controller FlightsController

Magán a FlightsController-ben a show() metódust. Megjegyzés: VSCode-ban ha beírjuk a "fun"-t, majd TAB-ot nyomunk, akkor legenerálja nekünk a metódus sablonját, így TAB-bal tudunk lépkedni a metódus neve, paraméterei és magja között.

public function show($id)
{
  dd($id);
}

A Laravel biztosít egy eszközt a routing-on keresztül: ha a metódusnak beállítunk egy paramétert, akkor az a böngészőbe beírt útvonaltól érkező utolsó helyen szereplő azonosítót fogja megadni, de nézzük ezt meg a példánkban és nyissuk meg például ezt az útvonalat: http://127.0.0.1:8000/flights/1 Ez ki is írja nekünk a dd (dump&die) funkcióval az id-t, ami a wildcard karakter volt az útvonal regisztrálásánál. Módosítsuk a fenti metódus magját és töröljük ki a dd() metódushívást:

$flight = Flight::findOrFail($id);
return view('flights.show', ['flight' => $flight]);

Ahhoz, hogy ez működjön, az osztály tetején importálni kell a Flight Model-t:

use App\Models\Flight;

A findOrFail() metódussal megkeressük az id-nak megfelelő adatsort az adatbázisban, szerencsére ez már azt is biztosítja, ha mondjuk olyan repülőjáratot akarunk lekérni, ami nem szerepel az adatbázis táblában, mondjuk a 999-est, akkor nem Exception-t, hanem 404-es hibakódot kapunk. A második sorban a nézetet adjuk vissza a view() metódus első paraméterében, ahol újdonságként a nézet nevénél észrevehetjük, hogy ponttal elválasztjuk a könyvtárat és a fájlt egymástól. Ez annyit jelent, hogy a resources/views mappában fogja továbbra is keresni a nézetet a metódus, de ezen belül nem csak a sima show nevű fájlt, hanem a flights mappán belüli show.blade.php-t. A view() metódus második paramétere a már ismert adatátküldést tartalmazza. Mivel ez a tömb ugyanazokat a neveket tartalmazza, ezért itt használható lenne a compact('flight') metódus alkalmazása is.

A helyes működéshez a resources/views mappához adjuk hozzá a flights mappát, abba pedig a show.blade.php-t. A fájl tartalma itt látható:

Ez eddig rendben is van, működik, hogy ha az URL-t bekérem az 1, 2 vagy 3-as végződéssel. Egy kis javítás: nálam a 3. repülőjáratnak nincs beállítva ár, az 1. és 3.-nak pedig nincs neve (mivel ezek ugye nem voltak kötelező mezők a Flight migrációs fájl alapján). Kezeljük ezt a kis problémát Blade utasítások segítségével.

Így a repülőjárat nevét és árát csak akkor fogja kiírni, ha azok meg vannak adva az adattáblában. Ez így most már teljesen jól működik. Egy dolgot csináljunk még meg, a flights.blade.php -ban az egyes járatok linkjét irányítsuk rá arra az útvonalra, ami valóban behozza azok részleteit.

Látható, hogy a # helyére beírtuk az új útvonalunkat úgy, hogy a wildcard helyre a repülőjárat id azonosítóját írtuk bele.

Ha most kipróbáljuk az oldalunkat és kattintgatunk, teszteljük, akkor tudjuk listázni a repülőjáratokat és ki tudunk választani egyes járatokat is, hogy a részleteiket is megtudhassuk.

Gyakorlásként mindenki megpróbálhatja, hogy újraszervezi a kódját aszerint, hogy a repülőjáratok dolgait (útvonal, Controller, nézet) logikusan egységbe foglalja... de ha ez nem menne, akkor érdemes továbbolvasni, mert végigvezetem az olvasót ezen a feladaton.


Kódújraszervezés feladat megoldása

Egyetlen dolgot kellene még megcsinálnunk, ami a kódújraszervezéssel kapcsolatos. Azt kellene megtennünk, hogy a repülőjáratok kilistázása logikusabb legyen, vagyis jobban kövesse az MVC tervezési minta előírását. Jelenleg úgy működik, hogy a web.php fájlban a /flights útvonal segítségével ott helyben történik meg a Flight Model-en keresztüli adatlekérés, majd a nézetnek való adatátadás. Ez azonban így nem igazán jó, hiszen ezt is inkább a FlightsController-re szeretnénk rábízni, illetve a repülőjárat listanézetet is oda kellene tenni a show nevű nézet mellé. Kezdjük az útvonal regisztrációval.

A korábbi /flights útvonal részeit megjegyzésbe tettem és a show alá "újraregisztrálom" a /flights útvonalat, de ezúttal már a FlightsController index metódusára irányítom. Ez az index metódus azonban még nem létezik a FlightsController-ben, úgyhogy hozzuk létre:

public function index()
{
  return view('flights.index', [
    'flights' => Flight::latest()->get()
  ]);
}

Látható, hogy ez a resoruces/views mappában lévő flights mappában lévő index.blade.php -nek küldi át az adatokat, úgyhogy ezt a nevű fájlt hozzuk létre oda. A tartalmát pedig egy az egyben másoljuk át a korábbi resources/views/flights.blade.php -ból, amire már a későbbiekben nem lesz szükség, így törölhető lesz.

Ezután minden működik ugyanúgy és már a kódunk is eléggé hatékonyan, következetesen működik. Egy képernyőkép a működő oldalunkról:

A komplex példa első kódrészletei ezen a linken találhatók meg.