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.

Presenter életciklusa

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() és forward() átirányít
  • error() befejezi a presentert hiba miatt
  • sendJson($data) befejezi a presentert és adatokat küld JSON formátumban
  • sendTemplate() befejezi a presentert és azonnal rendereli a sablont
  • sendResponse($response) befejezi a presentert és saját választ küld
  • terminate() 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:

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.

További olvasmányok

verzió: 4.0