Presentery a šablony
Nyní si představíme presentery a vše, co s nimi souvisí. Ukážeme si, jak napsat vlastní presenter, jak psát v šablonovacím jazyce Latte a jak využívat model, který jsme již dříve napsali.
Na začátek se bohužel nevyhneme troše teorie. Nette využívá architekturu MVP, Model-View-Presenter. Základními kameny této architektury jsou:
- Model – datová a funkční vrstva aplikace, která se stará o ukládání dat a aplikační logiku. Jakoukoliv událost uživatele (přihlášení, zobrazení či změna dat, vložení zboží do košíku) představuje akci modelu. Ten má pevně dané rozhraní, pomocí kterého s ním ostatní části aplikace komunikují, a sám o svém okolí nic neví.
- View, nebo také „pohled“ – stará se o samotné vykreslení výsledku požadavku uživatele. V Nette tuto část představují šablony.
- Presenter – obě předchozí vrstvy spojuje dohromady. Nejprve na základě požadavku od uživatele vyvolá příslušnou aplikační logiku (např. zmíněné přidání zboží do košíku či zobrazení dat) a pak požádá view o vykreslení výsledku.
Architektura MVP je podobná architektuře MVC. Obě architektury se liší hlavně v úloze jejich centrálního kamene, tedy Presenter × Controller. Presenter hraje čistě roli prostředníka, který jen volá model a výsledky předává view, kdežto Controller má navíc na starosti i některé události uživatelského rozhraní.
Model již máme díky Nette Database připravený. Zbývá nám tedy presenter a view. V Nette má každý presenter vlastní sadu views, takže budeme obojí psát souběžně.
HomepagePresenter
Jak již bylo řečeno, presenter je třída, která spolupracuje s modelem a výsledná data předává view. Nejprve
upravíme HomepagePresenter. Ten bude zajišťovat zobrazení úvodní stránky aplikace. V kostře aplikace vypadá
kód této třídy následovně:
class HomepagePresenter extends BasePresenter
{
public function renderDefault(): void
{
$this->template->anyVariable = 'any value';
}
}
Má tedy jednu jedinou metodu: renderDefault(). Ta se stará o předání dat view, který se bude jmenovat
default. Šablonu tohoto view naleznete ve složce templates/Homepage/, konkrétně se jedná o soubor
default.latte. Při běhu aplikace se nejprve vykonají požadované akce v presenteru, až pak se podle toho
vykresluje šablona. Posloupnost těchto kroků zachycuje následující diagram:

