Presentery

Seznámíme se s tím, jak se vlastně v Nette Framework tvoří aplikace. Po přečtení budete znát:

  • co jsou to presentery a akce
  • jak se používají šablony
  • co jsou persistentní parametry

Presenter je objekt, který vezme požadavek přeložený routerem z HTTP požadavku a vygeneruje odpověď. Odpovědí může být HTML stránka, obrázek, XML dokument, soubor na disku, JSON, přesměrování nebo cokoliv vymyslíte.

Obvykle se pod pojmem presenter myslí potomek třídy Nette\Application\UI\Presenter. Podle příchozích požadavků spouští odpovídající akce a vykresluje šablony.

Životní cyklus presenteru

Už víme, že tzv. akce presenteru způsobí zavolání metody render<Akce>, tedy například renderShow. Ale to není zdaleka jediná metoda, která se volá. Při psaní presenterů můžeme mimo jiné vytvořit i následující metody:

Životní cyklus presenteru

startup()

Ihned po vytvoření presenteru se zavolá metoda startup(). Ta inicializuje proměnné nebo ověří uživatelská oprávnění.

Pokud si píšete vlastní metodu startup(), nezapomeňte zavolat předka parent::startup().

action<Action>()

Obdoba metody render<View>(). Jsou situace, kdy presenter provede určitý úkon (přihlásí uživatele, zapíše data do databáze) a poté přesměruje jinam. Dávat název render metodě, která nic nekreslí, by bylo nepěkné, proto existuje alternativa s názvem action.

Důležité je vědět, že action<Action>() se volá dříve, než metoda render<View>(). Může tedy i rozhodnout, aby se zavolala jiná metoda render pomocí příkazu $this->setView('jineView') (zavolá se renderJineView()).

handle<Signal>()

Metoda zpracovává tzv. signály neboli subrequesty. Určeno zejména pro komponenty a zpracování AJAXových požadavků. Na stejné úrovni se také provádí zpracování formulářů.

beforeRender()

Metoda beforeRender, jak už název napovídá, se volá před nám známou metodou render<View>() a může obsahovat například nastavení šablony, předání proměnných společných pro více view a podobně.

render<View>()

Obvykle nasype do šablony potřebná data.

shutdown()

Je vyvolána při ukončení životního cyklu presenteru.

Přesnější by bylo říci, že jsme si povídali o životním cyklu třídy Nette\Application\UI\Presenter, ze které se presentery dědí nejčastěji. Obecně je totiž presenter jakákoliv třída implementující triviální rozhraní Nette\Application\IPresenter.

Odeslání odpovědi

Presenter můžeme během jeho životního cyklu kdykoliv ukončit a odeslat jinou odpověď, než je výchozí vykreslení šablony.

  • presenter ukončíme metodou $presenter->terminate()
  • presenter ukončíme a hned vyrenderujeme šablonu: $presenter->sendTemplate()
  • presenter ukončíme a odešleme payload: $presenter->sendPayload() (pro AJAX)
  • presenter ukončíme a odešleme vlastní odpověď: $presenter->sendResponse($response)
  • presenter ukončíme a odešleme JSON: $presenter->sendJson($data)
  • presenter ukončíme a odešleme chybovou hlášku: $presenter->error($message, $httpCode)

Příklad odeslání JSONu:

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

Příklady použití metody sendResponse:

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

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

// Odpovědí bude callback
$this->sendResponse(new Nette\Application\Responses\CallbackResponse(function (Http\IRequest $httpRequest, Http\IResponse $httpResponse) {
	if ($httpResponse->getHeader('Content-Type') === 'text/html')) {
		echo '<h1>Hello</h1>';
	}
});

Presenter lze také ukončit přesměrováním či forwardem, viz dále.

Repozitář dalších odpovědí presenteru najdete na Componette.

Vykreslení šablony

Cestu k souboru se šablonou odvodí presenter podle jednoduché logiky. Pro presenter Product a akci show zkusí, zda existuje jeden z těchto souborů:

- templates/Product/show.latte
- templates/Product.show.latte

Taktéž se pokusí dohledat layout (ten je nepovinný):

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

