Presentery

Seznámíme se s tím, jak se v Nette píší presentery a šablony. Po přečtení budete vědět:

  • jak funguje presenter
  • co jsou persistentní parametry
  • jak se kreslí šablony

Už víme, že presenter je třída, která představuje nějakou konkrétní stránku webové aplikace, např. homepage; produkt v e-shopu; přihlašovací formulář; sitemap feed atd. Aplikace může mít od jednoho po tisíce presenterů. V jiných frameworcích se jim také říká controllery.

Obvykle se pod pojmem presenter myslí potomek třídy Nette\Application\UI\Presenter, který je vhodný pro generování webových rozhraní a kterému se budeme věnovat ve zbytku této kapitoly. V obecném smyslu je presenter jakýkoliv objekt implementující rozhraní Nette\Application\IPresenter.

Životní cyklus presenteru

Úkolem presenteru je vyřídit požadavek a vrátit odpověď (což může být HTML stránka, obrázek, přesměrování atd.).

Tedy na počátku je mu předán požadavek. Není to přímo HTTP požadavek, ale objekt Nette\Application\Request, do kterého byl HTTP požadavek přetransformován za pomoci routeru. S tímto objektem obvykle nepřijdeme do styku, neboť presenter zpracování požadavku chytře deleguje do dalších metod, které si teď ukážeme.

Životní cyklus presenteru

Obrázek představuje seznam metod, které se postupně od shora dolů volají, pokud existují. Žádná z nich existovat nemusí, můžeme mít úplně prázdný presenter bez jediné metody a postavit na něm jednoduchý statický web.

__construct()

Konstruktor nepatří tak úplně do životního cyklu presenteru, protože se volá v okamžiku vytváření objektu. Ale uvádíme jej kvůli důležitosti, kvůli předávání závislostí.

Presenter by neměl obstarávat byznys logiku aplikace, zapisovat a číst z databáze, provádět výpočty atd. Od toho jsou třídy z vrstvy, kterou označujeme jako model. Například třída ArticleRepository může mít na starosti načítání a ukládání článků. Aby s ní mohl presenter pracovat, nechá si ji předat pomocí dependency injection:

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	/** @var ArticleRepository */
	private $articles;

	public function __construct(ArticleRepository $articles)
	{
		$this->articles = $articles;
	}
}

startup()

Ihned po obdržení požadavku se zavolá metoda startup(). Můžete ji využít k inicializaci properties, ověření uživatelských oprávnění atd. Je vyžadováno, aby metoda vždy volala předka parent::startup().

action<Action>(args...)

Obdoba metody render<View>(). Zatímco render<View>() je určená k tomu, aby připravila data pro konkrétní šablonu, která se následně vykreslí, tak v action<Action>() se zpracovává požadavek bez návaznosti na vykreslování šablony. Například se zpracují data, přihlásí či odhlásí uživatel, a tak podobně, a poté přesměruje jinam.

Důležité je, že action<Action>() se volá dříve než render<View>(), takže v ní můžeme případně změnit další běh dějin, tj. změnit šablonu, která se bude kreslit, a také metodu render<View>(), která se bude volat. A to pomocí setView('jineView').

Metodě se předávají parametry z požadavku. Je možné a doporučené uvést parametrům typy, např. actionShow(int $id, string $slug = null) – pokud bude parametr id chybět nebo pokud nebude integer, presenter vrátí chybu 404 a ukončí činnost.

handle<Signal>(args...)

Metoda zpracovává tzv. signály, se kterými se seznámíme v kapitole věnované komponentám. Je totiž určena zejména pro komponenty a zpracování AJAXových požadavků.

Metodě se předávají parametry z požadavku, jako v případě action<Action>(), včetně typové kontroly.

beforeRender()

Metoda beforeRender, jak už název napovídá, se volá před každou metodou render<View>(). Používá se pro společnou konfiguraci šablony, předání proměnných pro layout a podobně.

render<View>(args...)

Místo, kde připravujeme šablonu k následnému vykreslení, předáváme jí data atd.

Metodě se předávají parametry z požadavku, jako v případě action<Action>(), včetně typové kontroly.

public function renderShow(int $id): void
{
	// získáme data z modelu a předáme do šablony
	$this->template->article = $this->articles->getById($id);
}

afterRender()

Metoda afterRender, jak název opět napovídá, se volá za každou metodou render<View>(). Používá se spíš výjimečně.

shutdown()

Volá se na konci životního cyklu presenteru.

Dobrá rada, než půjdeme dál. Presenter jak vidno může obsluhovat více akcí/view, tedy mít více metod render<View>(). Ale doporučujeme navrhovat presentery s jednou nebo co nejméně akcemi.