V diagramu si povšimněte metody action<action>(). V souvislosti s ní si musíme zavést nový pojem, a
to „akce presenteru“. Nejlepší bude tento pojem vysvětlit na příkladu: akcí presenteru může být například
„zobrazení produktu“. Interně jí nazveme show. Pokud chceme ale produkt zobrazit, může dojít k několika
stavům: produkt je na skladě a můžeme jej objednat, produkt mohl být stažen z prodeje, vybraný produkt již nemusí
existovat… Právě pro tyto jednotlivé stavy pak vytvoříme vlastní view, můžeme je tedy chápat jako konkrétní realizace
zobrazení dané akce.
Ve většině případů bude platit, že co akce, to view, proto pokud se o nic nestaráme, vykonává se akce a view se
stejným jménem. V našem HomepagePresenteru by se tedy před metodou renderDefault() volala metoda
actionDefault(), pokud by existovala. Během ní se však můžeme rozhodnout view změnit voláním metody
setView($view), například $this->setView('error'). Pak by po vykonání metody
actionDefault() nedošlo k volání metody renderDefault(), ale renderError(). Také by se
vykreslovala šablona error.latte.
Nyní ale zpět k psaní kódu. Pokud chceme v šabloně nějaká data zobrazit, musíme presenter nějakým způsobem spojit s modelem. Model máme zaregistrovaný jako službu, takže se k němu i k jeho tabulkám můžeme pohodlně dostat.
Spojení s modelem
Protože jsme naše třídy modelu šikovně definovali jako služby, můžeme je okamžitě použít. Předáme si co
potřebujeme do property v metodě startup(). V configu máme třídu Todo\TaskRepository
zaregistrovanou jako službu se jménem taskRepository, takže se bude volat pomocí
$this->context->taskRepository.
// class HomepagePresenter
/** @var Todo\TaskRepository */
private $taskRepository;
protected function startup(): void
{
parent::startup();
$this->taskRepository = $this->context->taskRepository;
}
Budeme chtít zobrazit všechny nesplněné úkoly a seřadit vzestupně je podle času vytvoření. Vytvoříme si tedy metodu
na výběr nesplněných úkolů, ve třídě Todo\TaskRepository
// class TaskRepository
public function findIncomplete()
{
return $this->findBy(['done' => false])->order('created ASC');
}
A pomocí ní vybereme z tabulky task data a vložíme je do šablony.
// class HomepagePresenter
public function renderDefault(): void
{
$this->template->tasks = $this->taskRepository->findIncomplete();
}
Nette Database poskytuje pro dotazování do databáze takzvané „fluent interface“. Dotaz postupně skládáme zřetězením volání funkcí, které budou reprezentovat jednotlivé části dotazu. Podporovány jsou téměř všechny aspekty jazyka SQL. Tato abstrakce je poměrně užitečná – pokud později vyměníme SQL databázi za jiné úložiště, stačí pouze implementovat objekty se stejným rozhraním a funkčnost zůstane stejná. Toto rozhraní ukážeme později na složitějších dotazech.
Voláním where() nad objektem tabulky definujeme podmínku, podle které chceme záznamy vybírat. Můžeme jí
předat asociativní pole ve formátu sloupec → hodnota, případně rovnou kus SQL dotazu
s parametry pokud nám prosté vybírání podle rovnosti nestačí, nebo chceme použít složitější podmínky. Podobně
voláním order() zajistíme setřídění dat vzestupně podle sloupce created.
Důležitou vlastností těchto objektů je, že jednu instanci je možné použít pouze na jeden dotaz. Pokud bychom nad
jedním objektem zkoušeli sestavit dva dotazy, zamíchaly by se nám do druhého dotazu i části z toho prvního. Proto máme
taky třídy, které dědí od Todo\Repository, která vytváří vždy novou instanci Selection, pomocí metody Connection::table()
Zvídavé čtenáře opět odkazuji do dokumentace na téma Databáze & ORM.
Získaný objekt poté přiřadíme do šablony pomocí $this->template->tasks. Tím definujeme v šabloně
proměnnou $tasks, kterou můžeme používat. Takto můžeme šabloně předat prakticky libovolná data
v libovolné proměnné.
Šablona
V šabloně default.latte máme nyní k dispozici proměnnou $tasks. Pojďme ji tedy využít.
V šabloně je nyní uvítací stránka, tu můžeme bez obav smazat a nahradit jí vlastním kódem:
{block content}
<h1>Nesplněné úkoly</h1>
<table>
<thead>
<tr>
<th>Čas vytvoření</th>
<th>Úkol</th>
<th>Přiřazeno</th>
</tr>
</thead>
<tbody>
{foreach $tasks as $task}
<tr>
<td>{$task->created|date:'j. n. Y'}</td>
<td>{$task->text}</td>
<td>{$task->user->name}</td>
</tr>
{/foreach}
</tbody>
</table>
{/block}
Ve vypisování šablony nám pomáhají takzvaná makra. Ta jsou uvedena ve složených závorkách a zastávají funkci různých jazykových konstrukcí. Šablony můžeme psát i bez nich, ale proč bychom to dělali, že?
Seznam všech maker je samozřejmě v dokumentaci: Výchozí Latte makra.
{block content} a {/block} definuje obsahový blok. Nette používá takzvanou dědičnost šablon.
Hlavní šablona definuje bloky, které pak zděděné šablony mohou (a některé musí) přepsat. Pokud není uvedeno jinak, tak
šablona view dědí od šablony @layout.latte, kterou naleznete přímo ve složce app/templates.
V ní se nachází řádek:
{include content}
Ten říká, že místo něj se má vložit blok s názvem content. Protože blok jinde definován není, je
nutné tento blok definovat ve zděděné šabloně. Hned pod hlavičkou je uveden {block head}{/block}. Takový blok
je možno přepsat, ale není to nutné. Pokud přepsán nebude, použije se místo něj výchozí obsah (zde prázdný
řetězec).
Dále máme v bloku klasickou tabulku. Ta obsahuje <thead> a <tbody> tak, jak jsme
zvyklí. Uvnitř <tbody> je makro {foreach}. To se chová jako klasický foreach
v PHP. Prochází proměnnou $tasks a položky ukládá do proměnné $task. V ní máme od
databázové vrstvy jednotlivé sloupce. Všimněte si, že při výpisu se nemusíme starat o escapování pomocí
htmlspecialchars(). Nette se o escapování postará samo. Dokonce escapování provádí v závislosti na
kontextu – v bloku JavaScriptu bude používat json_encode(), v HTML htmlspecialchars()…
Podívejme se hned na první sloupeček s časem:
<td>{$task->created|date:'j. n. Y'}</td>
Konstrukcí za svislicí | zajistíme, že před výpisem proměnné se na ní aplikuje tzv. helper. Helpery jsou
malé funkce, které nám pomáhají jednoduše formátovat výpisy v šabloně. Helper date vypíše zadané datum
v nějakém přívětivějším formátu. Tento formát je mu zadán jako parametr za dvojtečkou. Specifikátor může být
stejný, jako v případě funkce date(), případně strftime().
Přehled všech helperů, které jsou k dispozici, je k nahlédnutí v dokumentaci v kapitole Helpery.
Další zajímavostí je konstrukce $task->user->name. Jak již bylo řešeno v předchozí části
věnované databázi, sloupec user_id považuje databázová vrstva za odkaz na tabulku user. Díky tomu
se pak můžeme takto odkázat na tabulku user.
Nyní si již můžeme výsledek zobrazit:

