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. Konstruktor (společně s metodou inject) slouží k 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
{
public function __construct(
private ArticleRepository $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 je zpravidla vykreslení šablony s HTML stránkou, ale může jí být také 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ěď a zároveň tak ukončit presenter:
redirect()
,redirectPermanent()
,redirectUrl()
aforward()
přesměrujeerror()
ukončí presenter kvůli chyběsendJson($data)
presenter ukončí a odešle data ve formátu JSONsendTemplate()
presenter ukončí a ihned vykreslí šablonusendResponse($response)
presenter ukončí a odešle vlastní odpověďterminate()
presenter ukončí bez odpovědi
Pokud žádnou z těchto metod nezavoláte, presenter automaticky přistoupí k vykreslí šablony. 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.
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', $id);
$url = $this->link('Product:show', [$id, 'lang' => 'cs']);
V šabloně se vytvářejí odkazy na další presentery & akce tímto způsobem:
<a n:href="Product:show $id">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, je-li metoda aktuálního požadavku POST):
$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()
. Jako druhý parametr lze uvést HTTP kód,
výchozí je 302 (nebo 303, je-li metoda aktuálního požadavku POST):
$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ších 30 sekund – 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.
Odeslání JSON
Příklad action-metody, která odešle data ve formátu JSON a ukončí presenter:
public function actionData(): void
{
$data = ['hello' => 'nette'];
$this->sendJson($data);
}
Parametry požadavku
Presenter a také každá komponenta získává z HTTP požadavku své parametry. Jejich hodnotu zjistíte metodou
getParameter($name)
nebo getParameters()
. Hodnoty jsou řetězce či pole řetězců, jde v podstatě
o surové data získané přímo z URL.
Pro větší pohodlí doporučujeme parametry zpřístupnit přes property. Stačí je označit atributem
#[Parameter]
:
use Nette\Application\Attributes\Parameter; // tento řádek je důležitý
class HomePresenter extends Nette\Application\UI\Presenter
{
#[Parameter]
public string $theme; // musí být public
}
U property doporučujeme uvádět i datový typ (např. string
) a Nette podle něj hodnotu automaticky
přetypuje. Hodnoty parametrů lze také validovat.
Při vytváření odkazu lze parametrům hodnotu přímo nastavit:
<a n:href="Home:default theme: dark">click</a>
Persistentní parametry
Persistentní parametry slouží k udržování stavu mezi různými požadavky. Jejich hodnota zůstává stejná i po
kliknutí na odkaz. Na rozdíl od dat v session se přenášejí v URL. A to zcela automaticky, není tedy potřeba je
explicitně uvádět v link()
nebo n:href
.
Příklad použití? Máte multijazyčnou aplikaci. Aktuální jazyk je parameter, který musí být neustále součástí URL.
Ale bylo by neskutečně únavné ho v každém odkazu uvádět. Tak z něj uděláte persistentní parametr lang
a
bude se přenášet sám. Paráda!
Vytvoření persistentního parametru je v Nette nesmírně jednoduché. Stačí vytvořit veřejnou property a označit ji
atributem: (dříve se používalo /** @persistent */
)
use Nette\Application\Attributes\Persistent; // tento řádek je důležitý
class ProductPresenter extends Nette\Application\UI\Presenter
{
#[Persistent]
public string $lang; // musí být public
}
Pokud bude $this->lang
mít hodnotu například 'en'
, tak i odkazy vytvořené pomocí
link()
nebo n:href
budou obsahovat parameter lang=en
. A po kliknutí na odkaz bude opět
$this->lang = 'en'
.
U property doporučujeme uvádět i datový typ (např. string
) a můžete uvést i výchozí hodnotu. Hodnoty
parametrů lze validovat.
Persistentní parametry se standardně přenášejí mezi všemi akcemi daného presenteru. Aby se přenášely i mezi více presentery, je potřeba je definovat buď:
- ve společném předkovi, od kterého presentery dědí
- v traitě, kterou presentery použijí:
trait LanguageAware
{
#[Persistent]
public string $lang;
}
class ProductPresenter extends Nette\Application\UI\Presenter
{
use LanguageAware;
}
Při vytváření odkazu lze persistentnímu parametru změnit hodnotu:
<a n:href="Product:show $id, lang: cs">detail v češtině</a>
Nebo jej lze vyresetovat, tj. odstranit z URL. Pak bude nabývat svou výchozí hodnotu:
<a n:href="Product:show $id, lang: null">klikni</a>
Interaktivní 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. Dokonce zjistíte, co mají společného s Hollywoodem.
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.
Validace parametrů
Hodnoty parametrů požadavku a persistentních
parametrů přijatých z URL zapisuje do properties metoda loadState()
. Ta také kontroluje, zda odpovídá
datový typ uvedený u property, jinak odpoví chybou 404 a stránka se nezobrazí.
Nikdy slepě nevěřte parametrům, protože mohou být snadno uživatelem přepsány v URL. Takto například ověříme, zda
je jazyk $this->lang
mezi podporovanými. Vhodnou cestou je přepsat zmíněnou metodu
loadState()
:
class ProductPresenter extends Nette\Application\UI\Presenter
{
#[Persistent]
public string $lang;
public function loadState(array $params): void
{
parent::loadState($params); // zde se nastaví $this->lang
// následuje vlastní kontrola hodnoty:
if (!in_array($this->lang, ['en', 'cs'])) {
$this->error();
}
}
}
Uložení a obnovení požadavku
Požadavek, který vyřizuje presenter, je objekt Nette\Application\Request a vrací ho metoda
presenteru getRequest()
.
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.
Podívejte se na návod Jak se vrátit k dřívější stránce.
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\Response. K dispozici je řada připravených odpovědí:
- Nette\Application\Responses\CallbackResponse – odešle callback
- Nette\Application\Responses\FileResponse – odešle soubor
- Nette\Application\Responses\ForwardResponse – forward()
- Nette\Application\Responses\JsonResponse – odešle JSON
- Nette\Application\Responses\RedirectResponse – přesměrování
- Nette\Application\Responses\TextResponse – odešle text
- Nette\Application\Responses\VoidResponse – prázdná odpověď
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));
Omezení přístupu pomocí #[Requires]
Atribut #[Requires]
poskytuje pokročilé možnosti pro omezení přístupu k presenterům a jejich metodám. Lze
jej použít pro specifikaci HTTP metod, vyžadování AJAXového požadavku, omezení na stejný původ (same origin), a
přístup pouze přes forwardování. Atribut lze aplikovat jak na třídy presenterů, tak na jednotlivé metody
action<Action>()
, render<View>()
, handle<Signal>()
a
createComponent<Name>()
.
Můžete určit tyto omezení:
- na HTTP metody:
#[Requires(methods: ['GET', 'POST'])]
- vyžadování AJAXového požadavku:
#[Requires(ajax: true)]
- přístup pouze ze stejného původu:
#[Requires(sameOrigin: true)]
- přístup pouze přes forward:
#[Requires(forward: true)]
- omezení na konkrétní akce:
#[Requires(actions: 'default')]
Podrobnosti najdete v návodu Jak používat atribut Requires.
Kontrola HTTP metody
Presentery v Nette automaticky ověřují HTTP metodu každého příchozího požadavku. Důvodem pro tuto kontrolu je
především bezpečnost. Standardně jsou povoleny metody GET
, POST
, HEAD
,
PUT
, DELETE
, PATCH
.
Chcete-li povolit navíc například metodu OPTIONS
, použijte k tomu atribut #[Requires]
(od Nette
Application v3.2):
#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}
Ve verzi 3.1 se ověření provádí v checkHttpMethod()
, která zjišťuje, zda je metoda specifikovaná
v požadavku obsažena v poli $presenter->allowedMethods
. Přidání metody udělejte takto:
class MyPresenter extends Nette\Application\UI\Presenter
{
protected function checkHttpMethod(): void
{
$this->allowedMethods[] = 'OPTIONS';
parent::checkHttpMethod();
}
}
Je důležité zdůraznit, že pokud povolíte metodu OPTIONS
, musíte ji následně také patřičně obsloužit
v rámci svého presenteru. Metoda je často používána jako tzv. preflight request, který prohlížeč automaticky odesílá
před skutečným požadavkem, když je potřeba zjistit, zda je požadavek povolený z hlediska CORS (Cross-Origin Resource
Sharing) politiky. Pokud metodu povolíte, ale neimplementujete správnou odpověď, může to vést k nekonzistencím a
potenciálním bezpečnostním problémům.