Způsob dohledávání šablon můžeme změnit přepsáním metod formatTemplateFiles nebo formatLayoutTemplateFiles.

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

  • $basePath je absolutní URL cesta ke kořenovému adresáři (např. /CD-collection)
  • $baseUrl je absolutní URL ke kořenovému adresáři (např. http://localhost/CD-collection)
  • $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()

O vykreslování šablon podrobněji v samostatné kapitole.

Ono to vlastně není nic těžkého! Pokud požaduji akci například Homepage:default, tak se

  1. vytvoří objekt třídy HomepagePresenter
  2. zavolá se metoda renderDefault() (existuje-li, ale nemusí)
  3. vykreslí se šablona např. templates/Homepage/default.latte s layoutem např. templates/@layout.latte

a v šabloně pak můžeme vytvořit odkaz na zmíněný Product:show($id), zhruba takto:

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

Přesměrování

K přechodu na jiný presenter slouží metody redirect() a forward(), které mají velmi podobnou syntax jako zmíněná funkce link(). Například po odeslání formuláře a zápisu dat do databáze přesměrujeme na detail produktu zavoláním:

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

Zatímco forward() přejde na novou akci bez přesměrování, metoda redirect() přesměruje prohlížeč HTTP kódem 302 nebo 303. Takto přesměrujeme permanentně s kódem 301:

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

Na jinou URL mimo rozsah aplikace lze přesměrovat metodou redirectUrl()

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

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

Před přesměrováním si občas chceme odeslat tzv. flash message, tedy zprávu, která se objeví po přesměrování v šabloně.

Chyba 404 a spol.

Pokud nemůžeme splnit požadavek z důvodu, že například záznam neexistuje v databázi, vyhodíme metodou error() výjimku Nette\Application\BadRequestException, která představuje HTTP chybu 404:

public function renderShow($id)
{
	$product = $this->productRepository->getProduct($id);
	if (!$product) {
		$this->error();
	}

	// ...
}

Pokud uživatel nemá oprávnění stránku vidět, vyhodíme výjimku Nette\Application\ForbiddenRequestException (chyba 403). Další chybové kódy lze uvést jako kód výjimky Nette\Application\BadRequestException.

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.

public function deleteFormSubmitted()
{
	// ... požádáme model o smazání záznamu ...

	// předáme flash zprávu
	$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 anonymní objekty, 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}

Použití modelových tříd

Jak jsme si řekli, model je samostatná vrstva a je složen z hromady tříd, kde každá obstarává část logiky aplikace. Pokud bychom měli například model App\ArticleRepository, který se stará o načítání a ukládání článků, mohli bychom si o něj v presenteru říct pomocí Dependency Injection, za předpokladu že je registrovaný jako službaDI kontejneru.

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

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

	public function renderShow($id)
	{
		$this->template->article = $this->articles->find($id);
	}
}

Díky tomu pak můžu tuto službu hned používat v metodách presenteru a nemusím se starat o její vytváření, protože mám jistotu, že mi bude vždy předána, pokud ji správně zaregistruji do DI kontejneru.

Injektovat bez použití konstruktoru lze pouze do public vlastností tříd s použitím anotace @inject.

Presenter a komponenty

Bavíme-li se o presenterech, tak pod pojmem komponenty obvykle myslíme potomky třídy Control. Přesnější by tedy bylo používat termín „controls“ (tj. ovládací prvky), ale „kontrola“ má v češtině zcela jiný význam a spíš se ujaly „komponenty“.

Sám presenter Nette\Application\UI\Presenter je přitom potomkem třídy Control, takže je tu velká podobnost mezi komponentami a presenterem. Především však UI\Control (a tím pádem i UI\Presenter) je tzv. komponentový kontejner, což znamená, že do něj lze vkládat další komponenty. Podobně, jako třeba do komponenty formuláře vkládáme formulářové prvky (textové políčko, tlačítko, …). A stejně jako u formulářů lze k prvkům přistupovat přes hranaté závorky:

// připojíme komponentu do presenteru
$presenter['mymenu'] = new MenuControl;

// získáme komponentu z presenteru a vykreslíme
$presenter['mymenu']->render();

Připojením komponenty do presenteru (jejich svázáním) získáte možnost:

  • vytvářet v komponentě odkazy
  • používat signály
  • používat v komponentě persistentní parametry

