Kompilace kontejneru do hloubky
Tato stránka odkrývá, co se děje, když Nette sestavuje váš DI kontejner: jakými fázemi prochází, kdy
se rozbalují konfigurační parametry, kdy se z řetězců @service stávají skutečné reference a – to je
otázka, kterou si autoři rozšíření kladou nejčastěji – ve které fázi můžete bezpečně hledat služby podle typu.
Je to hlubší doplněk k tvorbě rozšíření.
Nic z toho nepotřebujete k napsání běžné aplikace, ani běžného rozšíření. Jakmile ale vaše rozšíření začne
zkoumat nebo přetvářet graf služeb, začne na načasování záležet všechno: totéž volání getByType() dá
v jedné fázi spolehlivou odpověď a v jiné zavádějící. Tato stránka vysvětluje proč, abyste vždy věděli, kam váš
kód patří.
Dva světy: kompilace vs. běh aplikace
Nejdůležitější věc k pochopení je, že se Nette kontejner nesestavuje při každém requestu. Sestaví se jednou
do optimalizované PHP třídy, ta se uloží na disk a každý další request pak hotový soubor už jen načte přes
include. Celý mechanismus popsaný níže – rozšíření, resolvery, generátor kódu – běží jen při
(re)kompilaci.
To rozděluje svět na dvě reprezentace, které nikdy neexistují zároveň:
| při kompilaci | za běhu | |
|---|---|---|
| Co existuje | definice (recepty) v ContainerBuilder |
instance služeb v Container |
| Klíčové třídy | Compiler, ContainerBuilder, Resolver, PhpGenerator |
Container (rodič vygenerované třídy) |
%param%, @service |
textové značky, které se stále překládají | už přeložené / zapečené do kódu |
Vygenerovaná třída dědí z Nette\DI\Container a pro každou službu má metodu
createServiceXxx(). Její parametry i autowiring metadata jsou předpočítané, takže za běhu už není co
řešit – jen na požádání vytvořit instance služeb.
V debug módu se kontejner překompiluje automaticky, kdykoli se změní konfigurační soubor nebo třída rozšíření; obojí se sleduje jako závislost. V produkci se zkompiluje jednou a už se nikdy nekontroluje, a právě odtud plyne rychlost.
Fáze v kostce
Kompilaci řídí Compiler::compile() a scvrkává se na tři kroky:
public function compile(): string
{
$this->processExtensions(); // FÁZE A: schémata + loadConfiguration()
$this->processBeforeCompile(); // FÁZE B: resolve + beforeCompile() + complete
return $this->generateCode(); // FÁZE C: generování kódu + afterCompile()
}
Celý mentální model se vejde do jediné myšlenky – každá fáze ví víc než ta předchozí:
- Fáze A naplní graf definicemi. Typy služeb ještě nejsou spolehlivě známé, protože typ může plynout z návratové hodnoty továrny, na kterou se zatím nikdo nepodíval.
- Fáze B nejdřív vyřeší všechny typy (
resolve), pak dá rozšířením šanci graf přetvořit (beforeCompile) a nakonec autowiruje argumenty (complete). - Fáze C z hotového grafu vygeneruje PHP a nechá rozšíření sáhnout do vygenerovaného kódu.
Právě tato rostoucí znalost je důvod, proč je táž operace v jedné fázi bezpečná a v jiné nespolehlivá. Zbytek stránky prochází fáze právě touto optikou.
Fáze A: registrace definic
V této fázi Nette volá na každém rozšíření tři metody – getConfigSchema(), pak
setConfig() a nakonec loadConfiguration() – ale v pečlivě řízeném pořadí, protože
tady na pořadí opravdu záleží.
Proč na pořadí záleží
ParametersExtensionaExtensionsExtensionjdou první. První musí proběhnout dřív než cokoli jiného, aby mohlo rozbalit%param%v celé konfiguraci – každé další rozšíření pak dostane svoji sekci s už dosazenými hodnotami. Druhé registruje další rozšíření uvedená v sekciextensions:, takže také musí být na světě dřív, než přijdou na řadu ostatní.ServicesExtensionjde poslední. Uživatelská sekceservices:má tak vždy poslední slovo a může přepsat cokoli, co rozšíření nastavila.InjectExtensionse přesune úplně na konec, aby jeho práce viděla setupy přidané všemi ostatními rozšířeními.
Co si z toho odnést: v okamžiku, kdy běží loadConfiguration() vašeho rozšíření, jsou parametry už
rozbalené, ale uživatelské služby tam ještě nejsou. Tenhle jediný fakt řídí většinu časových pravidel níže.
Ze services: na objekty definic
Uživatelská sekce services: se právě tady, v posledním kroku fáze A, převede na objekty definic. Každý NEON záznam se
znormalizuje (sjednotí se zkratkové zápisy), rozpozná se jeho druh (běžná služba, továrna, accessor, …) a v builderu
vznikne odpovídající definice. Tady také poprvé jednoduché argumenty @name / @Type získávají
podobu reference – viz dále.
Na konci fáze A je graf kompletní co do počtu – všechna rozšíření i uživatel zaregistrovali, co chtěli – ale obraz ještě není ostrý:
- typy nejsou vyřešené u definic, jejichž typ plyne z návratové hodnoty továrny,
- argumenty nejsou autowirované,
- některé reference
@servicejsou stále jen stringy.
Přesně proto je hledání podle typu tady nespolehlivé – o tom více dále.
Parametry: kdy se rozbalují %param%
Jedna ze dvou hlavních otázek. Odpověď je krátká: jednorázově, na úplném začátku fáze A, přes celý konfigurační strom.
ParametersExtension běží první a jednou z prvních věcí, které dělá, je rozbalení placeholderů
%param% – nejdřív uvnitř samotných parametrů (parametr smí odkazovat na jiný), pak v celém zbytku
konfigurace. Takže než jakékoli jiné rozšíření, včetně ServicesExtension, dostane svou sekci, jsou
placeholdery už pryč. Rozšíření pracují s konkrétními hodnotami, nikdy s %...%.
Když placeholder tvoří celý řetězec, vrátí se jeho hodnota jak je – včetně polí a objektů – takže
se %mailer% může rozbalit na celé pole. Kdekoli jinde se konkatenuje do stringu a tečková notace
%foo.bar% sahá do vnořených polí.
Statické vs. dynamické parametry
Ne každou hodnotu lze zapéct do kódu. Parametr, jehož hodnota se liší podle prostředí – proměnná prostředí,
baseUrl odvozená z requestu – musí zůstat dynamický. Takové parametry ohlásíte přes
setDynamicParameterNames() nebo Expect::...->dynamic() ve schématu; více v dynamických parametrech.
Dynamický parametr se nenahradí hodnotou, ale výrazem, který ji přečte až za běhu. Takže
%env.DB_HOST% se nezapeče do stringu; stane se z něj runtime přístup ve vygenerovaném kontejneru. Všechno
ostatní je statické a zapeče se v čase kompilace – a odtud plyne obvyklé překvapení „moje hodnota z
getenv() je v každém prostředí stejná“: ten parametr byl prostě statický.
Opačná operace je escapování: aby se doslovné % nebo @ nebralo jako placeholder či
reference, zdvojí se (%%, @@). Nette to dělá automaticky u parametrů, které vkládá za vás,
takže se jejich hodnoty nikdy nezamění za placeholder nebo referenci.
Reference: kdy se @service mění na Reference
Druhá hlavní otázka. Překlad @service probíhá v několika krocích napříč různými fázemi,
podle toho, jak složitý ten řetězec je. Málokdy potřebujete tohle sledovat ručně, ale znalost tvaru vysvětluje, proč se
některé reference vyřeší dřív než jiné.
- Parsování (načtení configu).
@servicepoužitý jako entita – to, co službu vytváří, jako veFoo(@bar)– se stane referencí okamžitě.@servicepoužitý jako argument zůstává prozatím stringem.@v uvozovkách se escapuje na@@, takže se bere jako doslovný text, ne jako reference. - Fáze A (
loadConfiguration). Když se zpracovávají definice, čistý argument@namenebo@Typese překlopí na objektReference. To chytí jen jednoduché tvary;@service::CONSTnebo@uvnitř složitějšího výrazu jde na později. - Fáze B (
complete). Tady proběhne skutečný „chytrý" překlad:@service→ reference,@service::CONSTANT→ literál konstanty třídy,@service::property→ čtení té property,@@x→ doslovný text@x.
Ve slově reference se skrývá druhý překlad. Reference může ukazovat buď podle jména, nebo
podle typu (@Namespace\Type). Typová reference ještě není jméno služby – na konkrétní jméno
se vyřeší autowiringem, a to až v kroku complete, jakmile je postavený autowiring index. To je můstek k další
sekci: vyhledávání autowiringem se záměrně odkládá, dokud není index hotový.
| Tvar | Na referenci/výraz ve fázi | Na konkrétní službu ve fázi |
|---|---|---|
entita (@foo jako továrna) |
parsování | complete |
argument @foo, @Type |
fáze A | complete |
@foo::CONST, @foo::prop |
fáze B | complete |
typová reference @Type |
fáze A/B | complete (autowiring) |
Introspekce ContainerBuilder: kdy je bezpečná
Teď otázka, kterou si autoři rozšíření kladou nejčastěji: ve které metodě můžu hledat služby podle typu? Odpověď plyne z jednoho prostého pravidla o tom, jak si builder hlídá svůj vlastní stav.
Hledání podle typu (getByType(), getDefinitionByType(), findByType())
vyžaduje, aby byl graf služeb vyřešený – každý typ známý, autowiring index postavený. Takže kdykoli některé
z nich zavoláte a graf se od posledního resolve změnil, builder na místě vyřeší celý dosud známý graf. Během
samotného resolve je jakékoli hledání podle typu zakázané a vyhodí NotAllowedDuringResolvingException.
Hledání podle tagu (findByTag()) takový požadavek nemá – tagy na typech nezávisí, takže funguje
v každé fázi.
Fáze po fázi:
loadConfiguration()(fáze A) – hledání podle typu je nespolehlivé. Graf je neúplný: rozšíření, která běží později, ještě neregistrovala své služby, a hlavně tu ještě není uživatelskáservices:(ta běží poslední). VolánígetByType()sice funguje – spustí předčasný resolve části grafu – ale odpověď je z neúplného obrazu a předčasný resolve stojí výkon. Pravidlo: vloadConfiguration()jen registrujte definice; nehledejte podle typu.findByTag()je v pořádku.beforeCompile()(fáze B) – správné místo pro introspekci. Teď už existují všechny definice (i uživatelské), typy jsou vyřešené a autowiring index je postavený, takžegetByType(),findByType()ifindByTag()vrací spolehlivé odpovědi. Argumenty ještě nejsou autowirované – to je až úplně další krok (complete), po všech voláníchbeforeCompile(). Když tu definici změníte, dalšígetByType()graf transparentně přeresolvuje, takže můžete volně střídat úpravy a dotazy.afterCompile()(fáze C) – už jen kód. Pracuje nad vygenerovanou třídou, ne nad builderem. Graf je hotový; tady tvarujete výsledné PHP.
| Chci… | Fáze |
|---|---|
| zaregistrovat službu | loadConfiguration() |
| hledat podle tagu a upravit definice | loadConfiguration() nebo beforeCompile() |
hledat podle typu (getByType/findByType) |
beforeCompile() |
| zjistit, které služby autowiring dosadil do argumentů | při kompilaci ne – až za běhu |
| sáhnout do generovaného kódu | afterCompile() |
| spustit kód po startu kontejneru | inicializační kód |
Uvnitř fáze B: resolve a complete
Fáze B jsou dva průchody s voláními beforeCompile() vloženými mezi ně:
$this->builder->resolve(); // typy vyřešené, autowiring index postavený
foreach ($this->extensions as $extension) {
$extension->beforeCompile();
}
$this->builder->complete(); // AŽ TEĎ se autowirují argumenty
resolve() určí typ každé služby – vezme se z jejího type, nebo se odvodí z její
továrny: z návratového typu tovární metody, z vytvářené třídy nebo ze služby, na kterou míří reference – a pak
postaví autowiring index, který mapuje každý typ (třídu plus její rodiče a rozhraní) na jméno služby. Služba
označená autowired: false se do indexu nezanese; autowired: [A, B] zúží typy, pod kterými je
viditelná. Klíčové: resolve řeší typy, ne argumenty – autowiring argumentů by potřeboval hotový index,
který existuje až po tomto průchodu.
complete() je místo, kde se autowiring argumentů skutečně provede. Pro každou definici doplní
chybějící argumenty konstruktoru a setupů tím, že jejich typy dohledá v nyní už hotovém indexu. Právě proto se
typové reference nechávaly během resolve nevyřešené: to dohledání patří sem, jakmile je do čeho spolehlivě
nahlížet.
Fáze C: generování kódu
generateCode() předá hotový graf PhpGeneratoru, který vytvoří třídu dědící z
Container s metodou createServiceXxx() pro každou službu a s předpočítanými metadaty
aliases, tags a wiring. Každý Statement se stane PHP textem
(new Foo(...), volání metod, přístup k property) a každá Reference voláním
$this->getService(...).
Rozšíření pak dostanou závěrečný průchod afterCompile() nad vygenerovanou třídou – tady se
například vygenerují gettery statických a dynamických parametrů – plus možnost přidat inicializační kód, který běží
při každém requestu.
Časová osa na jednom obrázku
KOMPILACE (jednou, do cache)
│
├─ načtení config souborů NEON -> Statement/pole; merge souborů
│ @ v uvozovkách -> @@ ; entity -> Statement
│
▼ Compiler::compile()
│
├─ FÁZE A processExtensions()
│ ├─ ParametersExtension (PRVNÍ) ── %param% ROZBALENY v celém configu
│ │ dynamické -> runtime výraz
│ ├─ ExtensionsExtension (PRVNÍ) ── registruje další rozšíření
│ ├─ ...ostatní rozšíření... ── loadConfiguration(): jen registrujte definice
│ └─ ServicesExtension (POSLEDNÍ)── services: -> objekty Definition
│ @name/@Type -> Reference
│ [graf úplný co do počtu; TYPY a ARGUMENTY ještě ne; hledání podle typu nespolehlivé]
│
├─ FÁZE B processBeforeCompile()
│ ├─ builder.resolve() ── vyřeš všechny typy; postav autowiring index
│ │ [typy hotové; index hotový]
│ ├─ beforeCompile() rozšíření ── ZDE bezpečné getByType/findByType/findByTag
│ │ (argumenty ještě nejsou autowirované)
│ └─ builder.complete() ── autowiruj ARGUMENTY; dokonči překlad referencí
│ typové reference -> jména služeb
│
└─ FÁZE C generateCode()
├─ PhpGenerator.generate() ── Statement -> PHP; metody createServiceXxx()
├─ afterCompile() rozšíření ── úpravy kódu; gettery parametrů
└─ toString() ── výsledný PHP kód -> cache
────────────────────────────────────────────────────────────
BĚH (každý request)
│
├─ new Container($dynamicParams)
├─ initialize() ── boot kód rozšíření (session, hlavičky, validace)
└─ getService()/getByType() ── lazy instance z předpočítaných metadat
Nejčastější omyly
- „V
loadConfiguration()si najdu služby podle typu." Ne – graf je neúplný (uživatelskáservices:běží až po vás) agetByType()spustí předčasný resolve neúplného grafu. Přesuňte to dobeforeCompile().findByTag()je v pořádku i zde. - „Hodnota z
getenv()v parametru bude v každém prostředí jiná." Jen když je parametr dynamický. Jinak se zapeče v čase kompilace a je všude stejná. - „Reference
@Typeje hned jméno služby." Není – je to typová reference, na konkrétní jméno se vyřeší autowiringem až v kroku complete. - „Moje rozšíření čte pomocný soubor, ale změny se neprojeví." Registrujte ho přes
$builder->addDependency($file), jinak o něm cache neví a nepřekompiluje se. - „Během
resolve()můžu volatgetByType()." Ne – vyhodí výjimku. Hledání podle typu patří dobeforeCompile()nebo později, nikdy ne doprostřed resolvingu.