Povšimněte si panelu vpravo dole. Tento panel Nette zapíná jen ve vývojovém prostředí a zobrazují se na něm ladící informace. Po připojení k databázi nám v panelu přibyla nový položka – položené dotazy. Najetím na ní se nám zobrazí seznam všech dotazů, které byly pro vygenerování stránky použity. Dotaz si můžeme ihned nechat „vysvětlit“ (explain).

Nyní se ale vrhneme na další presenter.
TaskPresenter
Tato třída již v kostře není. Musíme si jí tedy vytvořit. Ve složce app/presenters vytvořte nový
soubor TaskPresenter.php. Umístíme do ní prozatím prázdnou třídu, která bude dědit z
BasePresenteru:
/**
* Presenter, který zajišťuje výpis seznamů úkolů.
*/
class TaskPresenter extends BasePresenter
{
}
BasePresenter je abstraktní společný předek všech presenterů v aplikaci. Můžeme v něm například
provádět inicializaci, načítat data, která mají být společná pro všechny presentery a další zajímavé věci.
// class TaskPresenter
/** @var @var Todo\ListRepository */
private $listRepository;
protected function startup(): void
{
parent::startup();
$this->listRepository = $this->context->listRepository;
}
V TaskPresenteru budeme chtít zobrazit jeden konkrétní seznam úkolů. Seznam, který chceme zobrazit, dostane
presenter jako ID v adrese. Vytvoříme si tedy další privátní property $list, do kterého uložíme v metodě
actionDefault záznam o seznamu úkolů:
// class TaskPresenter
/** @var Nette\Database\Table\ActiveRow */
private $list;
public function actionDefault(int $id): void
{
$this->list = $this->listRepository->find($id);
}
Parametr $id bude obsahovat identifikátor, který bude hledán v databázi. Nette si samo zkontroluje
action<action>() a render<view>() metody a pokud mají nějaké argumenty, které přišly
adresou, tak je předá podle jména. Více o tomto mechanismu se dozvíme v sekci o presenterech, prozatím se
spokojíme s, pro programátora nejlepším závěrem, „funguje to a nemusíme se o to starat“.
Ve třídě Todo\ListRepository si vytvoříme metodu, která nám z objektu ActiveRow |api:Nette\Database\Table\ActiveRow
, obsahujícího řádek z tabulky list, vrátí všechny související úkoly.
// class ListRepository
public function tasksOf(Nette\Database\Table\ActiveRow $list)
{
return $list->related('task')->order('created');
}
Získaný záznam spolu se seznamem úkolů předáme šabloně v metodě renderDefault():
// class TaskPresenter
public function renderDefault(): void
{
$this->template->list = $this->list;
$this->template->tasks = $this->listRepository->tasksOf($this->list);
}
Šablona
K view nám zbývá napsat jen šablonu. Ve složce templates vytvoříme podsložku Task a do ní
šablonu default.latte. Bude velmi podobná té předchozí:
Při vytváření šablon záleží na velikost písmen. Složka s názvem presenteru by měla začínat velkým písmenem, soubor se šablonou malým. Při vývoji na Windows to nepoznáme, ale pokud aplikaci přeneseme na linuxový server, způsobí to řadu chyb.
{block content}
<h1>{$list->title}</h1>
<table>
<thead>
<tr>
<th>Čas vytvoření</th>
<th>Úkol</th>
<th>Přiřazeno</th>
</tr>
</thead>
<tbody>
{foreach $tasks as $task}
<tr>
<td>{$task->created|date:'j. n. Y'}</td>
<td>{$task->text}</td>
<td>{$task->user->name}</td>
</tr>
{/foreach}
</tbody>
</table>
{/block}
Nyní už bude vše fungovat! Počkat… ale jak se k nově vytvořené stránce dostat? Možná by bylo dobré si ještě vytvořit menu s výpisem všech seznamů úkolů…
Navigace
Menu bude společné pro všechny části aplikace. Žhavý kandidát na umístění do BasePresenteru! Budeme jej
muset vykreslit na každé stránce, takže použijeme metodu beforeRender(), která se provede vždy, nezávisle na
view, které má být vykreslováno. Nejprve si ale Todo\ListRepository umístíme do privátní property, abychom
s modelem mohli pracovat.
// class BasePresenter
/** @var Todo\ListRepository */
private $listRepository;
protected function startup(): void
{
parent::startup();
$this->listRepository = $this->context->listRepository;
}
protected function beforeRender(): void
{
$this->template->lists = $this->listRepository->findAll()->order('title ASC');
}
Ještě musíme zajistit, aby se menu zobrazilo na každé stránce. Protože každá šablona view dědí od šablony
@layout.latte, umístíme vykreslení právě do ní. Připravíme si rovnou základ pro nějaké rozumné
rozvržení. Obsah tagu <body> upravíme následovně:
<div id="header">
<div id="header-inner">
<div class="title"><a href="{link Homepage:}">Úkolníček</a></div>
</div>
</div>
<div id="container">
<div id="sidebar">
<div class="task-lists">
<ul>
<li n:foreach="$lists as $list"><a href="{link Task: $list->id}">{$list->title}</a></li>
</ul>
</div>
</div>
<div id="content">
<div n:foreach="$flashes as $flash" class="flash {$flash->type}">{$flash->message}</div>
{include content}
</div>
<div id="footer">
</div>
</div>
Atribut n:foreach je druhý způsob, jak iterovat polem. Tento zápis je ekvivalentní, jako kdyby byl
foreach umístěn kolem HTML tagu, ve kterém je uveden. Podobně funguje většina maker, která řídí tok
programu: if, for, while… Pokud před makro napíšeme prefix inner-, bude
se chovat, jako kdyby bylo napsáno uvnitř HTML tagu. Uvedený zápis bychom tedy mohli poupravit takto:
<ul n:inner-foreach="$lists as $list">
<li><a href="{link Task: $list->id}">{$list->title}</a></li>
</ul>
Výsledek by byl nezměněn.
Zajímavější pro nás však bude nové makro {link Task: $list->id}. To vytváří URL adresu. Jako první
parametr je cílová dvojice Presenter:action. Pokud action neuvedeme a za dvojtečku nic nenapíšeme,
použije se výchozí, tedy default. Pokud se chceme odkazovat na jinou akci ve stejném presenteru, nemusíme presenter uvádět.
Pak nesmí být uvedena ani dvojtečka, která má speciální význam.
Jako další jsou uvedeny parametry, které se mají v adrese předat. Pokud je nepojmenujeme, tak je musíme uvést ve
stejném pořadí, v jakém jsou uvedeny v signatuře akce (tedy metody action<action>(), případně
view<action>(), pokud předchozí neexistuje). Parametry oddělujeme čárkou a pokud je chceme pojmenovat,
uděláme to stejně jako v případě klíčů pole. Výše uvedený zápis je tedy ekvivalentní s ukecanějším
<li><a href="{link Task: id: $list->id}">{$list->title}</a></li>
Pokud si chceme zápis ještě více usnadnit, existuje ještě makro n:href:
<li><a n:href="Task: $list->id">{$list->title}</a></li>
Nyní bychom již na stránce měli vidět jednoduché menu. Kliknutím se dostaneme na výpis konkrétního seznamu úkolů.

