La compilazione del container in dettaglio
Questa pagina apre la compilazione del container: le fasi che attraversa, quando i parametri di configurazione
vengono espansi, quando le stringhe @service diventano riferimenti reali e, la domanda che gli autori di estensioni
pongono più spesso, in quale fase si possono cercare in sicurezza i servizi per tipo. È il complemento approfondito di Creare estensioni.
Non vi serve nulla di tutto questo per scrivere una normale applicazione, e nemmeno una normale estensione. Ma quando la vostra
estensione inizia a esaminare o a rimodellare il grafo dei servizi, il momento diventa tutto: la stessa chiamata a
getByType() dà una risposta affidabile in una fase e una fuorviante in un'altra. Questa pagina spiega perché, così
saprete sempre dove collocare il vostro codice.
Due mondi: compilazione ed esecuzione
La cosa più importante da capire è che un container di Nette non viene assemblato a ogni richiesta. Viene costruito
una sola volta in una classe PHP ottimizzata, quella classe viene salvata su disco e ogni richiesta successiva si limita a fare
include del file già pronto. Tutta la macchina descritta qui sotto (estensioni, resolver, generatore di codice) gira
solo durante la (ri)compilazione.
Questo divide il mondo in due rappresentazioni che non coesistono mai:
| durante la compilazione | in fase di esecuzione | |
|---|---|---|
| Cosa esiste | definizioni (ricette) in ContainerBuilder |
istanze dei servizi in Container |
| Classi chiave | Compiler, ContainerBuilder, Resolver, PhpGenerator |
Container (genitore della classe generata) |
%param%, @service |
marcatori testuali ancora da tradurre | già tradotti / incorporati nel codice |
La classe generata estende Nette\DI\Container e ha un metodo createServiceXxx() per ogni servizio.
I suoi parametri e i metadati dell'autowiring sono precalcolati, quindi in fase di esecuzione non resta nulla da risolvere: solo
da istanziare i servizi su richiesta.
In modalità di sviluppo il container viene ricostruito automaticamente ogni volta che cambiano un file di configurazione o una classe di estensione; entrambi sono tracciati come dipendenze. In produzione viene compilato una sola volta e non viene più controllato, ed è da lì che viene la velocità.
Le fasi a colpo d'occhio
La compilazione è orchestrata da Compiler::compile() e si riduce a tre passaggi:
public function compile(): string
{
$this->processExtensions(); // FASE A: schemi + loadConfiguration()
$this->processBeforeCompile(); // FASE B: resolve + beforeCompile() + complete
return $this->generateCode(); // FASE C: generazione del codice + afterCompile()
}
L'intero modello mentale sta in un'unica idea: ogni fase sa più della precedente.
- La fase A riempie il grafo di definizioni. I tipi dei servizi non sono ancora noti con certezza, perché un tipo può provenire dal valore di ritorno di una factory che nessuno ha ancora esaminato.
- La fase B risolve prima tutti i tipi (
resolve), poi lascia che le estensioni rimodellino il grafo (beforeCompile) e infine esegue l'autowiring degli argomenti (complete). - La fase C trasforma il grafo finito in PHP e lascia che le estensioni intervengano sul codice generato.
È proprio questa conoscenza crescente a rendere la stessa operazione sicura in una fase e inaffidabile in un'altra. Il resto di questa pagina percorre le fasi tenendo a mente questa idea.
Fase A: registrazione delle definizioni
In questa fase Nette chiama tre metodi su ogni estensione (getConfigSchema(), poi setConfig(), poi
loadConfiguration()), ma in un ordine attentamente controllato, perché qui l'ordine conta davvero.
Perché l'ordine conta
ParametersExtensionedExtensionsExtensionvengono per prime. La prima deve girare prima di tutto il resto per poter espandere%param%in tutta la configurazione: ogni altra estensione riceve poi la propria sezione con i valori già inseriti. La seconda registra le ulteriori estensioni elencate nella sezioneextensions:, quindi anche lei deve esistere prima che vengano elaborate le altre.ServicesExtensionviene per ultima. La sezioneservices:dell'utente ha quindi sempre l'ultima parola e può sovrascrivere qualsiasi cosa impostata dalle estensioni.InjectExtensionè spostata proprio alla fine, così che il suo lavoro veda i setup aggiunti da tutte le altre estensioni.
La conclusione per voi: quando il loadConfiguration() della vostra estensione viene eseguito, i parametri sono
già espansi, ma i servizi dell'utente non ci sono ancora. Questo solo fatto guida quasi tutte le regole temporali
qui sotto.
Da services: alle definizioni
La sezione services: dell'utente viene trasformata qui in oggetti
definizione, nell'ultimo passaggio della fase A. Ogni voce NEON viene normalizzata (le notazioni abbreviate vengono
uniformate), ne viene rilevato il tipo (servizio comune, factory, accessor, …) e nel builder viene creata la definizione
corrispondente. È anche il primo momento in cui i semplici argomenti @name / @Type diventano
riferimenti, vedi qui sotto.
Alla fine della fase A tutte le definizioni sono presenti (ogni estensione e l'utente hanno registrato quel che volevano), ma il quadro non è ancora nitido:
- i tipi non sono risolti per le definizioni il cui tipo proviene dal valore di ritorno di una factory,
- gli argomenti non hanno subito l'autowiring,
- alcuni riferimenti
@servicesono ancora semplici stringhe.
È proprio per questo che qui la ricerca per tipo è inaffidabile: maggiori dettagli qui sotto.
Parametri: quando viene espanso %param%
Una delle due domande principali. La risposta è breve: una sola volta, proprio all'inizio della fase A, in tutto l'albero della configurazione.
ParametersExtension gira per prima e una delle prime cose che fa è espandere i segnaposto %param%:
prima dentro i parametri stessi (un parametro può fare riferimento a un altro), poi in tutto il resto della configurazione.
Quando quindi una qualsiasi altra estensione, compresa ServicesExtension, riceve la propria sezione, i segnaposto
sono già spariti. Le estensioni lavorano con valori concreti, mai con %...%.
Quando un segnaposto è l'intera stringa, il suo valore viene restituito così com'è, array e oggetti compresi,
quindi %mailer% può espandersi in un intero array. In qualsiasi altra posizione viene concatenato in una stringa, e
la notazione con il punto %foo.bar% raggiunge gli array annidati.
Parametri statici e dinamici
Non tutti i valori si possono incorporare nel codice. Un parametro il cui valore differisce da un ambiente all'altro (una
variabile d'ambiente, il baseUrl ricavato dalla richiesta) deve restare dinamico. Parametri del genere si
dichiarano con setDynamicParameterNames() oppure con Expect::...->dynamic() in uno schema; maggiori
dettagli in Parametri dinamici.
Un parametro dinamico non viene sostituito da un valore, ma da un'espressione che lo legge in fase di esecuzione.
Perciò %env.DB_HOST% non si congela in una stringa: diventa una lettura a runtime nel container generato. Tutto il
resto è statico e viene congelato in fase di compilazione, ed è la solita fonte della sorpresa „il valore del mio
getenv() è uguale in ogni ambiente“: il parametro era semplicemente statico.
L'operazione opposta è l'escaping: perché un % o una @ letterali non vengano interpretati,
si raddoppiano (%%, @@). Nette lo fa automaticamente per i parametri che inserisce al posto vostro,
così i loro valori non vengono mai scambiati per segnaposto o riferimenti.
Riferimenti: quando @service diventa un riferimento
La seconda domanda principale. La traduzione di @service avviene in più passaggi, in fasi diverse, a
seconda di quanto la stringa sia complessa. Raramente vi serve seguirla a mano, ma conoscere i passaggi spiega perché alcuni
riferimenti si risolvono prima di altri.
- Analisi (caricamento della configurazione). Una
@serviceusata come entità, cioè come la cosa che crea un servizio, come inFoo(@bar), diventa subito un riferimento. Una@serviceusata come argomento resta per ora una semplice stringa. Una@tra apici viene escapata in@@, quindi conta come testo letterale, non come riferimento. - Fase A (
loadConfiguration). Quando le definizioni vengono elaborate, un argomento@nameo@Typepulito viene trasformato in un oggettoReference. Questo riguarda solo le forme semplici;@service::CONSTo una@dentro un'espressione più ampia restano per dopo. - Fase B (
complete). La vera traduzione „intelligente“ avviene qui:@service→ riferimento,@service::CONSTANT→ una costante di classe letterale,@service::property→ la lettura di quella proprietà,@@x→ il testo letterale@x.
C'è una seconda traduzione nascosta nella parola riferimento stessa. Un Reference può puntare per
nome oppure per tipo (@Namespace\Type). Un riferimento per tipo non è ancora un nome di
servizio: viene risolto in un nome concreto dall'autowiring, e questo avviene solo nel passaggio complete, una volta
costruito l'indice dell'autowiring. È il ponte verso la sezione successiva: le ricerche dell'autowiring sono volutamente
rimandate finché l'indice non è pronto.
| Forma | Diventa riferimento/espressione in | Risolta in un servizio concreto in |
|---|---|---|
entità (@foo come factory) |
analisi | complete |
argomento @foo, @Type |
fase A | complete |
@foo::CONST, @foo::prop |
fase B | complete |
riferimento per tipo @Type |
fase A/B | complete (autowiring) |
Esaminare ContainerBuilder: quando è sicuro
Ed ecco la domanda che gli autori di estensioni pongono più spesso: in quale metodo posso cercare i servizi per tipo? La risposta deriva da una semplice regola su come il builder tiene traccia del proprio stato.
La ricerca per tipo (getByType(), getDefinitionByType(), findByType()) richiede
che il grafo dei servizi sia risolto: ogni tipo noto, l'indice dell'autowiring costruito. Perciò ogni volta che chiamate
uno di questi metodi e il grafo è cambiato dall'ultimo resolve, il builder risolve sul posto l'intero grafo noto. Durante
il resolve stesso, qualsiasi ricerca per tipo è vietata e solleva NotAllowedDuringResolvingException.
La ricerca per tag (findByTag()) non ha questo requisito: i tag non dipendono dai tipi, quindi funziona in
ogni fase.
Fase per fase:
loadConfiguration()(fase A): la ricerca per tipo è inaffidabile. Il grafo è incompleto: le estensioni che girano dopo non hanno ancora registrato i propri servizi e, soprattutto, la sezioneservices:dell'utente (che gira per ultima) non c'è. Una chiamata agetByType()funziona, perché provoca un resolve anticipato di un grafo parziale, ma la risposta viene da un quadro incompleto e il resolve prematuro spreca lavoro. Regola pratica: inloadConfiguration()limitatevi a registrare le definizioni; non cercate per tipo.findByTag()va bene.beforeCompile()(fase B): il posto giusto per esaminare. A questo punto tutte le definizioni esistono (comprese quelle dell'utente), i tipi sono risolti e l'indice dell'autowiring è costruito, quindigetByType(),findByType()efindByTag()restituiscono tutti risposte affidabili. Gli argomenti non hanno ancora subito l'autowiring: è il passaggio immediatamente successivo (complete), dopo tutte le chiamate abeforeCompile(). Quando qui modificate una definizione, il successivogetByType()risolve di nuovo il grafo in modo trasparente, così potete alternare liberamente modifiche e interrogazioni.afterCompile()(fase C): solo codice. Lavora sulla classe generata, non sul builder. Il grafo è finito; qui plasmate il PHP risultante.
| Voglio… | Fase |
|---|---|
| registrare un servizio | loadConfiguration() |
| cercare per tag e modificare le definizioni | loadConfiguration() oppure beforeCompile() |
cercare per tipo (getByType/findByType) |
beforeCompile() |
| dipendere dai servizi scelti dall'autowiring per gli argomenti | non in fase di compilazione: esaminatelo a runtime |
| intervenire sul codice generato | afterCompile() |
| eseguire codice dopo l'avvio del container | codice di inizializzazione |
Dentro la fase B: resolve e complete
La fase B è composta da due passaggi, con in mezzo le chiamate a beforeCompile():
$this->builder->resolve(); // tipi risolti, indice dell'autowiring costruito
foreach ($this->extensions as $extension) {
$extension->beforeCompile();
}
$this->builder->complete(); // SOLO ORA gli argomenti subiscono l'autowiring
resolve() determina il tipo di ogni servizio (preso dal suo type dichiarato oppure dedotto
dalla sua factory: il tipo di ritorno di un metodo factory, la classe che istanzia o il servizio a cui punta un riferimento) e
costruisce poi l'indice dell'autowiring, che associa ogni tipo (la classe più i suoi genitori e le sue interfacce) a un nome di
servizio. Un servizio contrassegnato con autowired: false resta fuori dall'indice; autowired: [A, B]
restringe i tipi sotto i quali è visibile. Fondamentale: il resolve fissa i tipi, non gli argomenti: per
l'autowiring degli argomenti servirebbe l'indice finito, che esiste solo dopo questo passaggio.
complete() è dove avviene davvero l'autowiring degli argomenti. Per ogni definizione riempie gli argomenti
mancanti del costruttore e del setup cercandone i tipi nell'indice ormai completo. È per questo che i riferimenti per tipo
erano rimasti irrisolti durante il resolve: la ricerca appartiene a questa fase, quando esiste un indice affidabile in cui
cercare.
Fase C: generazione del codice
generateCode() consegna il grafo finito a PhpGenerator, che produce una classe che estende
Container con un metodo createServiceXxx() per ogni servizio, più i metadati precalcolati
aliases, tags e wiring. Ogni Statement diventa testo PHP
(new Foo(...), chiamate di metodo, accesso alle proprietà) e ogni Reference diventa una chiamata a
$this->getService(...).
Le estensioni ottengono poi un ultimo passaggio afterCompile() sulla classe generata: è qui che vengono emessi,
per esempio, i getter dei parametri statici e dinamici, ed è qui che potete aggiungere il codice di inizializzazione che gira a ogni richiesta.
La linea del tempo in un'immagine
COMPILAZIONE (una volta sola, nella cache)
│
├─ carica i file di config NEON -> Statement/array; unisce i file
│ @ tra apici -> @@ ; entità -> Statement
│
▼ Compiler::compile()
│
├─ FASE A processExtensions()
│ ├─ ParametersExtension (PRIMA) ── %param% ESPANSI in tutta la config
│ │ quelli dinamici -> espressione a runtime
│ ├─ ExtensionsExtension (PRIMA) ── registra ulteriori estensioni
│ ├─ ...altre estensioni... ── loadConfiguration(): solo registrare definizioni
│ └─ ServicesExtension (ULTIMA) ── services: -> oggetti Definition
│ @name/@Type -> Reference
│ [grafo completo per numero; TIPI e ARGOMENTI non ancora; ricerca per tipo inaffidabile]
│
├─ FASE B processBeforeCompile()
│ ├─ builder.resolve() ── risolve tutti i tipi; costruisce l'indice autowiring
│ │ [tipi pronti; indice pronto]
│ ├─ beforeCompile() estensioni ── qui getByType/findByType/findByTag SICURI
│ │ (gli argomenti non hanno ancora l'autowiring)
│ └─ builder.complete() ── autowiring degli ARGOMENTI; fine traduzione riferimenti
│ riferimenti per tipo -> nomi di servizio
│
└─ FASE C generateCode()
├─ PhpGenerator.generate() ── Statement -> PHP; metodi createServiceXxx()
├─ afterCompile() estensioni ── ritocca il codice; emette i getter dei parametri
└─ toString() ── codice PHP finale -> cache
────────────────────────────────────────────────────────────
ESECUZIONE (a ogni richiesta)
│
├─ new Container($dynamicParams)
├─ initialize() ── codice di avvio delle estensioni (sessione, header, validazione)
└─ getService()/getByType() ── istanze pigre dai metadati precalcolati
Fraintendimenti comuni
- „In
loadConfiguration()cercherò i servizi per tipo.“ No: il grafo è incompleto (la sezioneservices:dell'utente gira dopo di voi) egetByType()provoca un resolve prematuro di un grafo parziale. Spostatelo inbeforeCompile().findByTag()va bene anche qui. - „Il valore di un
getenv()in un parametro sarà diverso in ogni ambiente.“ Solo se il parametro è dinamico. Altrimenti viene incorporato in fase di compilazione e resta uguale ovunque. - „Un riferimento
@Typeè già un nome di servizio.“ Non lo è: è un riferimento per tipo, risolto in un nome concreto dall'autowiring solo nel passaggio complete. - „La mia estensione legge un file di supporto, ma le modifiche non compaiono.“ Registratelo con
$builder->addDependency($file), altrimenti la cache non ne sa nulla e non ricostruirà. - „Durante
resolve()posso chiamaregetByType().“ No: sollevaNotAllowedDuringResolvingException. La ricerca per tipo appartiene abeforeCompile()o a fasi successive, mai in mezzo al resolve.