Nette Documentation Preview

syntax
Interaktivní komponenty
***********************

<div class=perex>

Komponenty jsou samostatné znovupoužitelné objekty, které vkládáme do stránek. Mohou to být formuláře, datagridy, ankety, vlastně cokoliv, co má smysl používat opakovaně. Ukážeme si:

- jak používat komponenty?
- jak je psát?
- co jsou to signály?

</div>

Nette má v sobě vestavěný komponentový systém. Něco podobného mohou pamětníci znát z Delphi nebo ASP.NET Web Forms, na něčem vzdáleně podobném je postaven React nebo Vue.js. Nicméně ve světě PHP frameworků jde o unikátní záležitost.

Přitom komponenty zásadním způsobem ovlivňují přístup k tvorbě aplikací. Můžete totiž stránky skládat z předpřipravených jednotek. Potřebujete v administraci datagrid? Najdete jej na [Componette |https://componette.org/search/component], repositáři open-source doplňků (tedy nejen komponent) pro Nette a jednoduše vložíte do presenteru.

Do presenteru můžete začlenit libovolný počet komponent. A do některých komponent můžete vkládat další komponenty. Vzniká tak komponentový strom, jehož kořenem je presenter.


Tovární metody
==============

Jak se do presenteru komponenty vkládají a následně používají? Obvykle pomocí továrních metod.

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í.

```php .{file:DefaultPresenter.php}
class DefaultPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentPoll(): PollControl
	{
		$poll = new PollControl;
		$poll->items = $this->item;
		return $poll;
	}
}
```

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

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

Továrny nikdy nevoláme přímo, zavolají se samy 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.

```php .{file:DefaultPresenter.php}
// přistoupíme ke komponentě a pokud to bylo poprvé,
// zavolá se createComponentPoll() která ji vytvoří
$poll = $this->getComponent('poll');
// alternativní syntax: $poll = $this['poll'];
```

V šabloně je možné vykreslit komponentu pomocí značky [{control} |#Vykreslení]. Není proto potřeba manuálně komponenty předávat do šablony.

```latte
<h2>Hlasujte</h2>

{control poll}
```


Hollywood style
===============

Komponenty běžně používají jednu svěží techniku, které rádi říkáme Hollywood style. Určitě znáte okřídlenou větu, kterou tak často slyší účastníci filmových konkurzů: „Nevolejte nám, my vám zavoláme“. A právě o tu jde.

V Nette totiž místo toho, abyste se museli neustále na něco ptát („byl formulář odeslaný?“, „bylo to validní?“ nebo „stiskl uživatel tohle tlačítko?“), řeknete frameworku „až se to stane, zavolej tuhle metodu“ a necháte další práci na něm. Pokud programujete v JavaScriptu, tento styl programování důvěrně znáte. Píšete funkce které se volají, až nastane určitá událost. A jazyk jim předá příslušné parametry.

Tohle zcela mění pohled na psaní aplikací. Čím víc úkolů můžete nechat na frameworku, tím méně máte práce vy. A tím méně toho můžete třeba opomenout.


Píšeme komponentu
=================

Pod pojmem komponenta obvykle myslíme potomka třídy [api:Nette\Application\UI\Control]. (Přesnější by tedy bylo používat termín „controls“, ale „kontroly“ mají v češtině zcela jiný význam a spíš se ujaly „komponenty“.) Samotný presenter [api:Nette\Application\UI\Presenter] je mimochodem také potomkem třídy `Control`.

```php .{file:PollControl.php}
use Nette\Application\UI\Control;

class PollControl extends Control
{
}
```


Vykreslení
==========

Už víme, že k vykreslení komponenty se používá značka `{control componentName}`. Ta vlastně zavolá metodu `render()` komponenty, ve které se postáráme o vykreslení. K dispozici máme, úplně stejně jako v presenteru, [Latte šablonu|templates] v proměnné `$this->template`, do které předáme parametry. Na rozdíl od presenteru musíme uvést soubor se šablonou a nechat ji vykreslit:

```php .{file:PollControl.php}
public function render(): void
{
	// vložíme do šablony nějaké parametry
	$this->template->param = $value;
	// a vykreslíme ji
	$this->template->render(__DIR__ . '/poll.latte');
}
```

Značka `{control}` umožňuje do metody `render()` předat parametry:

```latte
{control poll $id, $message}
```

```php .{file:PollControl.php}
public function render(int $id, string $message): void
{
	// ...
}
```

Někdy se může komponenta skládat z několika částí, které chceme vykreslovat odděleně. Pro každou z nich si vytvoříme vlastní vykreslovací metodu, zde v příkladu třeba `renderPaginator()`:

```php .{file:PollControl.php}
public function renderPaginator(): void
{
	// ...
}
```

A v šabloně ji pak vyvoláme pomocí:

```latte
{control poll:paginator}
```

Pro lepší pochopení je dobré vědět, jak se tato značka přeloží do PHP.

```latte
{control poll}
{control poll:paginator 123, 'hello'}
```

se přeloží jako:

```php
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
```

Metoda `getComponent()` vrací komponentu `poll` a nad touto komponentou volá metodu `render()`, resp. `renderPaginator()` pokud je jiný způsob renderování uveden ve značce za dvojtečkou.

.[caution]
Pozor, pokud se kdekoliv v parametrech objeví **`=>`**, všechny parametry budou zabaleny do pole a předány jako první argument:

```latte
{control poll, id: 123, message: 'hello'}
```

se přeloží jako:

```php
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
```

Vykreslení sub-komponety:

```latte
{control cartControl-someForm}
```

se přeloží jako:

```php
$control->getComponent("cartControl-someForm")->render();
```

Komponenty, stejně jako presentery, předávají do šablon několik užitečných proměnných automaticky:

- `$basePath` je absolutní URL cesta ke kořenovému adresáři (např. `/eshop`)
- `$baseUrl` je absolutní URL ke kořenovému adresáři (např. `http://localhost/eshop`)
- `$user` je objekt [reprezentující uživatele |security:authentication]
- `$presenter` je aktuální presenter
- `$control` je aktuální komponenta
- `$flashes` pole [zpráv |#flash zprávy] zaslaných funkcí `flashMessage()`


Signál
======

Už víme, že navigace v Nette aplikaci spočívá v odkazování nebo přesměrování na dvojice `Presenter:action`. Ale co když jen chceme provést akci na **aktuální stránce**? Například změnit řazení sloupců v tabulce; smazat položku; přepnout světlý/tmavý režim; odeslat formulář; hlasovat v anketě; atd.

Tomuto druhu požadavků se říká signály. A podobně jako akce vyvolávají metody `action<Action>()` nebo `render<Action>()`, signály volají metody `handle<Signal>()`. Zatímco pojem akce (nebo view) souvisí čistě jen s presentery, signály se týkají všech komponent. A tedy i presenterů, protože `UI\Presenter` je potomkem `UI\Control`.

```php
public function handleClick(int $x, int $y): void
{
	// ... processing of signal ...
}
```

Odkaz, který zavolá signál, vytvoříme obvyklým způsobem, tedy v šabloně atributem `n:href` nebo značkou `{link}`, v kódu metodou `link()`. Více v kapitole [Vytváření odkazů URL|creating-links#Odkazy na signál].

```latte
<a n:href="click! $x, $y">click here</a>
```

Signál se vždy volá na aktuálním presenteru a view, tudíž není možné jej vyvolat na jiném presenteru nebo view.

Signál tedy způsobí znovunačtení stránky úplně stejně jako při původním požadavku, jen navíc zavolá obslužnou metodu signálu s příslušnými parametry. Pokud metoda neexistuje, vyhodí se výjimka [api:Nette\Application\UI\BadSignalException], která se uživateli zobrazí jako chybová stránka 403 Forbidden.


Snippety a AJAX
===============

Signály vám možná trošku připomínají AJAX: handlery, které se vyvolávají na aktuální stránce. A máte pravdu, signály se opravdu často volají pomocí AJAXu a následně přenášíme do prohlížeče pouze změněné části stránky. Neboli tzv. snippety. Více informací naleznete na [stránce věnované AJAXu |ajax].


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. 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ších 30 sekund – 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í.

Zasílání obstarává metoda [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Prvním parametrem je text zprávy nebo objekt `stdClass` reprezentující zprávu. Nepovinným druhým parametrem její typ (error, warning, info apod.). Metoda `flashMessage()` vrací instanci flash zprávy jako objekt `stdClass`, které je možné přidávat další informace.

```php
$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 objekty `stdClass`, 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:

```latte
{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
```


Persistentní parametry
======================

Persistentní parametry slouží k udržování stavu v komponentách mezi různými požadavky. Jejich hodnota zůstává stejná i po kliknutí na odkaz. Na rozdíl od dat v session se přenášejí v URL. A to zcela automaticky, včetně odkazů vytvořených v jiných komponentách na téže stránce.

Máte např. komponentu pro stránkování obsahu. Takových komponent může být na stránce několik. A přejeme si, aby po kliknutí na odkaz zůstaly všechny komponenty na své aktuální stránce. Proto z čísla stránky (`page`) uděláme persistentní parametr.

Vytvoření persistentního parametru je v Nette nesmírně jednoduché. Stačí vytvořit veřejnou property a označit ji atributem: (dříve se používalo `/** @persistent */`)

```php
use Nette\Application\Attributes\Persistent;  // tento řádek je důležitý

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // musí být public
}
```

U property doporučujeme uvádět i datový typ (např. `int`) a můžete uvést i výchozí hodnotu. Hodnoty parametrů lze [validovat |#Validace persistentních parametrů].

Při vytváření odkazu lze persistentnímu parametru změnit hodnotu:

```latte
<a n:href="this page: $page + 1">next</a>
```

Nebo jej lze *vyresetovat*, tj. odstranit z URL. Pak bude nabývat svou výchozí hodnotu:

```latte
<a n:href="this page: null">reset</a>
```


Persistentní komponenty
=======================

Nejen parametry, ale také komponenty mohou být persistentní. U takové komponenty se její persistentní parametry přenáší i mezi různými akcemi presenteru nebo mezi více presentery. Persistentní komponenty značíme anotací u třídy presenteru. Třeba takto označíme komponenty `calendar` a `poll`:

```php
/**
 * @persistent(calendar, poll)
 */
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
```

Podkomponenty uvnitř těchto komponent není třeba značit, stanou se persistentní taky.

V PHP 8 můžete pro označení persistentních komponent použít také atributy:

```php
use Nette\Application\Attributes\Persistent;

#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
```


Komponenty se závislostmi
=========================

Jak vytvářet komponenty se závislostmi, aniž bychom si „zaneřádili“ presentery, které je budou používat? Díky chytrým vlastnostem DI kontejneru v Nette lze stejně jako u používání klasických služeb nechat většinu práce na frameworku.

Vezměme si jako příklad komponentu, která má závislost na službě `PollFacade`:

```php
class PollControl extends Control
{
	public function __construct(
		private int $id, //  Id ankety pro kterou vytváříme komponentu
		private PollFacade $facade,
	) {
	}

	public function handleVote(int $voteId): void
	{
		$this->facade->vote($id, $voteId);
		// ...
	}
}
```

Pokud bychom psali klasickou službu, nebylo by co řešit. O předání všech závislostí by se neviditelně postaral DI kontejner. Jenže s komponentami obvykle zacházíme tak, že jejich novou instanci vytváříme přímo v presenteru v [továrních metodách |#Tovární metody] `createComponent…()`. Ale předávat si všechny závislosti všech komponent do presenteru, abychom je pak předali komponentám, je těžkopádné. A toho napsaného kódu…

Logickou otázkou je, proč prostě nezaregistrujeme komponentu jako klasickou službu, nepředáme ji do presenteru a poté v metodě `createComponent…()` nevracíme? Takový přístup je ale nevhodný, protože komponentu chceme mít možnost vytvářet klidně i vícekrát.

Správným řešením je napsat pro komponentu továrnu, tedy třídu, která nám komponentu vytvoří:

```php
class PollControlFactory
{
	public function __construct(
		private PollFacade $facade,
	) {
	}

	public function create(int $id): PollControl
	{
		return new PollControl($id, $this->facade);
	}
}
```

Takhle továrnu zaregistrujeme do našeho kontejneru v konfiguraci:

```neon
services:
	- PollControlFactory
```

a nakonec ji použijeme v našem presenteru:

```php
class PollPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private PollControlFactory $pollControlFactory,
	) {
	}

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

Skvělé je, že Nette DI takovéhle jednoduché továrny umí [generovat |dependency-injection:factory], takže místo jejího celého kódu stačí napsat jenom její rozhraní:

```php
interface PollControlFactory
{
	public function create(int $id): PollControl;
}
```

A to je vše. Nette vnitřně tento interface naimplementuje a předá do presenteru, kde jej už můžeme používat. Magicky nám právě do naší komponenty přidá i parametr `$id` a instanci třídy `PollFacade`.


Komponenty do hloubky
=====================

Komponenty v Nette Application představují znovupoužitelné součásti webové aplikace, které vkládáme do stránek a kterým se ostatně věnuje celá tato kapitola. Jaké přesně schopnosti taková komponenta má?

1) je vykreslitelná v šabloně
2) ví, [kterou svou část|ajax#snippety] má vykreslit při AJAXovém požadavku (snippety)
3) má schopnost ukládat svůj stav do URL (persistentní parametry)
4) má schopnost reagovat na uživatelské akce (signály)
5) vytváří hierarchickou strukturu (kde kořenem je presenter)