Podívejme se na adresu, kterou Nette pro odkazy vytvořilo: /task/default/2. Jak je vidět, v adrese je uveden
presenter, akce i zadané ID. Tvar těchto adres lze plně ovlivnit pomocí routování. Opět bez jakýchkoliv zásahů do
jiných částí aplikace.
Pojďme si trošku zaexperimentovat. Co se stane, když změníme ID na neexistující? Chyba!

Ukázala se nám laděnka v celé své kráse. Jedná se o jeden z nejužitečnějších nástrojů v Nette. Ukazuje, kde přesně k chybě došlo a zachycuje stav aplikace v době chyby. Pokud by chyba byla v SQL dotazu, ukáže nám i problematický dotaz.
Chyba Argument 1 passed to Todo\ListRepository::tasksOf() must be
an instance of Nette\Database\Table\ActiveRow, boolean given nám říká, že jsme se pokusili zavolat metodu
Todo\ListRepository::tasksOf() s argumentem, co není objekt. Pokud funkce fetch() nenalezne žádný
výsledek, vrátí false. To můžeme v TaskPresenteru snadno ověřit a zareagovat na to
změnou view:
public function actionDefault(int $id): void
{
$this->list = $this->listRepository->find($id);
if ($this->list === false) {
$this->setView('notFound');
}
}
Pro tento view poté musíme vytvořit šablonu notFound.latte ve složce app/templates/Task:
{status 404}
{block content}
<h1>Seznam úkolů nenalezen</h1>
<p>Litujeme, ale Vámi požadovaný seznam úkolů nebyl nalezen.</p>
Makrem {status 404} nastavujeme HTTP status na 404: Nenalezeno. Povšimněte si, že v šabloně chybí
ukončovací makro pro {block content} – u posledního bloku v šabloně můžeme uzavírací část
vynechat.
Pokud zkusíme opět přistoupit na neexistující ID, bude situace o poznání lepší:

