Píšeme komponenty
Komponenty představují základní kámen znovupoužitelnosti kódu, usnadňují vám práci a dovolují využívat práce komunity. Komponenty jsou báječné. Řekneme si
- jak psát komponenty?
- co jsou to signály?
- jak posílat flash zprávy?
- jak na AJAX?
Komponenta představuje vykreslitelný objekt. Jsou to například formuláře, menu, ankety a podobně. V rámci jedné stránky jich může existovat libovolný počet. Na stránkách http://addons.nette.org/cs/ můžete najít open-source komponenty, které sem umístili dobrovolníci z komunity okolo Nette Framework.
Příklad komponenty a jejího začlenění do stránky najdete v instalačním balíku ve složce
examples/Fifteen.
Komponenta je zpravidla potomkem třídy Nette\Application\UI\Control, tím také začneme:
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
Bavíme-li se o komponentách, 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“.
Šablony
Komponenta obsahuje továrničku na svou šablonu. Ta standardně vytvoří šablonu,
předá ji některé základní
proměnné a zaregistruje standardní
helpery. O vykreslení se už musíme postarat sami, a to v metodě
render(). Tam také musíme určit soubor, ze kterého bude
šablona načtena, a zaregistrovat proměnné, které se budou v šabloně
používat. Šablonu můžeme umístit do stejné složky a pod stejným názvem
jako komponentu:
public function render()
{
$template = $this->template;
$template->setFile(dirname(__FILE__) . '/poll.phtml');
// vložíme do šablony nějaké parametry
$template->param = $value;
// a vykreslíme ji
$template->render();
}
Odkazy
Pomocí metody link() odkazujeme na jednotlivé signály.
V šablonách se odkazy vykreslují pomocí stejnojmenného makra.
U presenterů se při volání signálů za jejich název píše vykřičník, aby se odlišily od odkazů na akce. Komponenty však akce nemají, a proto se u nich vykřičník nemusí uvádět.
Příklad použití v komponentě:
$url = $this->link('click', $x, $y);
Příklad použití v šabloně:
<a n:href="click $x, $y"> ... </a>
Flash zprávy
Komponenta má své vlastní úložiště flash zpráv nezávislé na presenteru. Jde o zprávy, které např. informují o výsledku operace, po kterých následuje přesměrování.
Zasílání obstarává metoda flashMessage.
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.
Příklad:
public function deleteFormSubmitted(Form $form)
{
... 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 automaticky předány v proměnné
$flashes. Tato proměnná obsahuje pole s objekty
(stdClass), které obsahují vlastnosti message (text
zprávy), type (typ zprávy) a mohou obsahovat již zmíněné
extra informace.
Příklad:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Nejdůležitejší samozřejmě je, že pokud po uložení zprávy
flashMessage() následuje přesměrování, bude i v dalším
požadavku v šabloně existovat stejný parametr $flashes.
Zprávy zůstanou poté živé další 3 sekundy – například pro případ,
že by z důvodu chybného přenosu uživatel stránku dal obnovit. Pokud
někdo dvakrát za sebou obnoví stránku (F5), tak mu zpráva tedy nezmizí,
pokud klikne jinam, tak ji už neuvidí.
Signál neboli subrequest
Signál (aneb subrequest) je komunikace se serverem pod prahem normálního
view, tedy akce, které se dějí, aniž by se změnilo view. View může měnit
pouze presenter, proto komponenty pracují vždy pod tímto prahem, tudíž
$component->link() vede na signál,
$presenter->link() obvykle na view (nebo signál, je-li označen
vykřičníkem přidaným na konec). Pro úplnost, i komponenta může volat
$this->presenter->link('view').
Signál způsobí znovunačtení stránky úplně stejně jako při
původním požadavku (kromě případu, kdy je volán AJAXem) a vyvolá metodu
signalReceived($signal), jejíž výchozí implementace ve třídě
PresenterComponent se pokusí zavolat metodu složenou ze slov
handle{signal}.
Další zpracování je na daném objektu. Objekty, které dědí od
PresenterComponent (tzn. Control a
Presenter) reagují tak, že se snaží zavolat metodu
handle{signal} s příslušnými parametry.
Jinými slovy: vezme se definice funkce handle{signal} a všechny
parametry, které přišly s požadavkem, a k argumentům se podle jména
dosadí parametry z URL a pokusí se danou metodu zavolat. Např. jako prametr
$id se předá hodnota z parametru id v URL, jako
$something se předá something z URL, atd.
Pokud metoda neexistuje, metoda signalReceived vyvolá
výjimku.
Signál může přijímat jakákoliv komponenta, presenter nebo
objekt, který implementuje rozhraní ISignalReceiver.
Mezi hlavní příjemce signálů budou patřit Presentery a
vizuální komponenty dědící od Control (a ty se při přijetí
signálu automaticky invalidují, což je důležité pro AJAX).
Signál má sloužit jako znamení pro objekt, že má něco udělat – anketa
si má započítat hlas od uživatele, blok s novinkami se má rozbalit a
zobrazit dvakrát tolik novinek, formulář byl odeslán a má zpracovat data a
podobně.
Signál se vždy volá na aktuálním presenteru a view, tudíž není možné jej směřovat jinam.
URL pro signál vytváříme pomocí metody
PresenterComponent::link(). Jako parametr $destination
předáme řetězec {signal}! a jako $args pole
argumentů, které chceme signálu předat. Signál se vždy volá na aktuální
view s aktuálními parametry, parametry signálu se jen přidají. Navíc se
přidává hned na začátku parametr ?do, který určuje
signál.
Jeho formát je buď {signal}, nebo
{signalReceiver}-{signal}. {signalReceiver} je název
komponenty v presenteru. Proto nemůže být v názvu komponenty pomlčka –
používá se k oddělení názvu komponenty a signálu.
Metoda isSignalReceiver() ověří, zda je komponenta (první
argument) příjemcem signálu (druhý argument). Druhý argument můžeme
vynechat – pak zjišťuje, jestli je komponenta příjemcem jakéhokoliv
signálu. Experimentálně lze jako druhý parametr uvést TRUE a
tím ověřit, jestli je příjemcem nejen uvedená komponenta, ale také
kterýkoliv její potomek.
V kterékoliv fázi předcházející handle{signal} můžeme
vykonat signál manuálně zavoláním metody
$this->processSignal(), která si bere na starosti vyřízení
signálu – vezme komponentu, která se určila jako příjemce signálu
(pokud není určen příjemce signálu, je to presenter samotný) a pošle jí
signál.
Příklad:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
Tím je signál provedený a už se nebude znovu volat.
Subrequest vs. request
Rozdíly mezi signálem a požadavkem:
- subrequest přenáší všechny komponenty
- request přenáší označené (perzistentní) komponenty
Signály
Signály umožňují komponentám získávat od uživatele podněty k nějakým akcím. O zachycení signálu se stará presenter, ten ho odevzdá komponentě, která je jeho příjemcem. Protože presenter je sám o sobě komponentou, tak ho klidně odevzdá i sám sobě.
Signál je v komponentě reprezentován metodou handle<název
signálu>. Může mít specifikované parametry. V případě, že
signál není platný, je vyvolána výjimka Nette\Application\UI\BadSignalException.
public function handleClick($x, $y)
{
if (!$this->isClickable($x, $y)) {
throw new Nette\Application\UI\BadSignalException('Action not allowed.');
}
... zpracování signálu ...
}
Invalidace a snippety
Při signálu může dojít ke změnám, které si vyžadují překreslit
komponentu. K tomu slouží triptych metod invalidateControl(),
validateControl() a isControlInvalid(), což je
základem AJAXu v Nette.
Nette však nabízí ještě jemnější rozlišení, než na úrovni komponent, a to tzv. snippetů neboli ústřižků.
Lze tedy invalidovat a validovat na úrovni těchto snippetů (každá komponenta jich může mít libovolné množství). Pokud se invaliduje celá komponenta, tak je i každý její snippet považován za invalidní. Komponenta je invalidní i tehdy, pokud je invalidní některá z jejích podřazených komponent.
Více informací naleznete na stránce věnované AJAXu.
Perzistentní parametry
Často se stává, že je v komponentách potřeba držet nějaký parametr
pro uživatele po celou dobu, kdy se s komponentou pracuje. Může to být
například číslo stránky ve stránkování. Takový parametr označíme jako
perzistentní pomocí anotace
@persistent.
class PollControl extends Control
{
/** @persistent */
public $page = 1;
...
}
Tento parametr bude automaticky přenášen v každém odkazu jako GET parametr, a to až do chvíle, kdy uživatel stránku s touto komponentou opustí.
Nikdy se nespoléhejte na správnost perzistentních parametrů, protože mohou být snadno podvrženy (přepsáním v URL adrese stránky). Ověřte si například, zda je číslo stránky v platném rozsahu.
Komponenty do hloubky
Komponenty bývají ve většině případů vykreslitelné. Vedle nich však existují i nevykreslitelné komponenty. Stejně tak některé komponenty mohou mít potomky, jiné zase ne. Nette Framework pro všechny tyto typy komponent zavádí několik tříd a rozhraní.
Dědičnost objektově orientovaného programování nám umožňuje třídy zařadit do hierarchické struktury, stejně jako je to v reálném světě. Můžeme totiž vytvářet nové třídy odvozením od jiných. Tyto odvozené třídy jsou pak potomkem původní třídy a dědí jeho členské proměnné a metody. Odvozená třída může přidávat další funkcionalitu (metody a členské proměnné) k již zděděným schopnostem.
Ke správnému pochopení „jak věci pracují“, je potřeba vědět, kde má která třída své kořeny.
Nette\Object
|
+- Nette\ComponentModel\Component { IComponent }
|
+- Nette\ComponentModel\Container { IContainer }
|
+- Nette\Application\UI\PresenterComponent { ISignalReceiver, IStatePersistent }
|
+- Nette\Application\UI\Control { IPartiallyRenderable }
|
+- Nette\Application\UI\Presenter { IPresenter }
Nette\ComponentModel\IComponent
Rozhraní Nette\ComponentModel\IComponent
musí implementovat každá komponenta. Musí obsahovat metodu
getName() vracející její název a metodu
getParent() vracející jejího rodiče. Obojí lze nastavit
metodou setParent() – první parametr je rodič a druhý název
komponenty.
Nette\ComponentModel\Component
Nette\ComponentModel\Component
je standardní implementací IComponent. Je společným předkem
všech komponent, vycházejí z ní všechny prvky formulářů. Obsahuje
metody zjišťují příbuznost objektů a hlavně propojení (provázání)
s rodiči:
lookup($type) vyhledá v hierarchii směrem nahoru objekt
požadované třídy nebo rozhraní. Například
$component->lookup('Nette\Application\UI\Presenter') vrací
presenter, pokud je k němu, i přes několik úrovní, komponenta
připojena.
lookupPath($type), která vrací tzv. cestu, což řetězec
vzniklý spojením jmen všech komponent na cestě mezi aktuální a hledanou
komponentou. Takže např.
$component->lookupPath('Nette\Application\UI\Presenter') vrací
jedinečný identifikátor komponenty vůči presenteru.
Nette\ComponentModel\IContainer
Rodičovské komponenty kromě rozhraní IComponent
implementují i Nette\ComponentModel\IContainer,
které obsahuje metody pro přidání, odebrání, získání a iteraci nad
komponentami. Komponenty pak mohou vytvářet hierarchii – např. presentery
mohou obsahovat formuláře obsahující textová políčka a tlačítka. Celý
strom komponent je tedy tvořen větvemi v podobě objektů
IContainer a listů IComponent.
Nette\ComponentModel\Container
Nette\ComponentModel\Container
je standardní implementací rozhraní IContainer. Je předkem
formuláře nebo tříd Control či Presenter.
Disponuje metodami pro snadné přidávání, získávání a odstraňování
objektů a samozřejmě iteraci nad svým obsahem. Při pokusu o získání
nedefinovaného potomka je zavolána továrnička
createComponent($name). Metoda createComponent($name)
zavolá v aktuální komponentě metodu createComponent<název
komponenty> a jako parametr jí předá název komponenty. Vytvořená
komponenta je poté přidána do aktuální komponenty jako její potomek.
Nette\Application\UI\PresenterComponent
Předek Nette\Application\UI\PresenterComponent všech komponent používaných v presenteru. Komponenty presenteru jsou objekty, které si presenter uchovává počas svého životního cyklu.
Mají schopnost vzájemně ovlivňovat ostatní poděděné komponenty, ukládat své stavy do URL a odpovídat na uživatelské příkazy a nemusí být vykreslitelné.
Nette\Application\UI\Control
Control je
vykreslitelná komponenta, znovupoužitelná součást webové aplikace, které
se věnuje celá tato kapitola. Tuto třídu máme obvykle na mysli, když
hovoříme o komponentách. Navíc si umí pamatovat, kterou svou část má
vykreslit při AJAXovém
požadavku. K tomu slouží triptych metod
invalidateControl(), validateControl() a
isControlInvalid().
Control nepředstavuje pravoúhlou oblast ve stránce, ale logickou komponentu, která se může renderovat i do více podob. Každá komponenta může být navíc na stránce vykreslena vícekrát, nebo podmíněně, nebo pokaždé s jinou šablonou atd.
Znovupoužitelná součást aplikace.
Strom komponent
Každá komponenta děděná z třídy
Nette\ComponentModel\Component má jako první parametr
konstruktoru rodiče (IContainer) v hierarchii stromu komponent.
Uvedením stejné komponenty pod různými jmény se dá dosáhnout například
zobrazení jedné komponenty na stránce vícekrát.
Rodičem může být presenter, nějaká komponenta nebo jakýkoliv jiný
objekt implementující rozhraní IContainer. Hierarchie pak může
vypadat nějak takto:
Nette\Application\UI\Presenter { kořenem ve stromu komponent je vždy presenter }
|
--Nette\Application\UI\Control { implementuje IContainer => může být rodičem }
|
--Nette\ComponentModel\Component
|
--Nette\ComponentModel\Component { neimplementuje IContainer => nemůže být rodičem }
|
--Nette\Application\UI\Control
|
--Nette\ComponentModel\Component
Komponentový model Nette umožňuje velmi dynamickou práci se stromem (komponenty můžeme vyjímat, přesouvat, přidávat), proto by byla chyba se spoléhat na to, že po vytvoření komponenty je hned znám rodič, rodič rodiče atd. Nemusí být.
$control = new NewsControl;
// ...
$parent->addComponent($control, 'shortNews');
// nebo alternativně starším (statickým) způsobem
$control = new NewsControl($parent, 'shortNews');
Monitorování změn
Jak poznat, kdy byla komponenta připojena do stromu presenteru? Sledovat
změnu rodiče nestačí, protože k presenteru mohl být připojen třeba
rodič rodiče. Pomůže metoda monitor($type). Každá komponenta
může monitorovat libovolný počet tříd/rozhraní. Připojení nebo
odpojení je ohlášeno zavoláním metody attached($obj) resp.
detached($obj), kde $obj je objekt
sledované třídy.
Pro lepší pochopení příklad: třída UploadControl,
reprezentující formulářový prvek pro upload souborů v Nette\Forms, musí
formuláři nastavit atribut enctype na hodnotu
multipart/form-data. V době vytvoření objektu ale k žádnému
formuláři připojena být nemusí (leda by se v konstruktoru předal $parent,
ale ani ten nemusí být formulářem či kontejnerem připojeným
k formuláři). Ve kterém okamžiku tedy formulář modifikovat? Řešení je
jednoduché – v konstruktoru se požádá o monitoring:
class UploadControl extends Nette\Forms\Controls\BaseControl
{
public function __construct($label)
{
$this->monitor('Nette\Forms\Form');
...
}
...
}
a jakmile je formulář k dispozici, zavolá se metoda attached:
protected function attached($form)
{
if ($form instanceof Nette\Forms\Form) {
$form->getElementPrototype()->enctype = 'multipart/form-data';
}
}
Monitorování a dohledávání komponent nebo cest přes lookup je velmi pečlivě optimalizované pro maximální výkon.
Iterování nad dětmi
K iterování slouží metoda getComponents($deep = FALSE, $type =
NULL). První parametr určuje, zda se mají komponenty procházet do
hloubky (neboli rekurzivně). S hodnotou TRUE tedy nejen projde všechny
komponenty, jichž je rodičem, ale také potomky svých potomků atd. Druhý
parametr slouží jako volitelný filtr podle tříd nebo rozhraní.
Příkladem mohou být formuláře. Takto nějak se provádí kupříkladu ověření validace prvků:
$valid = TRUE;
foreach ($form->getComponents(TRUE, 'Nette\Forms\IControl') as $control) {
if (!$control->getRules()->validate()) {
$valid = FALSE;
break;
}
}


pilec | 7. 2. 2012, 21:19 | comment
Komu by nestačil zde uvedený textový popis funkcionality komponent, může se podívat na skvělou přednášku Honzy Tvrdíka na toto téma.