Komponentový model
******************
.[perex]
Důležitým pojmem v Nette je komponenta. Do stránek vkládáme [vizuální interaktivní komponenty |application:components], komponentami jsou i formuláře nebo všechny jejich prvky. Základní dvě třídy, od kterých všechny tyto komponenty dědí, jsou součástí balíčku `nette/component-model` a mají za úkol vytvářet stromovou hierarchii komponent.
Component
=========
[api:Nette\ComponentModel\Component] je společným předkem všech komponent. Obsahuje metody `getName()` vracející název komponenty a metodu `getParent()` vracející jejího rodiče. Ani jedno nenastavujete sami: obojí nastaví kontejner ve chvíli, kdy do něj komponentu přidáte. (Používá k tomu metodu `setParent()`, která je interní, takže ji ze svého kódu nevolejte.)
lookup(?string $type, bool $throw=true): ?IComponent .[method]
--------------------------------------------------------------
Vyhledá v hierarchii směrem nahoru objekt požadované třídy nebo rozhraní. Například `$component->lookup(Nette\Application\UI\Presenter::class)` vrací presenter, pokud je k němu, i přes několik úrovní, komponenta připojena. Pokud žádný odpovídající objekt neexistuje, vyhodí výjimku; při předání `false` jako druhého argumentu vrátí místo toho `null`. Když jako `$type` předáte `null`, metoda hledá nejvyšší komponentu ve stromu, tj. kořen bez rodiče.
lookupPath(?string $type=null, bool $throw=true): ?string .[method]
-------------------------------------------------------------------
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::class)` vrací jedinečný identifikátor komponenty vůči presenteru. Když je `$type` `null` (nebo vynechán), cesta se měří až ke kořeni stromu.
Container
=========
[api:Nette\ComponentModel\Container] je rodičovská komponenta, tj. komponenta obsahující potomky a tvořící tak stromovou strukturu. Disponuje metodami pro snadné přidávání, získávání a odstraňování objektů. Je předkem například formuláře či tříd `Control` a `Presenter`. Potomci využívající trait `ArrayAccess` (například `Control` a `Presenter`) navíc umožňují přístup k potomkům zápisem pole, např. `$container['child']`.
addComponent(IComponent $component, ?string $name, ?string $insertBefore=null): static .[method]
------------------------------------------------------------------------------------------------
Přidá komponentu do kontejneru jako potomka. Je-li `$name` roven `null`, použije se vlastní název komponenty. Volitelným `$insertBefore` - názvem existujícího potomka - vložíte novou komponentu těsně před něj; jinak se přidá na konec. Metoda vrací kontejner, takže lze volání řetězit.
Název smí obsahovat jen písmena, číslice a podtržítka, protože pomlčka spojuje názvy do cesty (viz [getComponent() |#getComponent]); jiný název vyhodí `Nette\InvalidArgumentException`. Pokud už kontejner má potomka toho jména nebo komponenta už má rodiče, metoda vyhodí `Nette\InvalidStateException`.
removeComponent(IComponent $component): void .[method]
------------------------------------------------------
Odebere potomka z kontejneru. Pokud komponenta jeho potomkem není, vyhodí `Nette\InvalidArgumentException`.
Komponenta zůstává v kontejneru, dokud neproběhnou její callbacky `$detached` (viz [monitorování předků |#Monitorování předků]), takže ji tyto callbacky pořád vidí na jejím místě. .{data-version:4.0.2}
getComponent(string $name, bool $throw=true): ?IComponent .[method]
-------------------------------------------------------------------
Vrací komponentu. Při pokusu o získání nedefinovaného potomka je zavolána továrna `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ám říkáme továrny na komponenty a mohou je implementovat potomci třídy `Container`.
Název může být i cesta k hlouběji vnořenému potomkovi, jména se v ní spojují pomlčkou: `$form->getComponent('address-street')` vrátí komponentu `street` uvnitř kontejneru `address`. Pokud taková komponenta neexistuje, metoda vyhodí `Nette\InvalidArgumentException`, a když najde podobný název, rovnou ho nabídne, například `Component with name 'adress' does not exist, did you mean 'address'?`. Když jako druhý argument předáte `false`, dostanete místo toho `null`.
getComponents(): IComponent[] .[method]
---------------------------------------
Vrací přímé potomky jako pole; klíče obsahují názvy těchto komponent. Pro získání celého podstromu rekurzivně použijte `getComponentTree()`, případně v kombinaci s `array_filter()` pro filtrování podle typu. (Parametry `$deep` a `$filterType` známé ze starších verzí byly ve verzi 4.0 odstraněny.)
getComponentTree(): list<IComponent> .[method]
----------------------------------------------
Získá celou hierarchii komponent včetně všech vnořených podřízených komponent jako indexované pole. Prohledávání jde nejprve do hloubky.
Monitorování předků
===================
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í.
Jak může komponenta zjistit okamžik, kdy je připojena pod presenter - nebo pod jiného předka daného typu? Sledovat přímého rodiče nestačí, protože spojení může nastat výš ve stromu, například když se připojí rodič rodiče. Přesně k tomu slouží metoda [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]: komponenta oznámí, že chce být upozorněna, kdykoli se nad ní ve stromu objeví předek třídy nebo rozhraní `$type`, nebo z něj zmizí. Komponenta může monitorovat libovolný počet typů; callback `$attached` se spustí při připojení odpovídajícího předka a dostane ho jako argument, callback `$detached` při jeho odpojení. Monitorování lze opět ukončit metodou `unmonitor($type)`.
Pokud předek ve stromu už je, když `monitor()` zavoláte, spustí se callback `$attached` hned. Nezáleží tedy na tom, jestli komponenta začne monitorovat před připojením, nebo až po něm.
Oznámení kopírují strukturu stromu. Při připojování je předek uvědoměn dříve než jeho potomci (odshora dolů), takže rodič může nejprve připravit sdílený stav, případně potomka odebrat ještě předtím, než se spustí jeho vlastní callback. Při odpojování je pořadí opačné - nejdříve jsou uvědoměni potomci. Callback registrovaný pro několik typů se pro tentýž předek zavolá jen jednou a druhá registrace stejného callbacku nemá žádný účinek. Důvody tohoto chování popisuje [článek na blogu k verzi 4.0 |https://blog.nette.org/cs/component-model-4-0].
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:
```php
class UploadControl extends Nette\Forms\Controls\BaseControl
{
public function __construct($label)
{
$this->monitor(Nette\Forms\Form::class, function ($form): void {
$form->setHtmlAttribute('enctype', 'multipart/form-data');
});
// ...
}
// ...
}
```
a jakmile je formulář k dispozici, zavolá se callback.
Odmítnutí připojení .{data-version:4.0.2}
-----------------------------------------
Callback může změnu i zastavit. Když `$attached` vyhodí výjimku, komponenta se vůbec nepřipojí: změna se vrátí, callbacky, které už proběhly, dostanou svůj protějšek `$detached`, a výjimka pokračuje ke kódu, který komponentu přidával. Takhle Nette Forms nedovolí vložit formulář do jiného formuláře. Tady zase průvodce odmítá žít uvnitř formuláře:
```php
class Wizard extends Nette\ComponentModel\Container
{
public function __construct()
{
$this->monitor(Nette\Forms\Form::class, function (): void {
throw new Nette\InvalidStateException('A wizard cannot be placed inside a form.');
});
}
}
$form->addComponent(new Wizard, 'wizard'); // vyhodí výjimku, $form zůstane, jak byl
```
Stejně to funguje i opačně: callback `$detached`, který vyhodí výjimku, nechá komponentu na jejím místě.
Změny stromu v callbacku
------------------------
Callbacky smějí strom samy měnit: odebrat vlastní komponentu, sourozence nebo předka. Oznámení s tím počítají, takže komponenta, kterou ze stromu odebral některý dřívější callback, už oznámení nedostane.
Callback `$detached` běží ještě předtím, než se komponenta skutečně odpojí, takže `lookup()` v něm pořád najde předka, od kterého se odpojuje. .{data-version:4.0.2}
Klonování a serializace
=======================
Komponentu lze zkopírovat pomocí `clone`. Kopie dostane kopie všech svých potomků, ale žádného rodiče: začíná mimo strom, a proto její monitory dostanou `$detached` od předků originálu. Uvnitř kopie `lookup()` vidí vlastní strom kopie.
Komponenty nelze serializovat; `serialize()` vyhodí `Nette\NotImplementedException`.
Pokud aktualizujete balíček na novější verzi, podívejte se na stránku [upgrade|upgrading].
{{maintitle: Komponentový model Nette: třídy Component a Container}}
{{description: Formuláře, jejich prvky i vizuální komponenty v Nette dědí od tříd Component a Container. Jak funguje pojmenování, monitorování předků a klonování.}}
Komponentový model
Důležitým pojmem v Nette je komponenta. Do stránek vkládáme vizuální interaktivní komponenty, komponentami jsou i formuláře
nebo všechny jejich prvky. Základní dvě třídy, od kterých všechny tyto komponenty dědí, jsou součástí balíčku
nette/component-model a mají za úkol vytvářet stromovou hierarchii komponent.
Component
Nette\ComponentModel\Component
je společným předkem všech komponent. Obsahuje metody getName() vracející název komponenty a metodu
getParent() vracející jejího rodiče. Ani jedno nenastavujete sami: obojí nastaví kontejner ve chvíli, kdy do
něj komponentu přidáte. (Používá k tomu metodu setParent(), která je interní, takže ji ze svého kódu
nevolejte.)
lookup (?string $type, bool
$throw=true): ?IComponent
Vyhledá v hierarchii směrem nahoru objekt požadované třídy nebo rozhraní. Například
$component->lookup(Nette\Application\UI\Presenter::class) vrací presenter, pokud je k němu, i přes několik
úrovní, komponenta připojena. Pokud žádný odpovídající objekt neexistuje, vyhodí výjimku; při předání
false jako druhého argumentu vrátí místo toho null. Když jako $type předáte
null, metoda hledá nejvyšší komponentu ve stromu, tj. kořen bez rodiče.
lookupPath (?string $type=null, bool
$throw=true): ?string
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::class) vrací jedinečný identifikátor
komponenty vůči presenteru. Když je $type null (nebo vynechán), cesta se měří až ke kořeni
stromu.
Container
Nette\ComponentModel\Container
je rodičovská komponenta, tj. komponenta obsahující potomky a tvořící tak stromovou strukturu. Disponuje metodami pro
snadné přidávání, získávání a odstraňování objektů. Je předkem například formuláře či tříd
Control a Presenter. Potomci využívající trait ArrayAccess (například
Control a Presenter) navíc umožňují přístup k potomkům zápisem pole, např.
$container['child'].
addComponent (IComponent $component,
?string $name, ?string $insertBefore=null): static
Přidá komponentu do kontejneru jako potomka. Je-li $name roven null, použije se vlastní název
komponenty. Volitelným $insertBefore – názvem existujícího potomka – vložíte novou komponentu těsně
před něj; jinak se přidá na konec. Metoda vrací kontejner, takže lze volání řetězit.
Název smí obsahovat jen písmena, číslice a podtržítka, protože pomlčka spojuje názvy do cesty (viz getComponent()); jiný název vyhodí Nette\InvalidArgumentException. Pokud už
kontejner má potomka toho jména nebo komponenta už má rodiče, metoda vyhodí Nette\InvalidStateException.
removeComponent (IComponent
$component): void
Odebere potomka z kontejneru. Pokud komponenta jeho potomkem není, vyhodí Nette\InvalidArgumentException.
Komponenta zůstává v kontejneru, dokud neproběhnou její callbacky $detached (viz monitorování předků), takže ji tyto callbacky pořád vidí na jejím místě.
getComponent (string $name, bool
$throw=true): ?IComponent
Vrací komponentu. Při pokusu o získání nedefinovaného potomka je zavolána továrna 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ám říkáme továrny na komponenty a mohou je
implementovat potomci třídy Container.
Název může být i cesta k hlouběji vnořenému potomkovi, jména se v ní spojují pomlčkou:
$form->getComponent('address-street') vrátí komponentu street uvnitř kontejneru
address. Pokud taková komponenta neexistuje, metoda vyhodí Nette\InvalidArgumentException, a když
najde podobný název, rovnou ho nabídne, například
Component with name 'adress' does not exist, did you mean 'address'?. Když jako druhý argument předáte
false, dostanete místo toho null.
getComponents(): IComponent[]
Vrací přímé potomky jako pole; klíče obsahují názvy těchto komponent. Pro získání celého podstromu rekurzivně
použijte getComponentTree(), případně v kombinaci s array_filter() pro filtrování podle typu.
(Parametry $deep a $filterType známé ze starších verzí byly ve verzi 4.0 odstraněny.)
getComponentTree():
list<IComponent>
Získá celou hierarchii komponent včetně všech vnořených podřízených komponent jako indexované pole. Prohledávání
jde nejprve do hloubky.
Monitorování předků
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í.
Jak může komponenta zjistit okamžik, kdy je připojena pod presenter – nebo pod jiného předka daného typu? Sledovat
přímého rodiče nestačí, protože spojení může nastat výš ve stromu, například když se připojí rodič rodiče.
Přesně k tomu slouží metoda monitor($type, $attached,
$detached): komponenta oznámí, že chce být upozorněna, kdykoli se nad ní ve stromu objeví předek třídy nebo
rozhraní $type, nebo z něj zmizí. Komponenta může monitorovat libovolný počet typů; callback
$attached se spustí při připojení odpovídajícího předka a dostane ho jako argument, callback
$detached při jeho odpojení. Monitorování lze opět ukončit metodou unmonitor($type).
Pokud předek ve stromu už je, když monitor() zavoláte, spustí se callback $attached hned.
Nezáleží tedy na tom, jestli komponenta začne monitorovat před připojením, nebo až po něm.
Oznámení kopírují strukturu stromu. Při připojování je předek uvědoměn dříve než jeho potomci (odshora dolů),
takže rodič může nejprve připravit sdílený stav, případně potomka odebrat ještě předtím, než se spustí jeho
vlastní callback. Při odpojování je pořadí opačné – nejdříve jsou uvědoměni potomci. Callback registrovaný pro
několik typů se pro tentýž předek zavolá jen jednou a druhá registrace stejného callbacku nemá žádný účinek. Důvody
tohoto chování popisuje článek na blogu k verzi 4.0.
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::class, function ($form): void {
$form->setHtmlAttribute('enctype', 'multipart/form-data');
});
// ...
}
// ...
}
a jakmile je formulář k dispozici, zavolá se callback.
Odmítnutí připojení
Callback může změnu i zastavit. Když $attached vyhodí výjimku, komponenta se vůbec nepřipojí: změna se
vrátí, callbacky, které už proběhly, dostanou svůj protějšek $detached, a výjimka pokračuje ke kódu,
který komponentu přidával. Takhle Nette Forms nedovolí vložit formulář do jiného formuláře. Tady zase průvodce odmítá
žít uvnitř formuláře:
class Wizard extends Nette\ComponentModel\Container
{
public function __construct()
{
$this->monitor(Nette\Forms\Form::class, function (): void {
throw new Nette\InvalidStateException('A wizard cannot be placed inside a form.');
});
}
}
$form->addComponent(new Wizard, 'wizard'); // vyhodí výjimku, $form zůstane, jak byl
Stejně to funguje i opačně: callback $detached, který vyhodí výjimku, nechá komponentu na
jejím místě.
Změny stromu v callbacku
Callbacky smějí strom samy měnit: odebrat vlastní komponentu, sourozence nebo předka. Oznámení s tím počítají,
takže komponenta, kterou ze stromu odebral některý dřívější callback, už oznámení nedostane.
Callback $detached běží ještě předtím, než se komponenta skutečně odpojí, takže
lookup() v něm pořád najde předka, od kterého se odpojuje.
Klonování a serializace
Komponentu lze zkopírovat pomocí clone. Kopie dostane kopie všech svých potomků, ale žádného rodiče:
začíná mimo strom, a proto její monitory dostanou $detached od předků originálu. Uvnitř kopie
lookup() vidí vlastní strom kopie.
Komponenty nelze serializovat; serialize() vyhodí Nette\NotImplementedException.
Pokud aktualizujete balíček na novější verzi, podívejte se na stránku upgrade.