Povšimněte si, že jsme nikde nevytvářeli metodu renderNotFound(). View může existovat i bez ní. Stačí
jen vytvořit šablonu se správným názvem a je hotovo.
Titulek stránek
Nyní se sice můžeme pohodlně přesouvat mezi jednotlivými výpisy, ale pokud se podíváme do historie nebo do názvu okna prohlížeče, zjistíme, že titulek stránky se vůbec nemění.
Díky dědičnosti šablon však bude doplnění titulku záležitostí chvilky. Šablona view se vykonává vždy před
šablonou layoutu, teprve po ní se vykresluje layout a vkládají se do něj bloky z šablony. Před blokem
{content} v šabloně view tedy můžeme nastavit libovolnou proměnnou a tu použít v layoutu.
Nejprve tedy v @layout.latte upravíme obsah elementu <title>:
<title>{block title|stripTags|strip}Úkolníček{/block}</title>
Využijeme faktu, že bloky je možné překrýt a nadefinujeme tedy pouze výchozí hodnotu. Dále je vhodné pomocí helperu
očesat obsah bloku o tagy pomocí stripTags a zaříznout na jeden řádek pomocí strip. Nyní už
stačí pouze v požadovaném pohledu překrýt blok a titulek bude automaticky v <head>.
Homepage/default.latte:
<h1 n:block="title">Nesplněné úkoly</h1>
Task/default.latte:
<h1 n:block="title">{$list->title}</h1>
Task/notFound.latte:
<h1 n:block="title">Seznam úkolů nenalezen</h1>
A to je vše. Nyní už se titulek stránky bude při procházení webem měnit.
Ostylování
Aplikace byla prozatím poměrně smutná. Černobílým HTML dnes již nikoho neohromíte, proto je načase s tím něco
udělat. Stáhneme si tedy soubor screen.css, uložíme
ho do složky www/css a odkážeme se na něj z hlavičky v @layout.latte. Pozor! Předchozí dvě
definice stylů odstraníme, aby se nám styly nemíchaly.
Snahou bylo vmáčknout celý design do jednoho jediného souboru, proto využívá některé modernější CSS3 vlastnosti. Pokud máte starší prohlížeč, budete jen ochuzeni o některé efekty, aplikace však bude vypadat a fungovat velmi podobně.
<link rel="stylesheet" href="{$basePath}/css/screen.css" type="text/css">
Použitá proměnná {$basePath} obsahuje absolutní URL k našemu webu na serveru. Kvůli generování hezkých
adres pomocí mod_rewrite je adresa aktuální stránky čistě virtuální. Složka task/default nikde
neexistuje, proto nemůžeme použít relativní adresy a nesmíme zapomínat na {$basePath} při vkládání
externích zdrojů.
Úspěšně jsme vytvořili seznam všech nedokončených úkolů a zobrazení jednotlivých seznamů úkolů. Můžeme se vrhnout do použití formulářů.
Zdrojové kódy aplikace v této fázi naleznete na GitHubu.