Edit
Lang

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 https://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 příkladu 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í filtry. 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(__DIR__ . '/poll.latte');
    // vložíme do šablony nějaké parametry
    $template->param = $value;
    // a vykreslíme ji
    $template->render();
}

Ze šablony můžeme komponentě i předat parametry. Ty se předají až metodě render().

{control poll $poll}
// PollControl.php
public function render($poll) { ... }
// PollPresenter.php
protected function createComponentPoll() { ... }

Odkazy

Pomocí metody link() odkazujeme na jednotlivé signály. V šablonách se odkazy vykreslují pomocí makra {link}, ze šablony komponenty můžeme odkázat i na libovolný presenter pomocí makra {plink}.

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ě Nette\Application\UI\Component se pokusí zavolat metodu složenou ze slov handle{signal}. Další zpracování je na daném objektu. Objekty, které dědí od Component (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. A pokud metoda neexistuje, metoda signalReceived vyvolá výjimku.

Example signal handler:

public function handleClick($x, $y)
{
    if (!$this->isClickable($x, $y)) {
        throw new Nette\Application\UI\BadSignalException('Action not allowed.');
    }

    // ... processing of signal ...
}

Signál může přijímat jakákoliv komponenta, presenter nebo objekt, který implementuje rozhraní ISignalReceiver a je připojený do stromu komponent.

Mezi hlavní příjemce signálů budou patřit Presentery a vizuální komponenty dědící od Control. 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 Component::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, je ovšem možné takto zanořit několik komponent.

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. Jako druhý parameter lze 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 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ý předčasně 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áší pouze perzistentní komponenty

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ží metody redrawControl() a isControlInvalid(), což je základem AJAXu v Nette.

Nette však nabízí ještě jemnější rozlišení aktuálnosti, 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 slepě nevěřte perzistentním parametrům, 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 se závislostmi

Co když ale naše komponenta potřebuje nějaké věci k tomu aby fungovala, třeba PollControl potřebuje PollManager přes který by hlasovala a ukládala ankety, provede se to vstříknutím do konstruktoru:

class PollControl extends Control
{
    /**
     * @var App\Model\PollManager
     */
    private pollManager;

    /**
     * @var int Id ankety pro kterou vytváříme komponentu
     */
    private $pollId;

    /**
     * @param $pollId
     * @param App\Model\PollManager $pollManager model starající se o hlasování
     */
    public function __construct($pollId, PollManager $pollManager)
    {
        $this->pollManager = $pollManager;
        $this->pollId = $pollId
    }

    /**
     * @param $voteId Id možnosti pro kterou jsme hlasovali
     */
    public function handleVote($voteId)
    {
        $this->pollManager->vote($pollId, $voteId);
        //...
    }
}

No jo, ale jak se to do toho konstruktoru dostane? O to se právě postará DI kontejner díky generované továrničce:

interface IPollControlFactory
{
    /**
     * @param $pollId
     * @return PollControl
     */
    public function create($pollId);
}

a tenhle interface zaregistrujeme do našeho kontejneru v neonu:

services:
    - IPollControlFactory

a tuto továrničku použijeme v našem presenteru:

class PollPresenter extends \Nette\UI\Application\Presenter
{
    /**
     * @var IPollControlFactory
     * @inject
     */
    public $pollControlFactory;

    protected function createComponentPollControl()
    {
        $pollId = 1; // můžeme si předat náš parametr
        return $this->pollControlFactory->create($pollId);
    }
}

A to je vše. Nette vnitřně tento interface naimplementuje a vstříkne nám ji do presenteru, kde ji už můžeme používat. Magicky nám právě do naší komponenty přidá i parametr $pollId a instanci třídy App\Model\PollManager.

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 objektů 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\ComponentModel\Component  { IComponent }
|
+- Nette\ComponentModel\Container  { IContainer }
   |
   +- Nette\Application\UI\Component  { ISignalReceiver, IStatePersistent }
      |
      +- Nette\Application\UI\Control  { IPartiallyRenderable }
         |
         +- Nette\Application\UI\Presenter  { IPresenter }

Nette\ComponentModel\IComponent

Rozhraní Nette\ComponentModel\IComponent musí implementovat každá komponenta. Vyžaduje 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) vrací tzv. cestu, což je ř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 iterací 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 například formuláře a 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. Těmto metodán říkáme továrničky na komponenty a mohou je implementovat potomci třídy Container.

Nette\Application\UI\Component

Třída Nette\Application\UI\Component je předek 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 (signály) 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 (nebo její potomky) 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, jak jsme si už ukázali.

Control nepředstavuje výseč stránky, ale její logickou část. Je možné ji renderovat opakovaně, nebo podmíněně a klidně pokaždé s jinou šablonou.

Znovupoužitelná součást aplikace.

Strom 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

Zpožděné provázání

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 (v konstruktoru) znám rodič, rodič rodiče atd. Většinou totiž rodič při vytvoření vůbec známý není.

$control = new NewsControl;
// ...
$parent->addComponent($control, '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 a 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í. 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)
{
    parent::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í.

Například takto nějak se interně provádí ověření validace prvků:

$valid = TRUE;
foreach ($form->getComponents(TRUE, 'Nette\Forms\IControl') as $control) {
    if (!$control->getRules()->validate()) {
        $valid = FALSE;
        break;
    }
}