Pokud nic z toho nepotřebujeme, není potřeba komponentu s presenterem vázat.

Továrny na komponenty

Továrna na komponenty představuje elegantní způsob, jak komponenty vytvářet teprve ve chvíli, kdy jsou skutečně potřeba (lazy / on demand). Celé kouzlo spočívá v implementaci metody s názvem createComponent<Name>(), kde <Name> je název vytvářené komponenty, a která komponentu vytvoří a vrátí. Komponenta je následně připojena k presenteru. Metodě createComponent<Name> je předáván volitelný parametr s názvem komponenty, kterou vytváří.

class DefaultPresenter extends Nette\Application\UI\Presenter
{
	public function renderDefault()
	{
		$menu = $this['menu']; // přistoupíme ke komponentě
		// a pokud to bylo poprvé, zavolá se createComponentMenu()
		// ...
	}

	protected function createComponentMenu()
	{
		// vytvoříme a nakonfigurujeme komponentu
		$menu = new MenuControl;
		$menu->items = $this->item;
		// a vrátíme ji
		return $menu;
	}
}

Názvy komponent začínají vždy malým písmenem, přestože se v názvu továrny píší s velkým.

Díky tomu, že jsou všechny komponenty vytvářeny v samostatné metodě, získává kód na přehlednosti.

Továrny nikdy nevoláme přímo, zavolá se sama ve chvíli, kdy komponentu poprvé použijeme. Díky tomu je komponenta vytvořena ve správný okamžik a pouze v případě, když je skutečně potřeba. Pokud komponentu nepoužijeme (třeba při AJAXovém požadavku, kdy se přenáší jen část stránky, nebo při cachování šablony), nevytvoří se vůbec a ušetříme výkon serveru.

V šabloně je možné získat a vykreslit komponentu pomocí {control}. Není proto potřeba manuálně komponenty předávat do šablony.

<h2>Editační formulář</h2>

{control editForm}

Podrobnější informace o komponentách najdete na samostatné stránce.

Persistentní parametry

Krom klasických parametrů, které jsme používali nyní, existují i tzv. persistentní parametry. Ty se liší v jediné avšak zásadní věci: přenášejí se automaticky. To znamená, že je nemusíme v odkazech explicitně uvádět, ale přesto se přenesou.

Pokud má vaše aplikace dvě jazykové mutace, bylo by neskutečně únavné v každém odkazu přenášet i aktuální jazyk. To není s Nette Framework potřeba. Prostě si parametr $lang označíme jako persistentní a to tímto způsobem:

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

	...

Pokud aktuální hodnota parametru $lang bude 'en', tak do odkazu

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

se automaticky doplní i lang => en. Paráda!

Samozřejmě můžeme lang uvést a jeho hodnotu změnit:

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

Zároveň v proměnné třídy $lang objektu ProductPresenter budeme mít hodnotu parametru k dispozici, můžeme k ní přistoupit přes $this->lang. Můžeme v definici třídy uvést i výchozí hodnotu persistentního parametru. Bude-li mít parametr tuto výchozí hodnotou, pak nebude přenášen v URL.

Persistentní proměnná musí být deklarovaná jako public.

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

Persistentní komponenty

Nejen parametry, ale také komponenty mohou být persistentní. Jejich stav se pak přenáší při přechodu na jiný presenter podobně, jako v případě persistentních parametrů. Persistentní komponenty značíme touto anotací: (zde značíme komponenty calendar a menu):

/**
 * @persistent(calendar, menu)
 */
class DefaultPresenter extends Nette\Application\UI\Presenter
{
	// ...
}

Podkomponenty uvnitř těchto komponent není třeba nijak značit, jsou persistentní samy o sobě.

Kde mohu získat komponenty?

Na stránce Doplňky, pluginy a komponenty můžete najít open-source komponenty, které sem umístili dobrovolníci z komunity okolo Nette Framework. Nette Foundation za ně neručí.

Konfigurace

Pomocí aplikační konfigurace lze ovlivnit některé chování aplikace. Zapisujeme ji do sekce application v našem konfiguračním souboru, viz konfigurace application.

Související články na blogu