Odeslání odpovědi

Odpovědí presenteru může být HTML stránka, odeslání souboru, JSON nebo třeba přesměrování na jinou stránku.

Kdykoliv během životního cyklu můžeme některou z následujících metod odeslat odpověď, čímž se presenter ukončí:

  • redirect(), redirectPermanent(), redirectUrl() a forward() přesměruje
  • error() ukončí presenter kvůli chybě
  • sendJson($data) presenter ukončí a odešle data ve formátu JSON
  • sendTemplate() presenter ukončí a ihned vykreslí šablonu
  • sendResponse($response) presenter ukončí a odešle vlastní odpověď
  • terminate() presenter ukončí bez odpovědi

Příklad odeslání JSONu:

public function actionData(): void
{
	$data = ['hello' => 'nette'];
	$this->sendJson($data);
}

Teď něco důležitého: pokud explicitně neřekneme, co má presenter odpovědět, bude odpovědí vykreslení šablony s HTML stránkou. Proč? Protože v 99 % případů chceme vykreslit šablonu, tudíž presenter tohle chování bere jako výchozí a chce nám ulehčit práci.

Vykreslení šablony

Nette používá šablonovací systém Latte. Jednak proto, že jde o nejlépe zabezpečený šablonovací systém pro PHP, a zároveň také systém nejintuitivnější. Nemusíte se učit mnoho nového, vystačíte si se znalostí PHP a několika značek.

Je obvyklé, že stránka se složí ze šablony layoutu + šablony dané akce. Takhle třeba může vypadat šablona layoutu, všimněte si bloků {block} a značky {include}:

<!DOCTYPE html>
<html>
<head>
	<title>{block title}My App{/block}</title>
</head>
<body>
	<header>...</header>
	{include content}
	<footer>...</footer>
</body>
</html>

A tohle bude šablona akce:

{block title}Homepage{/block}

{block content}
<h1>Homepage</h1>
....
{/block}

Ta definuje blok content, který se vloží na místo {include content} v layoutu, a také re-definuje blok title, kterým přepíše {block title} v layoutu. Zkuste si představit výsledek.

Cestu k šablonám odvodí presenter podle jednoduché logiky. Třeba u presenteru Product a akce show zkusí, zda existuje jeden z těchto souborů umístěných relativně od adresáře s třídou presenteru:

  • templates/Product/show.latte
  • templates/Product.show.latte

Pokud šablonu nenajde, je odpovědí chyba 404.

Taktéž se pokusí dohledat layout:

  • templates/Product/@layout.latte
  • templates/Product.@layout.latte
  • templates/@layout.latte layout společný pro více presenterů

Proměnné do šablony předáváme tak, že je zapisujeme do $this->template. Třeba takto vytvoříme proměnnou $article:

$this->template->article = $this->articles->getById($id);