Každou z těchto funkcí obstarává některá z tříd dědičné linie. Vykreslování (1 + 2) má na starosti [api:Nette\Application\UI\Control], začlenění do [životního cyklu |presenters#zivotni-cyklus-presenteru] (3, 4) třída [api:Nette\Application\UI\Component] a vytváření hierachické struktury (5) třídy [Container a Component |component-model:].

```
Nette\ComponentModel\Component  { IComponent }
|
+- Nette\ComponentModel\Container  { IContainer }
	|
	+- Nette\Application\UI\Component  { SignalReceiver, StatePersistent }
		|
		+- Nette\Application\UI\Control  { Renderable }
			|
			+- Nette\Application\UI\Presenter  { IPresenter }
```


Životní cyklus componenty
-------------------------

[* lifecycle-component.svg *] *** *Životní cyklus componenty* .<>


Validace persistentních parametrů
---------------------------------

Hodnoty [persistentních parametrů |#Persistentní parametry] přijatých z URL zapisuje do properties metoda `loadState()`. Ta také kontroluje, zda odpovídá datový typ uvedený u property, jinak odpoví chybou 404 a stránka se nezobrazí.

Nikdy slepě nevěřte persistentním parametrům, protože mohou být snadno uživatelem přepsány v URL. Takto například ověříme, zda je číslo stránky `$this->page` větší než 0. Vhodnou cestou je přepsat zmíněnou metodu `loadState()`:

```php
class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1;

	public function loadState(array $params): void
	{
		parent::loadState($params); // zde se nastaví $this->page
		// následuje vlastní kontrola hodnoty:
		if ($this->page < 1) {
			$this->error();
		}
	}
}
```

Opačný proces, tedy sesbírání hodnot z persistentních properites, má na starosti metoda `saveState()`.


Signály do hloubky
------------------

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 |api:Nette\Application\UI\BadSignalException].

Signál může přijímat jakákoliv komponenta, presenter nebo objekt, který implementuje rozhraní `SignalReceiver` 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ě.

URL pro signál vytváříme pomocí metody [Component::link() |api:Nette\Application\UI\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()|api:Nette\Application\UI\Presenter::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()|api:Nette\Application\UI\Presenter::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:

```php
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.


/--comment
	/** @var callable[]&(callable(Component $sender): void)[]; Occurs when component is attached to presenter */
	public $onAnchor;

	/**
	 * Returns destination as Link object.
	 * @param  string   $destination in format "[homepage] [[[module:]presenter:]action | signal! | this] [#fragment]"
	 * @param  array|mixed  $args
	 */
	public function lazyLink(string $destination, $args = []): Link
\--

Interaktivní komponenty

Komponenty jsou samostatné znovupoužitelné objekty, které vkládáme do stránek. Mohou to být formuláře, datagridy, ankety, vlastně cokoliv, co má smysl používat opakovaně. Ukážeme si:

  • jak používat komponenty?
  • jak je psát?
  • co jsou to signály?

Nette má v sobě vestavěný komponentový systém. Něco podobného mohou pamětníci znát z Delphi nebo ASP.NET Web Forms, na něčem vzdáleně podobném je postaven React nebo Vue.js. Nicméně ve světě PHP frameworků jde o unikátní záležitost.

Přitom komponenty zásadním způsobem ovlivňují přístup k tvorbě aplikací. Můžete totiž stránky skládat z předpřipravených jednotek. Potřebujete v administraci datagrid? Najdete jej na Componette, repositáři open-source doplňků (tedy nejen komponent) pro Nette a jednoduše vložíte do presenteru.

Do presenteru můžete začlenit libovolný počet komponent. A do některých komponent můžete vkládat další komponenty. Vzniká tak komponentový strom, jehož kořenem je presenter.

Tovární metody

Jak se do presenteru komponenty vkládají a následně používají? Obvykle pomocí továrních metod.

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í.

class DefaultPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentPoll(): PollControl
	{
		$poll = new PollControl;
		$poll->items = $this->item;
		return $poll;
	}
}

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

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

Továrny nikdy nevoláme přímo, zavolají se samy 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.

// přistoupíme ke komponentě a pokud to bylo poprvé,
// zavolá se createComponentPoll() která ji vytvoří
$poll = $this->getComponent('poll');
// alternativní syntax: $poll = $this['poll'];

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

<h2>Hlasujte</h2>

{control poll}

Hollywood style

Komponenty běžně používají jednu svěží techniku, které rádi říkáme Hollywood style. Určitě znáte okřídlenou větu, kterou tak často slyší účastníci filmových konkurzů: „Nevolejte nám, my vám zavoláme“. A právě o tu jde.

V Nette totiž místo toho, abyste se museli neustále na něco ptát („byl formulář odeslaný?“, „bylo to validní?“ nebo „stiskl uživatel tohle tlačítko?“), řeknete frameworku „až se to stane, zavolej tuhle metodu“ a necháte další práci na něm. Pokud programujete v JavaScriptu, tento styl programování důvěrně znáte. Píšete funkce které se volají, až nastane určitá událost. A jazyk jim předá příslušné parametry.

Tohle zcela mění pohled na psaní aplikací. Čím víc úkolů můžete nechat na frameworku, tím méně máte práce vy. A tím méně toho můžete třeba opomenout.

Píšeme komponentu

Pod pojmem komponenta obvykle myslíme potomka třídy Nette\Application\UI\Control. (Přesnější by tedy bylo používat termín „controls“, ale „kontroly“ mají v češtině zcela jiný význam a spíš se ujaly „komponenty“.) Samotný presenter Nette\Application\UI\Presenter je mimochodem také potomkem třídy Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Vykreslení

Už víme, že k vykreslení komponenty se používá značka {control componentName}. Ta vlastně zavolá metodu render() komponenty, ve které se postáráme o vykreslení. K dispozici máme, úplně stejně jako v presenteru, Latte šablonu v proměnné $this->template, do které předáme parametry. Na rozdíl od presenteru musíme uvést soubor se šablonou a nechat ji vykreslit:

public function render(): void
{
	// vložíme do šablony nějaké parametry
	$this->template->param = $value;
	// a vykreslíme ji
	$this->template->render(__DIR__ . '/poll.latte');
}

Značka {control} umožňuje do metody render() předat parametry:

{control poll $id, $message}
public function render(int $id, string $message): void
{
	// ...
}

Někdy se může komponenta skládat z několika částí, které chceme vykreslovat odděleně. Pro každou z nich si vytvoříme vlastní vykreslovací metodu, zde v příkladu třeba renderPaginator():

public function renderPaginator(): void
{
	// ...
}

A v šabloně ji pak vyvoláme pomocí:

{control poll:paginator}

Pro lepší pochopení je dobré vědět, jak se tato značka přeloží do PHP.

{control poll}
{control poll:paginator 123, 'hello'}

se přeloží jako:

$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');

Metoda getComponent() vrací komponentu poll a nad touto komponentou volá metodu render(), resp. renderPaginator() pokud je jiný způsob renderování uveden ve značce za dvojtečkou.

Pozor, pokud se kdekoliv v parametrech objeví =>, všechny parametry budou zabaleny do pole a předány jako první argument:

{control poll, id: 123, message: 'hello'}

se přeloží jako:

$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);

Vykreslení sub-komponety:

{control cartControl-someForm}

se přeloží jako:

$control->getComponent("cartControl-someForm")->render();

Komponenty, stejně jako presentery, předávají do šablon několik užitečných proměnných automaticky:

  • $basePath je absolutní URL cesta ke kořenovému adresáři (např. /eshop)
  • $baseUrl je absolutní URL ke kořenovému adresáři (např. http://localhost/eshop)
  • $user je objekt reprezentující uživatele
  • $presenter je aktuální presenter
  • $control je aktuální komponenta
  • $flashes pole zpráv zaslaných funkcí flashMessage()

Signál

Už víme, že navigace v Nette aplikaci spočívá v odkazování nebo přesměrování na dvojice Presenter:action. Ale co když jen chceme provést akci na aktuální stránce? Například změnit řazení sloupců v tabulce; smazat položku; přepnout světlý/tmavý režim; odeslat formulář; hlasovat v anketě; atd.

Tomuto druhu požadavků se říká signály. A podobně jako akce vyvolávají metody action<Action>() nebo render<Action>(), signály volají metody handle<Signal>(). Zatímco pojem akce (nebo view) souvisí čistě jen s presentery, signály se týkají všech komponent. A tedy i presenterů, protože UI\Presenter je potomkem UI\Control.

public function handleClick(int $x, int $y): void
{
	// ... processing of signal ...
}

Odkaz, který zavolá signál, vytvoříme obvyklým způsobem, tedy v šabloně atributem n:href nebo značkou {link}, v kódu metodou link(). Více v kapitole Vytváření odkazů URL.

<a n:href="click! $x, $y">click here</a>

Signál se vždy volá na aktuálním presenteru a view, tudíž není možné jej vyvolat na jiném presenteru nebo view.

Signál tedy způsobí znovunačtení stránky úplně stejně jako při původním požadavku, jen navíc zavolá obslužnou metodu signálu s příslušnými parametry. Pokud metoda neexistuje, vyhodí se výjimka Nette\Application\UI\BadSignalException, která se uživateli zobrazí jako chybová stránka 403 Forbidden.

Snippety a AJAX

Signály vám možná trošku připomínají AJAX: handlery, které se vyvolávají na aktuální stránce. A máte pravdu, signály se opravdu často volají pomocí AJAXu a následně přenášíme do prohlížeče pouze změněné části stránky. Neboli tzv. snippety. Více informací naleznete na stránce věnované AJAXu.

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. 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ších 30 sekund – 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í.

Zasílání obstarává metoda flashMessage. Prvním parametrem je text zprávy nebo objekt stdClass reprezentující zprávu. Nepovinným druhým parametrem její typ (error, warning, info apod.). Metoda flashMessage() vrací instanci flash zprávy jako objekt stdClass, které je možné přidávat další informace.

$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 objekty stdClass, 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}

Persistentní parametry

Persistentní parametry slouží k udržování stavu v komponentách mezi různými požadavky. Jejich hodnota zůstává stejná i po kliknutí na odkaz. Na rozdíl od dat v session se přenášejí v URL. A to zcela automaticky, včetně odkazů vytvořených v jiných komponentách na téže stránce.

Máte např. komponentu pro stránkování obsahu. Takových komponent může být na stránce několik. A přejeme si, aby po kliknutí na odkaz zůstaly všechny komponenty na své aktuální stránce. Proto z čísla stránky (page) uděláme persistentní parametr.

Vytvoření persistentního parametru je v Nette nesmírně jednoduché. Stačí vytvořit veřejnou property a označit ji atributem: (dříve se používalo /** @persistent */)

use Nette\Application\Attributes\Persistent;  // tento řádek je důležitý

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // musí být public
}

U property doporučujeme uvádět i datový typ (např. int) a můžete uvést i výchozí hodnotu. Hodnoty parametrů lze validovat.

Při vytváření odkazu lze persistentnímu parametru změnit hodnotu:

<a n:href="this page: $page + 1">next</a>

Nebo jej lze vyresetovat, tj. odstranit z URL. Pak bude nabývat svou výchozí hodnotu:

<a n:href="this page: null">reset</a>

Persistentní komponenty

Nejen parametry, ale také komponenty mohou být persistentní. U takové komponenty se její persistentní parametry přenáší i mezi různými akcemi presenteru nebo mezi více presentery. Persistentní komponenty značíme anotací u třídy presenteru. Třeba takto označíme komponenty calendar a poll:

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

Podkomponenty uvnitř těchto komponent není třeba značit, stanou se persistentní taky.

V PHP 8 můžete pro označení persistentních komponent použít také atributy:

use Nette\Application\Attributes\Persistent;

#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Komponenty se závislostmi

Jak vytvářet komponenty se závislostmi, aniž bychom si „zaneřádili“ presentery, které je budou používat? Díky chytrým vlastnostem DI kontejneru v Nette lze stejně jako u používání klasických služeb nechat většinu práce na frameworku.

Vezměme si jako příklad komponentu, která má závislost na službě PollFacade:

class PollControl extends Control
{
	public function __construct(
		private int $id, //  Id ankety pro kterou vytváříme komponentu
		private PollFacade $facade,
	) {
	}

	public function handleVote(int $voteId): void
	{
		$this->facade->vote($id, $voteId);
		// ...
	}
}

Pokud bychom psali klasickou službu, nebylo by co řešit. O předání všech závislostí by se neviditelně postaral DI kontejner. Jenže s komponentami obvykle zacházíme tak, že jejich novou instanci vytváříme přímo v presenteru v továrních metodách createComponent…(). Ale předávat si všechny závislosti všech komponent do presenteru, abychom je pak předali komponentám, je těžkopádné. A toho napsaného kódu…

Logickou otázkou je, proč prostě nezaregistrujeme komponentu jako klasickou službu, nepředáme ji do presenteru a poté v metodě createComponent…() nevracíme? Takový přístup je ale nevhodný, protože komponentu chceme mít možnost vytvářet klidně i vícekrát.

Správným řešením je napsat pro komponentu továrnu, tedy třídu, která nám komponentu vytvoří:

class PollControlFactory
{
	public function __construct(
		private PollFacade $facade,
	) {
	}

	public function create(int $id): PollControl
	{
		return new PollControl($id, $this->facade);
	}
}

Takhle továrnu zaregistrujeme do našeho kontejneru v konfiguraci:

services:
	- PollControlFactory

a nakonec ji použijeme v našem presenteru:

class PollPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private PollControlFactory $pollControlFactory,
	) {
	}

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

Skvělé je, že Nette DI takovéhle jednoduché továrny umí generovat, takže místo jejího celého kódu stačí napsat jenom její rozhraní:

interface PollControlFactory
{
	public function create(int $id): PollControl;
}

A to je vše. Nette vnitřně tento interface naimplementuje a předá do presenteru, kde jej už můžeme používat. Magicky nám právě do naší komponenty přidá i parametr $id a instanci třídy PollFacade.

Komponenty do hloubky

Komponenty v Nette Application představují znovupoužitelné součásti webové aplikace, které vkládáme do stránek a kterým se ostatně věnuje celá tato kapitola. Jaké přesně schopnosti taková komponenta má?

  1. je vykreslitelná v šabloně
  2. ví, kterou svou část má vykreslit při AJAXovém požadavku (snippety)
  3. má schopnost ukládat svůj stav do URL (persistentní parametry)
  4. má schopnost reagovat na uživatelské akce (signály)
  5. vytváří hierarchickou strukturu (kde kořenem je presenter)

Každou z těchto funkcí obstarává některá z tříd dědičné linie. Vykreslování (1 + 2) má na starosti Nette\Application\UI\Control, začlenění do životního cyklu (3, 4) třída Nette\Application\UI\Component a vytváření hierachické struktury (5) třídy Container a Component.

Nette\ComponentModel\Component  { IComponent }
|
+- Nette\ComponentModel\Container  { IContainer }
	|
	+- Nette\Application\UI\Component  { SignalReceiver, StatePersistent }
		|
		+- Nette\Application\UI\Control  { Renderable }
			|
			+- Nette\Application\UI\Presenter  { IPresenter }

Životní cyklus componenty

Životní cyklus componenty

Validace persistentních parametrů

Hodnoty persistentních parametrů přijatých z URL zapisuje do properties metoda loadState(). Ta také kontroluje, zda odpovídá datový typ uvedený u property, jinak odpoví chybou 404 a stránka se nezobrazí.

Nikdy slepě nevěřte persistentním parametrům, protože mohou být snadno uživatelem přepsány v URL. Takto například ověříme, zda je číslo stránky $this->page větší než 0. Vhodnou cestou je přepsat zmíněnou metodu loadState():

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1;

	public function loadState(array $params): void
	{
		parent::loadState($params); // zde se nastaví $this->page
		// následuje vlastní kontrola hodnoty:
		if ($this->page < 1) {
			$this->error();
		}
	}
}

Opačný proces, tedy sesbírání hodnot z persistentních properites, má na starosti metoda saveState().

Signály do hloubky

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.

Signál může přijímat jakákoliv komponenta, presenter nebo objekt, který implementuje rozhraní SignalReceiver 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ě.

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.