Presenterek
Megismerkedünk azzal, hogyan írjunk presentereket és sablonokat a Nette-ben. Az olvasás után tudni fogja:
- hogyan működik a presenter
- mik azok a perzisztens paraméterek
- hogyan renderelődnek a sablonok
Már tudjuk, hogy a presenter egy olyan osztály, amely egy webalkalmazás egy konkrét oldalát képviseli, pl. a kezdőlapot; egy terméket a webáruházban; a bejelentkezési űrlapot; a sitemap feedet stb. Az alkalmazásnak egytől több ezer presenterig terjedhet a száma. Más keretrendszerekben kontrollereknek is nevezik őket.
Általában presenter alatt a Nette\Application\UI\Presenter osztály leszármazottját értjük, amely alkalmas webes felületek generálására, és amelynek a továbbiakban ebben a fejezetben szenteljük a figyelmet. Általános értelemben a presenter bármely objektum, amely implementálja a Nette\Application\IPresenter interfészt.
Presenter életciklusa
A presenter feladata a kérés feldolgozása és a válasz visszaadása (ami lehet HTML oldal, kép, átirányítás stb.).
Tehát az elején átadódik neki a kérés. Ez nem közvetlenül HTTP kérés, hanem egy Nette\Application\Request objektum, amelybe a HTTP kérés a router segítségével átalakításra került. Ezzel az objektummal általában nem találkozunk, mivel a presenter a kérés feldolgozását okosan delegálja további metódusokba, amelyeket most megmutatunk.
A kép felsorolja azokat a metódusokat, amelyek sorban fentről lefelé hívódnak meg, ha léteznek. Egyiknek sem kell léteznie, lehet teljesen üres presenterünk egyetlen metódus nélkül, és építhetünk rá egy egyszerű statikus weboldalt.
__construct()
A konstruktor nem igazán tartozik a presenter életciklusához, mert az objektum létrehozásának pillanatában hívódik meg. De a fontossága miatt említjük. A konstruktor (a inject metódussal együtt) a függőségek átadására szolgál.
A presenternek nem kellene az alkalmazás üzleti logikáját intéznie, adatbázisból írni és olvasni, számításokat
végezni stb. Erre valók a modellnek nevezett réteg osztályai. Például az ArticleRepository
osztály felelhet a
cikkek betöltéséért és mentéséért. Hogy a presenter dolgozhasson vele, dependency injection segítségével kéri át:
class ArticlePresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private ArticleRepository $articles,
) {
}
}
startup()
A kérés kézhezvétele után azonnal meghívódik a startup()
metódus. Használhatja
property-k inicializálására, felhasználói jogosultságok ellenőrzésére stb. Kötelező, hogy a metódus mindig meghívja
az ős parent::startup()
metódusát.
action<Action>(args...)
A render<View>()
metódus megfelelője. Míg a render<View>()
arra szolgál, hogy
előkészítse az adatokat egy konkrét sablonhoz, amely aztán renderelődik, addig az action<Action>()
a
kérést dolgozza fel a sablon renderelésétől függetlenül. Például feldolgozza az adatokat, bejelentkezteti vagy
kijelentkezteti a felhasználót, és így tovább, majd átirányít máshová.
Fontos, hogy az action<Action>()
korábban hívódik meg, mint a render<View>()
, így
benne esetleg megváltoztathatjuk a további történéseket, azaz megváltoztathatjuk a renderelendő sablont, és a meghívandó
render<View>()
metódust is. Ezt a setView('jineView')
segítségével tehetjük meg.
A metódusnak a kérésből származó paraméterek adódnak át. Lehetséges és ajánlott a paraméterek típusának
megadása, pl. actionShow(int $id, ?string $slug = null)
– ha az id
paraméter hiányzik, vagy ha nem
integer, a presenter 404-es hibát ad vissza és befejezi a működését.
handle<Signal>(args...)
A metódus az ún. signálokat dolgozza fel, amelyekkel a komponenseknek szentelt fejezetben ismerkedünk meg. Ugyanis főként komponensekhez és AJAX kérések feldolgozásához készült.
A metódusnak a kérésből származó paraméterek adódnak át, mint az action<Action>()
esetében,
beleértve a típusellenőrzést is.
beforeRender()
A beforeRender
metódus, ahogy a neve is sugallja, minden render<View>()
metódus előtt
hívódik meg. A sablon közös konfigurálására, a layout változóinak átadására és hasonló dolgokra használják.
render<View>(args...)
Az a hely, ahol előkészítjük a sablont a későbbi renderelésre, adatokat adunk át neki stb.
A metódusnak a kérésből származó paraméterek adódnak át, mint az action<Action>()
esetében,
beleértve a típusellenőrzést is.
public function renderShow(int $id): void
{
// adatokat szerzünk a modellből és átadjuk a sablonnak
$this->template->article = $this->articles->getById($id);
}
afterRender()
Az afterRender
metódus, ahogy a neve ismét sugallja, minden render<View>()
metódus után
hívódik meg. Ritkábban használják.
shutdown()
A presenter életciklusának végén hívódik meg.
Jó tanács, mielőtt továbbmennénk. A presenter, mint látható, több akciót/view-t is kezelhet, tehát több
render<View>()
metódusa lehet. De javasoljuk olyan presenterek tervezését, amelyeknek egy vagy a lehető
legkevesebb akciója van.
Válasz küldése
A presenter válasza általában egy HTML oldalt tartalmazó sablon renderelése, de lehet fájlküldés, JSON, vagy akár átirányítás egy másik oldalra is.
Az életciklus bármely pontján elküldhetünk választ a következő metódusok valamelyikével, és ezzel egyidejűleg befejezhetjük a presentert:
redirect()
,redirectPermanent()
,redirectUrl()
ésforward()
átirányíterror()
befejezi a presentert hiba miattsendJson($data)
befejezi a presentert és adatokat küld JSON formátumbansendTemplate()
befejezi a presentert és azonnal rendereli a sablontsendResponse($response)
befejezi a presentert és saját választ küldterminate()
befejezi a presentert válasz nélkül
Ha egyiket sem hívja meg ezek közül a metódusok közül, a presenter automatikusan a sablon rendereléséhez fog hozzá. Miért? Mert az esetek 99%-ában sablont szeretnénk renderelni, ezért a presenter ezt a viselkedést veszi alapértelmezettnek, és meg akarja könnyíteni a munkánkat.
Linkek létrehozása
A presenter rendelkezik a link()
metódussal, amellyel URL linkeket lehet létrehozni más presenterekhez. Az
első paraméter a cél presenter & akció, ezt követik az átadott argumentumok, amelyek tömbként is megadhatók:
$url = $this->link('Product:show', $id);
$url = $this->link('Product:show', [$id, 'lang' => 'hu']);
A sablonban a linkek más presenterekhez & akciókhoz a következőképpen hozhatók létre:
<a n:href="Product:show $id">termék részletei</a>
Egyszerűen a valós URL helyett írja be az ismert Presenter:action
párt, és adja meg az esetleges
paramétereket. A trükk az n:href
-ben van, amely azt mondja, hogy ezt az attribútumot a Latte dolgozza fel, és
valós URL-t generál. A Nette-ben tehát egyáltalán nem kell az URL-eken gondolkodnia, csak a presentereken és akciókon.
További információkat az URL linkek létrehozása fejezetben talál.
Átirányítás
Másik presenterhez való átlépéshez a redirect()
és forward()
metódusok szolgálnak, amelyeknek
nagyon hasonló a szintaxisa, mint a link() metódusnak.
A forward()
metódus azonnal átlép az új presenterhez HTTP átirányítás nélkül:
$this->forward('Product:show');
Példa az ún. ideiglenes átirányításra 302-es HTTP kóddal (vagy 303-mal, ha az aktuális kérés metódusa POST):
$this->redirect('Product:show', $id);
Állandó átirányítást 301-es HTTP kóddal így érhet el:
$this->redirectPermanent('Product:show', $id);
Más, alkalmazáson kívüli URL-re a redirectUrl()
metódussal lehet átirányítani. Második paraméterként
megadható a HTTP kód, az alapértelmezett 302 (vagy 303, ha az aktuális kérés metódusa POST):
$this->redirectUrl('https://nette.org');
Az átirányítás azonnal befejezi a presenter működését az ún. csendes befejező kivétel, a
Nette\Application\AbortException
dobásával.
Az átirányítás előtt küldhetünk flash message-t, azaz üzeneteket, amelyek az átirányítás után megjelennek a sablonban.
Flash üzenetek
Ezek általában valamilyen művelet eredményéről tájékoztató üzenetek. A flash üzenetek fontos jellemzője, hogy a sablonban átirányítás után is elérhetők. Megjelenítésük után még további 30 másodpercig élnek – például arra az esetre, ha a felhasználó hibás átvitel miatt frissítené az oldalt – az üzenet tehát nem tűnik el azonnal.
Csak meg kell hívni a flashMessage()
metódust, és a sablonba való átadásról a presenter gondoskodik. Az első paraméter az üzenet szövege, a nem kötelező
második paraméter pedig a típusa (error, warning, info stb.). A flashMessage()
metódus visszaadja a flash üzenet
példányát, amelyhez további információkat lehet hozzáadni.
$this->flashMessage('Az elem törölve lett.');
$this->redirect(/* ... */); // és átirányítunk
A sablonban ezek az üzenetek a $flashes
változóban érhetők el stdClass
objektumokként, amelyek
tartalmazzák a message
(üzenet szövege), type
(üzenet típusa) tulajdonságokat, és
tartalmazhatják a már említett felhasználói információkat is. Például így rendereljük őket:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Hiba 404 és társai
Ha a kérést nem lehet teljesíteni, például azért, mert a megjeleníteni kívánt cikk nem létezik az adatbázisban,
404-es hibát dobunk az error(?string $message = null, int $httpCode = 404)
metódussal.
public function renderShow(int $id): void
{
$article = $this->articles->getById($id);
if (!$article) {
$this->error();
}
// ...
}
A hiba HTTP kódját második paraméterként lehet átadni, az alapértelmezett 404. A metódus úgy működik, hogy
Nette\Application\BadRequestException
kivételt dob, mire az Application
átadja a vezérlést az
error-presenternek. Ez egy olyan presenter, amelynek feladata a bekövetkezett hibáról tájékoztató oldal megjelenítése. Az
error-presenter beállítása az application konfigurációban
történik.
JSON küldése
Példa egy action-metódusra, amely adatokat küld JSON formátumban és befejezi a presentert:
public function actionData(): void
{
$data = ['hello' => 'nette'];
$this->sendJson($data);
}
Kérés paraméterei
A presenter és minden komponens is megkapja a paramétereit a HTTP kérésből. Értéküket a
getParameter($name)
vagy getParameters()
metódussal tudhatja meg. Az értékek stringek vagy string
tömbök, lényegében nyers adatok, amelyeket közvetlenül az URL-ből nyerünk.
A nagyobb kényelem érdekében javasoljuk a paraméterek property-ken keresztüli elérhetővé tételét. Csak meg kell
őket jelölni a #[Parameter]
attribútummal:
use Nette\Application\Attributes\Parameter; // ez a sor fontos
class HomePresenter extends Nette\Application\UI\Presenter
{
#[Parameter]
public string $theme; // public-nak kell lennie
}
A property-nél javasoljuk az adattípus megadását (pl. string
), és a Nette ez alapján automatikusan
átalakítja az értéket. A paraméterek értékeit lehet validálni is.
Link létrehozásakor a paraméterek értékét közvetlenül be lehet állítani:
<a n:href="Home:default theme: dark">kattints</a>
Perzisztens paraméterek
A perzisztens paraméterek az állapot megőrzésére szolgálnak a különböző kérések között. Értékük ugyanaz marad
a linkre kattintás után is. A session adatokkal ellentétben az URL-ben kerülnek átvitelre. És ez teljesen automatikusan
történik, tehát nem kell explicit módon megadni őket a link()
vagy n:href
esetén.
Példa a használatra? Van egy többnyelvű alkalmazása. Az aktuális nyelv egy paraméter, amelynek folyamatosan az URL
részének kell lennie. De hihetetlenül fárasztó lenne minden linkben megadni. Így csinál belőle egy lang
perzisztens paramétert, és magától átadódik. Remek!
Perzisztens paraméter létrehozása a Nette-ben rendkívül egyszerű. Csak létre kell hozni egy public property-t és
megjelölni egy attribútummal: (korábban a /** @persistent */
volt használatos)
use Nette\Application\Attributes\Persistent; // ez a sor fontos
class ProductPresenter extends Nette\Application\UI\Presenter
{
#[Persistent]
public string $lang; // public-nak kell lennie
}
Ha a $this->lang
értéke például 'en'
lesz, akkor a link()
vagy
n:href
segítségével létrehozott linkek is tartalmazni fogják a lang=en
paramétert. És a linkre
kattintás után ismét $this->lang = 'en'
lesz.
A property-nél javasoljuk az adattípus megadását (pl. string
), és megadhat alapértelmezett értéket is.
A paraméterek értékeit lehet validálni.
A perzisztens paraméterek alapértelmezés szerint az adott presenter összes akciója között átadódnak. Ahhoz, hogy több presenter között is átadódjanak, definiálni kell őket vagy:
- egy közös ősben, amelytől a presenterek örökölnek
- egy trait-ben, amelyet a presenterek használnak:
trait LanguageAware
{
#[Persistent]
public string $lang;
}
class ProductPresenter extends Nette\Application\UI\Presenter
{
use LanguageAware;
}
Link létrehozásakor a perzisztens paraméter értékét meg lehet változtatni:
<a n:href="Product:show $id, lang: hu">részletek magyarul</a>
Vagy resetelhető, azaz eltávolítható az URL-ből. Ekkor az alapértelmezett értékét veszi fel:
<a n:href="Product:show $id, lang: null">kattints</a>
Interaktív komponensek
A presenterek beépített komponensrendszerrel rendelkeznek. A komponensek önálló, újrafelhasználható egységek, amelyeket presenterekbe illesztünk be. Lehetnek űrlapok, datagrid-ek, menük, valójában bármi, amit érdemes ismételten használni.
Hogyan illesztjük be és használjuk a komponenseket a presenterben? Ezt a Komponensek fejezetben tudhatja meg. Még azt is megtudhatja, mi közük van Hollywoodhoz.
És hol szerezhetek komponenseket? A Componette oldalon talál nyílt forráskódú komponenseket és számos más kiegészítőt a Nette-hez, amelyeket a keretrendszer körüli közösség önkéntesei helyeztek el itt.
Mélyebbre megyünk
Azzal, amit eddig ebben a fejezetben megmutattunk, valószínűleg teljesen elboldogul. A következő sorok azoknak szólnak, akiket mélyebben érdekelnek a presenterek, és mindent tudni akarnak róluk.
Paraméterek validálása
Az URL-ből kapott kérés paramétereinek és perzisztens paramétereinek értékeit a loadState()
metódus írja be a
property-kbe. Ez ellenőrzi azt is, hogy megfelelnek-e a property-nél megadott adattípusnak, különben 404-es hibával
válaszol, és az oldal nem jelenik meg.
Soha ne bízzon vakon a paraméterekben, mert azokat a felhasználó könnyen felülírhatja az URL-ben. Így például
ellenőrizzük, hogy a $this->lang
nyelv a támogatottak között van-e. Megfelelő módszer az említett
loadState()
metódus felülírása:
class ProductPresenter extends Nette\Application\UI\Presenter
{
#[Persistent]
public string $lang;
public function loadState(array $params): void
{
parent::loadState($params); // itt állítódik be a $this->lang
// következik a saját értékellenőrzés:
if (!in_array($this->lang, ['en', 'hu'])) {
$this->error();
}
}
}
Kérés mentése és visszaállítása
A presenter által kezelt kérés egy Nette\Application\Request objektum, és a
presenter getRequest()
metódusa adja vissza.
Az aktuális kérést el lehet menteni a sessionbe, vagy onnan visszaállítani, és hagyni, hogy a presenter újra
végrehajtsa. Ez hasznos például olyan helyzetben, amikor a felhasználó egy űrlapot tölt ki, és lejár a bejelentkezése.
Hogy ne veszítse el az adatokat, a bejelentkezési oldalra való átirányítás előtt az aktuális kérést elmentjük a
sessionbe a $reqId = $this->storeRequest()
segítségével, amely visszaadja annak azonosítóját egy rövid
string formájában, és ezt átadjuk paraméterként a bejelentkezési presenternek.
Bejelentkezés után meghívjuk a $this->restoreRequest($reqId)
metódust, amely kiemeli a kérést a
sessionből és forwardol rá. A metódus közben ellenőrzi, hogy a kérést ugyanaz a felhasználó hozta-e létre, aki most
bejelentkezett. Ha másik felhasználó jelentkezett be, vagy a kulcs érvénytelen, nem csinál semmit, és a program
folytatódik tovább.
Nézze meg a Hogyan térjünk vissza egy korábbi oldalra útmutatót.
Kanonizáció
A presentereknek van egy igazán nagyszerű tulajdonsága, amely hozzájárul a jobb SEO-hoz (keresőoptimalizálás).
Automatikusan megakadályozzák a duplikált tartalom létezését különböző URL-eken. Ha egy bizonyos célhoz több URL cím
vezet, pl. /index
és /index?page=1
, a keretrendszer egyiküket elsődlegesnek (kanonikusnak) határozza
meg, a többit pedig 301-es HTTP kóddal átirányítja rá. Ennek köszönhetően a keresőmotorok nem indexelik kétszer az
oldalakat, és nem osztják meg a page rankjüket.
Ezt a folyamatot kanonizációnak nevezik. A kanonikus URL az, amelyet a router generál, általában tehát az első megfelelő route a gyűjteményben.
A kanonizáció alapértelmezés szerint be van kapcsolva, és kikapcsolható a
$this->autoCanonicalize = false
segítségével.
Az átirányítás nem történik meg AJAX vagy POST kérés esetén, mert adatvesztéshez vezetne, vagy nem lenne hozzáadott értéke SEO szempontból.
A kanonizációt manuálisan is kiválthatja a canonicalize()
metódussal, amelynek hasonlóan a
link()
metódushoz, átadódik a presenter, az akció és a paraméterek. Létrehoz egy linket, és összehasonlítja
az aktuális URL címmel. Ha különböznek, átirányít a generált linkre.
public function actionShow(int $id, ?string $slug = null): void
{
$realSlug = $this->facade->getSlugForId($id);
// átirányít, ha a $slug különbözik a $realSlug-tól
$this->canonicalize('Product:show', [$id, $realSlug]);
}
Események
A startup()
, beforeRender()
és shutdown()
metódusokon kívül, amelyek a presenter
életciklusának részeként hívódnak meg, definiálhatunk további függvényeket is, amelyeket automatikusan meg kell hívni.
A presenter definiálja az ún. eseményeket, amelyek handlereit
hozzáadhatja a $onStartup
, $onRender
és $onShutdown
tömbökhöz.
class ArticlePresenter extends Nette\Application\UI\Presenter
{
public function __construct()
{
$this->onStartup[] = function () {
// ...
};
}
}
A $onStartup
tömb handlerei közvetlenül a startup()
metódus előtt hívódnak meg, továbbá a
$onRender
a beforeRender()
és render<View>()
között, végül a
$onShutdown
közvetlenül a shutdown()
előtt.
Válaszok
A presenter által visszaadott válasz egy objektum, amely implementálja a Nette\Application\Response interfészt. Számos előkészített válasz áll rendelkezésre:
- Nette\Application\Responses\CallbackResponse – callback-et küld
- Nette\Application\Responses\FileResponse – fájlt küld
- Nette\Application\Responses\ForwardResponse – forward()
- Nette\Application\Responses\JsonResponse – JSON-t küld
- Nette\Application\Responses\RedirectResponse – átirányítás
- Nette\Application\Responses\TextResponse – szöveget küld
- Nette\Application\Responses\VoidResponse – üres válasz
A válaszokat a sendResponse()
metódussal küldjük el:
use Nette\Application\Responses;
// Egyszerű szöveg
$this->sendResponse(new Responses\TextResponse('Hello Nette!'));
// Fájlt küld
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));
// A válasz egy callback lesz
$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) {
if ($httpResponse->getHeader('Content-Type') === 'text/html') {
echo '<h1>Hello</h1>';
}
};
$this->sendResponse(new Responses\CallbackResponse($callback));
Hozzáférés korlátozása
#[Requires]
segítségével
A #[Requires]
attribútum fejlett lehetőségeket kínál a presenterekhez és metódusaikhoz való hozzáférés
korlátozására. Használható HTTP metódusok specifikálására, AJAX kérés megkövetelésére, azonos eredetre (same origin)
való korlátozásra, és csak forwardoláson keresztüli hozzáférésre. Az attribútum alkalmazható mind a presenter
osztályokra, mind az egyes action<Action>()
, render<View>()
,
handle<Signal>()
és createComponent<Name>()
metódusokra.
Meghatározhatja ezeket a korlátozásokat:
- HTTP metódusokra:
#[Requires(methods: ['GET', 'POST'])]
- AJAX kérés megkövetelése:
#[Requires(ajax: true)]
- csak azonos eredetű hozzáférés:
#[Requires(sameOrigin: true)]
- csak forwardon keresztüli hozzáférés:
#[Requires(forward: true)]
- korlátozás konkrét akciókra:
#[Requires(actions: 'default')]
Részleteket a Hogyan használjuk a Requires attribútumot útmutatóban talál.
HTTP metódus ellenőrzése
A Nette presenterei automatikusan ellenőrzik minden bejövő kérés HTTP metódusát. Ennek az ellenőrzésnek az oka
elsősorban a biztonság. Alapértelmezés szerint a GET
, POST
, HEAD
, PUT
,
DELETE
, PATCH
metódusok engedélyezettek.
Ha további metódust szeretne engedélyezni, például az OPTIONS
-t, használja a #[Requires]
attribútumot (Nette Application v3.2-től):
#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}
A 3.1-es verzióban az ellenőrzés a checkHttpMethod()
-ban történik, amely megállapítja, hogy a kérésben
megadott metódus szerepel-e a $presenter->allowedMethods
tömbben. Metódus hozzáadása így történik:
class MyPresenter extends Nette\Application\UI\Presenter
{
protected function checkHttpMethod(): void
{
$this->allowedMethods[] = 'OPTIONS';
parent::checkHttpMethod();
}
}
Fontos hangsúlyozni, hogy ha engedélyezi az OPTIONS
metódust, azt követően megfelelően kezelnie is kell a
presenterében. A metódust gyakran használják ún. preflight kérésként, amelyet a böngésző automatikusan küld a
tényleges kérés előtt, amikor meg kell állapítani, hogy a kérés engedélyezett-e a CORS (Cross-Origin Resource Sharing)
politika szempontjából. Ha engedélyezi a metódust, de nem implementálja a megfelelő választ, az inkonzisztenciákhoz és
potenciális biztonsági problémákhoz vezethet.