Presentery a komponenty předávají do šablon několik užitečných proměnných automaticky:

  • $basePath je absolutní URL cesta ke kořenovému adresáři (např. /eshop)
  • $baseUrl je absolutní URL ke kořenovému adresáři (např. http://localhost/eshop)
  • $user je objekt reprezentující uživatele
  • $presenter je aktuální presenter
  • $control je aktuální komponenta nebo presenter
  • $flashes pole zpráv zaslaných funkcí flashMessage()

Vytváření odkazů

Presenter disponuje metodou link(), pomocí které lze vytvářet URL odkazy na další presentery. Prvním parametrem je cílový presenter & akce, následují předávané argumenty, které mohou být uvedeny jako pole:

$url = $this->link('Product:show', $productId);

$url = $this->link('Product:show', [$productId, 'lang' => 'cs']);

V šabloně se vytvářejí odkazy na další presentery & akce tímto způsobem:

<a n:href="Product:show $productId">detail produktu</a>

Prostě místo reálného URL napíšete známý pár Presenter:action a uvedete případné parametry. Trik je v tom n:href, které říká, že tento atribut zpracuje Latte a vygeneruje reálné URL. V Nette tak vůbec nemusíte uvažovat nad URL, jen nad presentery a akcemi.

Více informací najdete v kapitole Vytváření odkazů URL.

Přesměrování

K přechodu na jiný presenter slouží metody redirect() a forward(), které mají velmi podobnou syntax jako metoda link().

Metoda forward() přejde na nový presenter okamžitě bez HTTP přesměrování:

$this->forward('Product:show');

Příklad tzv. dočasného přesměrování s HTTP kódem 302 nebo 303:

$this->redirect('Product:show', $id);

Permanentní přesměrování s HTTP kódem 301 docílíte takto:

$this->redirectPermanent('Product:show', $id);

Na jinou URL mimo aplikaci lze přesměrovat metodou redirectUrl():

$this->redirectUrl('https://nette.org');

Přesměrování okamžitě ukončí činnost presenteru vyhozením tzv. tiché ukončovací výjimky Nette\Application\AbortException.

Před přesměrováním lze odeslat flash message, tedy zprávy, které budou po přesměrování zobrazeny v šabloně.

Flash zprávy

Jde o zprávy obvykle informující o výsledku nějaké operace. Důležitým rysem flash zpráv je to, že jsou v šabloně k dispozici i po přesměrování. I po zobrazení zůstanou živé ještě další 3 sekundy – například pro případ, že by z důvodu chybného přenosu uživatel dal stránku obnovit – zpráva mu tedy hned nezmizí.

Stačí zavolat metodu flashMessage() a o předání do šablony se postará presenter. Prvním parametrem je text zprávy a nepovinným druhým parametrem její typ (error, warning, info apod.). Metoda flashMessage() vrací instanci flash zprávy, které je možné přidávat další informace.

$this->flashMessage('Položka byla smazána.');
$this->redirect(...); // a přesměrujeme

Šabloně jsou tyto zprávy k dispozici v proměnné $flashes jako objekty stdClass, které obsahují vlastnosti message (text zprávy), type (typ zprávy) a mohou obsahovat již zmíněné uživatelské informace. Vykreslíme je třeba takto:

{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}

Chyba 404 a spol.

Pokud nelze splnit požadavek, třeba z důvodu, že článek který chceme zobrazit neexistuje v databázi, vyhodíme chybu 404 metodou error(string $message = null, int $httpCode = 404).

public function renderShow(int $id): void
{
	$article = $this->articles->getById($id);
	if (!$article) {
		$this->error();
	}
	...
}

HTTP kód chyby lze předat jako druhý parametr, výchozí je 404. Metoda funguje tak, že vyhodí výjimku Nette\Application\BadRequestException, načež Application předá řízení error-presenteru. Což je presenter, jehož úkolem je zobrazit stránku informující o nastalé chybě. Nastavení error-preseteru se provádí v konfiguraci application.

Persistentní parametry

Persistentní parametry se v odkazech přenášejí automaticky. To znamená, že je nemusíme explicitně uvádět v každém volání link() nebo n:href v šabloně, ale přesto se přenesou.

Pokud má vaše aplikace více jazykových mutací, tak aktuální jazyk je parameter, který musí být neustále součástí URL. A bylo by neskutečně únavné ho v každém odkazu uvádět. To není s Nette potřeba. Prostě si parametr lang označíme jako persistentní tímto způsobem:

class ProductPresenter extends Nette\Application\UI\Presenter
{
	/** @persistent */
	public $lang;

Pokud aktuální hodnota parametru lang bude 'en', tak URL vytvořené pomocí link() nebo n:href v šabloně bude obsahovat lang=en. Paráda!

Při vytváření odkazu nicméně lze persistentní parametr uvést a tím změnit jeho hodnotu:

<a n:href="Product:show $productId, lang => cs">detail v češtině</a>

Persistentní proměnná musí být deklarovaná jako public. Můžeme uvést i výchozí hodnotu. Bude-li mít parametr hodnotu stejnou jako výchozí, nebude v URL obsažen.

Persistence zohledňuje hierarchii tříd presenterů, tedy parametr definovaný v určitém presenteru nebo traitě je poté automaticky přenášen do každého presenteru z něj dědícího nebo užívajícího stejnou traitu.

Komponenty

Presentery v sobě mají zabudovaný komponentový systém. Komponenty jsou samostatné znovupoužitelné celky, které vkládáme do presenterů. Mohou to být formuláře, datagridy, menu, vlastně cokoliv, co má smysl používat opakovaně.

Jak se do presenteru komponenty vkládají a následně používají? To se dozvíte v kapitole Komponenty.

A kde mohu získat komponenty? Na stránce Componette najdete open-source komponenty a také řadu dalších doplňku pro Nette, které sem umístili dobrovolníci z komunity okolo frameworku.

Jdeme do hloubky

S tím, co jsme si dosud v této kapitole ukázali, si nejspíš úplně vystačíte. Následující řádky jsou určeny těm, kdo se zajímají o presentery do hloubky a chtějí vědět úplně všechno.

Požadavek a parametry

Požadavek, který vyřizuje presenter, je objekt Nette\Application\Request a vrací ho metoda presenteru getRequest(). Jeho součástí je pole parametrů a každý z nich patří buď některé z komponent, nebo přímo presenteru (což je vlastně také komponenta, byť speciální). Nette tedy parametry přerozdělí a předá mezi jednotlivé komponenty (a presenter) zavoláním metody loadState(array $params), což dále popisujeme v kapitole Komponenty. Získat parametry lze metodu getParameters(): array, jednotlivě pomocí getParameter($name). Hodnoty parametrů jsou řetězce či pole řetězců, jde v podstatě o surové data získané přímo z URL.

Uložení a obnovení požadavku

Aktuální požadavek lze uložit do session nebo naopak z ní obnovit a nechat jej presenter znovu vykonat. To se hodí například v situaci, když uživatel vyplňuje formulář a vyprší mu přihlášení. Aby o data nepřišel, před přesměrováním na přihlašovací stránku aktuální požadavek uložíme do session pomocí $reqId = $this->storeRequest(), které vrátí jeho identifikátor v podobě krátkého řetězce a ten předáme jako parameter přihlašovacímu presenteru.

Po přihlášení zavoláme metodu $this->restoreRequest($reqId), která požadavek vyzvedne ze session a forwarduje na něj. Metoda přitom ověří, že požadavek vytvořil stejný uživatel, jako se nyní přihlásil. Pokud by se přihlásil jiný uživatel nebo klíč byl neplatný, neudělá nic a program pokračuje dál.

Kanonizace

Presentery mají jednu opravdu skvělou vlastnost, která přispívá k lepšímu SEO (optimalizaci nalezitelnosti na internetu). Automaticky zabraňují existenci duplicitního obsahu na různých URL. Pokud k určitému cíli vede více URL adres, např. /index a /index?page=1, framework určí jednu z nich za primární (kanonickou) a ostatní na ni přesměruje pomocí HTTP kódu 301. Díky tomu vám vyhledávače stránky neindexují dvakrát a nerozmělní jejich page rank.

Tomuto procesu se říká kanonizace. Kanonickou URL je ta, kterou vygeneruje router, zpravidla tedy první odpovídající routa v kolekci.

Kanonizace je ve výchozím nastavení zapnutá a lze ji vypnout přes $this->autoCanonicalize = false.

K přesměrování nedojde při AJAXovém nebo POST požadavku, protože by došlo ke ztrátě dat nebo by to nemělo přidanou hodnotu z hlediska SEO.

Kanonizaci můžete vyvolat i manuálně pomocí metody canonicalize(), které se podobně jako metodě link() předá presenter, akce a parametry. Vyrobí odkaz a porovná ho s aktuální URL adresou. Pokud se liší, tak přesměruje na vygenerovaný odkaz.

public function actionShow(int $id, string $slug = null): void
{
	$realSlug = $this->facade->getSlugForId($id);
	// přesměruje, pokud $slug se liší od $realSlug
	$this->canonicalize('Product:show', [$id, $realSlug]);
}

Události

Kromě metod startup(), beforeRender() a shutdown(), které se volají jako součást životního cyklu presenteru, lze definovat ještě další funkce, které se mají automaticky zavolat. Presenter definuje tzv. událost, jejichž handlery přidáte do polí $onStartup, $onRender a $onShutdown.

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct()
	{
		$this->onStartup[] = function () {
			// ...
		};
	}
}

Handlery v poli $onStartup se volají těsně před metodou startup(), dále $onRender mezi beforeRender() a render<View>() a nakonec $onShutdown těsně před shutdown().

Odpovědi

Odpověď, kterou vrací presenter, je objekt implementující rozhraní Nette\Application\IResponse. K dispozici je řada připravených odpovědí:

Odpovědi se odesílají metodou sendResponse():

use Nette\Application\Responses;

// Prostý text
$this->sendResponse(new Responses\TextResponse('Hello Nette!'));

// Odešle soubor
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));

// Odpovědí bude callback
$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));

Šablony

Místa, kde se dohledávají šablony, lze změnit přepsáním metod formatTemplateFiles nebo formatLayoutTemplateFiles, které vrací pole názvů souborů.

Layout se dohledává v souborech s názvem @layout.latte. To můžete změnit pomocí setLayout('necoJineho'), pak bude hledat soubory @necoJineho.latte. Nebo pomocí setLayout(false) dohledávání layoutu vypnout.

Odkazy generované pomocí link() nebo n:href jsou vždy absolutní cesty, ale nikoliv absolutní URL s protokolem a doménou http://domain. Aby presenter generoval absolutní URL, je třeba nastavit $this->absoluteUrls = true.


Související články na blogu