Vytvoření presenteru
V této kapitole 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()
{
$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 továrny,
můžeme je okamžitě použít. V presenteru jsou dostupné pomocí
„kontejneru“, který obsahuje všechny služby a továrny. Tento kotejner je
přístupný jako atribut $context. Jednotlivé továrny jsou
přístupné pomocí metody create<Factory>(), takže
například model Tasks, který je definovaný jako továrna
task, vytvoříme zavoláním createTasks() nad
objektem kontejneru.
Vyžádáme si tedy vytvoření nového modelu, pomocí něj vybereme
z tabulky task data a vložíme je do šablony. Budeme chtít
zobrazit všechny nesplněné úkoly a seřadit vzestupně je podle času
vytvoření:
public function renderDefault()
{
$this->template->tasks = $this->context->createTasks()
->where(array('done' => false))->order('created ASC');
}
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. V praxi to tedy znamená, že pro každý dotaz do databáze musíme
zavolat znovu metodu createTasks(). Proto musí být náš model
definovaný jako továrna a nikoliv jako služba.
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í kapitole o 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.
V TaskPresenteru budeme chtít zobrazit jeden konkrétní
seznam úkolů. Seznam, který chceme zobrazit, dostane presenter jako ID
v adrese. Vytvoříme si privátní atribut $taskList, do kterého
uložíme v metodě actionDefault záznam o seznamu úkolů:
private $taskList;
public function actionDefault($id)
{
$this->taskList = $this->context->createTasklists()->get($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“.
Získaný záznam spolu se seznamem úkolů předáme šabloně v metodě
renderDefault():
public function renderDefault($id)
{
$this->template->taskList = $this->taskList;
$this->template->tasks = $this->taskList->related('task')->order('created');
}
Metoda renderDefault by měla mít stejné parametry, jako
action, i když je přímo v metodě nevyužijeme.
Š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>{$taskList->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.
public function beforeRender()
{
$this->template->taskLists = $this->context->createTasklists()->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="$taskLists 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="$taskLists 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 Call to a member function related() on a non-object nám
říká, že jsme se pokusili zavolat metodu related nad něčím,
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($id)
{
$this->taskList = $this->context->createTasklists()->get($id);
if ($this->taskList === 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>{ifset $title}{$title} – {/ifset}Úkolníček</title>
Makrem {ifset} ověříme existenci proměnné
$title. Pokud tato proměnná bude existovat, použijeme ji. Její
nastavení provedeme v šablonách jednotlivých view. Na začátek souborů
(před makro {block content}) umístíme:
Homepage/default.latte:
{var $title = 'Přehled úkolů'}
Task/default.latte:
{var $title = $taskList->title}
Task/notFound.latte:
{var $title = 'Nenalezeno'}
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 tasklist.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/tasklist.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ů.
Závěr
To je prozatím vše. Ú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 v tagu 03_presenter, případně si je také můžete stáhnout: 03_presenter.zip.
Comments 
kedrigern | 30. 4. 2012, 0:09 | question
Došel jsem až na konec kapitoly Spojení s modelem a chtěl si výsledek zobrazit. Bez dat (s prázdnou DB funguje), když jsem nahrál data (dle skriptu v předchozím části), tak jsem skončil s Laďenkou:
PDOException No reference found for $task->user.
Chápu, že chyba je, že není zadefinována reference. Pokud v šabloně nahradím: {$task->user->name} za: {$task->user_id->name} tak se mi správně zobrazí id.Jenže úplně nerozumím, kde se bere, že mohu zapsat: $task->user->name čili, jak opravit tuto chybu.
kedrigern | 30. 4. 2012, 0:25 | comment
Ještě bych doplnil k předchozímu, že pro vytvoření DB jsem použil PHPmyAdmin (kolonku pro vložení SQL) ve verzi 3.4.5deb1 (Ubuntu) a MySQL 5.1.62–0ubuntu0.11.10.1. Netuším zda to nemohlo rozhodit pojmenování FK či tak.
TomasHalasz | 8. 5. 2012, 21:27 | comment
to kedrigern: Já řešil to samé a problém byl v tom, že jsem měl v MySQL na těch tabulkách engine MyISAM. Je potřeba aby engine byl InnoDB pak fungujou FK a jde $task->user->name
pho | 15. 5. 2012, 13:04 | comment
mel bych problem: vytvorim si tridu s:
public function renderDefault(){ $this->template->users=$this->context->createUsers()->order(‚login ASC‘); } kdyz pak volam defaultni template s: {foreach $users as $user} {$users->login} {$users->surname} {$users->name} {$users->email} {/foreach} Tak mi to hodi chybu. Chyba je podle vseho ve vytvorenem sql dotazu, ktery vypada nasledovne: SELECTid FROM users ORDER BY
login ASC
Misto * je tam naprosto nesmyslne id a netusim proc. Nevite pls v cem by mohl byt zadrhel? Dekuju!
Kranox | 17. 5. 2012, 19:04 | comment
pho: neviem ti poradiť s tým SQL dotazom, prečo je tam id, ale taký detail $users->login by nemalo byť $user->login ? Bez toho ‚s‘ na konci podľa tvojho foreach cyklu.
pho | 21. 5. 2012, 14:07 | comment
jisteze to bylo cele v tomhle :)
Daris | 20. 3. 2012, 10:55 | comment
Kdyby Vám to nefungovalo jako mě, můžete mít špatně nastavený apache. Poznáte to tak, že se nebudete moct nalézt stránky http://localhost/…sk/default/1 a pod.
řešení špatného nastavení najdete zde:
http://www.tipypropc.cz/…veru-apache/