Component Model 4.0: od shora dolů a bez magie
Component Model je ta nejtišší knihovna v celém Nette. Nikdo o ní nemluví, skoro nikdo ji nepoužívá přímo, a přitom stojí pod každým formulářem a každým Control v aplikaci. Verze 4.0 je z velké části úklid zastaralého API, ale schovává se v ní i jedna opravdová změna chování.
K čemu vlastně Component Model je? Pokud píšete presentery a
formuláře, používáte Component Model každý den, aniž byste o tom
věděli. Definuje základní stavební kameny: Component (něco,
co má jméno a rodiče), Container (komponenta, která může
obsahovat další komponenty). Z nich dědí
Nette\Application\UI\Control, Nette\Forms\Form i
Nette\Forms\Container.
getComponents() zase jen pro přímé potomky
Metoda getComponents() kdysi měla dva parametry:
getComponents($deep, $filterType). Přes ně se daly získat
rekurzivně všechny komponenty v podstromu a rovnou je filtrovat podle typu.
Od těchto parametrů jsme ale postupně ustupovali. Nejdřív jsme je v 3.x
tiše skryli ze signatury, ve 4.0 jsou pryč úplně a jejich použití vyhodí
DeprecatedException. Metoda teď vždy vrací pole přímých
potomků a nic víc.
Pro průchod celým podstromem slouží getComponentTree().
Není to novinka 4.0, existuje už od verze 3.1:
// dříve:
$all = $container->getComponents(true);
// nyní (ale není to totéž)
$all = $container->getComponentTree();
Není to ale jen přejmenování, liší se podoba výsledku. Staré
getComponents(true) vracelo iterátor, kde klíče byly jména
komponent. Metoda getComponentTree() vrací obyčejné pole
s číselnými klíči.
Ty číselné klíče nejsou kosmetika. Jméno komponenty je unikátní jen
v rámci jednoho kontejneru, ne v rámci celého stromu, takže napříč
podstromem klidně narazíte na víc komponent se stejným jménem na různých
úrovních. To byl důvod, proč getComponents(true) nemohlo vracet
pole, ale zmíněný iterátor. Čímž se komplikovala implementace a
signatura. Proto byla snaha parametr true dát pryč a mít raději
dvě prosté metody.
Filtrování podle typu (náhrada $filterType) se v dnešním
PHP udělá snadno:
$forms = array_filter(
$container->getComponentTree(),
fn($c) => $c instanceof Form,
);
Bez magie: konec SmartObjectu
Třída Component přestala používat trait
Nette\SmartObject, který kdysi hlídal přístup
k neexistujícím vlastnostem a přidával magické gettery. V praxi si toho
ale nevšimnete. Verze nette/application 3.3 a nette/forms 3.3, které
s Component Model 4.0 počítají, si SmartObject přidávají přímo do
Control, respektive BaseControl. Pokud tedy píšete
presentery, formuláře nebo vlastní controly, nemění se pro vás nic. Změny
si všimnete jen v případě, že rozšiřujete přímo základní třídy
z Component Modelu.
Pořadí oznamování: od shora dolů
Poslední změna je jediná, která opravdu mění chování, a týká se
monitoringu. To je klíčová vlastnost Component Modelu. Komponenta si
může říct: „dej mi vědět, až mě někdo připojí pod presenter“, a
teprve v tu chvíli, kdy zná svého předka, doplní třeba signály nebo
persistentní parametry. Slouží k tomu metoda monitor():
$this->monitor(Presenter::class, function (Presenter $presenter): void {
// teď už vím, pod jakým presenterem visím
});
A právě tady nastala změna.
Když komponentu připojíte ke stromu, který už je zavěšený pod
nějakým předkem, spustí se všechny relevantní attached
callbacky v podstromu. Otázka zní: v jakém pořadí?
Do verze 3.x se callbacky volaly zdola nahoru (od nejhlubšího potomka k předkovi). Ve 4.0 se volají shora dolů (od předka k potomkům). Vypadá to jako kosmetika, ale jde o opravu nepraktického chování.
Starý algoritmus pracoval ve dvou fázích. Nejdřív prošel celý podstrom a do seznamu si posbíral všechny dvojice (callback, předek). Teprve potom seznam najednou odpálil. Problém je v tom, že seznam byl snímek stromu pořízený předtím, než cokoliv proběhlo.
Jenže callbacky se stromem běžně manipulují. Rodičovská komponenta
může ve svém attached odebrat nebo přesunout potomky, provést
přesměrování, zrušit sama sebe. A protože callbacky potomků už byly
nasbírané, zavolaly se i pro komponenty, které mezitím ze stromu vypadly.
Komponenta dostala oznámení „jsi připojena pod presenterem“ v okamžiku,
kdy už pod ním dávno nebyla.
Nový algoritmus už nepracuje ve dvou fázích. Místo aby si callbacky posbíral dopředu a pak je naráz spustil, prochází strom a u každého uzlu rovnou zavolá jeho callbacky, teprve pak sestoupí k potomkům. Před každým sestupem ověří, že potomek je pořád skutečně potomkem a že už nebyl zpracován. Díky tomu:
- Rodič je oznámen dřív než jeho potomci. To odpovídá intuici i tomu, jak funguje třeba capture fáze událostí v DOM. Než dostane slovo potomek, jeho rodič už měl příležitost připravit půdu.
- Rodič může potomka zastavit. Když rodičovský callback potomka odebere, jeho callback se prostě nezavolá. Strom se prochází živý, ne ze snímku.
- Přesuny a reentrance jsou ošetřené. Pokud callback přesune komponentu do kontejneru, který přijde na řadu později, množina již zpracovaných uzlů zajistí, že se nezpracuje podruhé. Hrubý booleovský zámek z verze 3.x nahradila přesná evidence per-objekt.
- Deduplikace zůstává. Stejná dvojice callback plus konkrétní předek se zavolá právě jednou, i když na téhož předka míří víc monitorů.
Jde tedy o změnu, která dělá chování předvídatelnějším a opravuje případy, kdy se dřív callback volal nad komponentou, která už ve stromu nebyla.
Způsobí to BC break?
Tohle je jediná změna chování, ne jen úklid API, takže si zaslouží
poctivou odpověď. Prošel jsem řadu knihoven postavených nad Component
Modelem a nenašel jsem jediný případ, kde by změna pořadí něco
rozbila. Důvod je prostý: typický attached callback se
dívá jen „nahoru“ na předka, který v tu chvíli existuje, a řeší
sám sebe. Nepředpokládá nic o tom, jestli už proběhl callback sourozence
nebo potomka.
Rozbít se to ale teoreticky může, a to ve dvou situacích:
- Spoléháte na opačné pořadí. Pokud máte dvě komponenty, které
se přes
attachedcallbacky vzájemně koordinují, a potomek dřív zapisoval stav, který předek četl (nebo naopak), nové pořadí to prohodí. Předek teď běží první a uvidí jiný stav než dřív. Tohle je vzácné, protože vyžaduje záměrnou komunikaci mezi komponentami napříč stromem skrz callbacky. - Spoléháte na staré chybné chování. Pokud váš kód nějak využíval to, že se callback potomka zavolal i poté, co ho rodič během připojování odebral, tak se teď tento callback prostě nespustí.
Pokud nic takového neděláte, a to je drtivá většina případů, upgrade na vás z tohoto pohledu nebude mít žádný dopad.
Jak migrovat
Pro většinu projektů je upgrade triviální. Projděte si, jestli se vás netýká něco z následujícího:
- přetěžované
attached()/detached()přepište namonitor($type, $attached, $detached), getComponents(true)nahraďtegetComponentTree(),- filtrování
getComponents(false, Foo::class)nahraďtearray_filter()nadgetComponents()(nebo nadgetComponentTree(), pokud jste filtrovali rekurzivně), - magické
$component->namea spol. nahraďte gettery, - konstantu
NAME_SEPARATORpřejmenujte naNameSeparator, - pokud přetěžujete
addComponent(), doplňte do signatury návratový typ: static(dřív byl jen v phpDocu jako@return static).
Chcete-li odeslat komentář, přihlaste se