Nette Documentation Preview

syntax
La compilazione del container in dettaglio
******************************************

.[perex]
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 |extensions].

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.

.[note]
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:

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

- **`ParametersExtension` ed `ExtensionsExtension` vengono 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 sezione `extensions:`, quindi anche lei deve esistere prima che vengano elaborate le altre.
- **`ServicesExtension` viene per ultima.** La sezione `services:` 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 |extensions#Tipi di 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 |#Riferimenti: quando @service diventa un riferimento].

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 `@service` sono ancora semplici stringhe.

È proprio per questo che qui la ricerca per tipo è inaffidabile: maggiori dettagli [qui sotto |#Esaminare ContainerBuilder: quando è sicuro].


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 |application:bootstrapping#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 `@service` usata *come entità*, cioè come la cosa che crea un servizio, come in `Foo(@bar)`, diventa subito un riferimento. Una `@service` usata *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 `@name` o `@Type` pulito viene trasformato in un oggetto `Reference`. Questo riguarda solo le forme semplici; `@service::CONST` o 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 sezione `services:` dell'utente (che gira per ultima) non c'è. Una chiamata a `getByType()` 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: **in `loadConfiguration()` 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**, quindi `getByType()`, `findByType()` e `findByTag()` restituiscono tutti risposte **affidabili**. Gli argomenti *non* hanno ancora subito l'autowiring: è il passaggio immediatamente successivo (`complete`), dopo tutte le chiamate a `beforeCompile()`. Quando qui modificate una definizione, il successivo `getByType()` 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 |extensions#Codice di inizializzazione]


Dentro la fase B: resolve e complete
====================================

La fase B è composta da due passaggi, con in mezzo le chiamate a `beforeCompile()`:

```php
$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 |extensions#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 sezione `services:` dell'utente gira dopo di voi) e `getByType()` provoca un resolve prematuro di un grafo parziale. Spostatelo in `beforeCompile()`. `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 chiamare `getByType()`." No: solleva `NotAllowedDuringResolvingException`. La ricerca per tipo appartiene a `beforeCompile()` o a fasi successive, mai in mezzo al resolve.

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

  • ParametersExtension ed ExtensionsExtension vengono 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 sezione extensions:, quindi anche lei deve esistere prima che vengano elaborate le altre.
  • ServicesExtension viene per ultima. La sezione services: 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 @service sono 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 @service usata come entità, cioè come la cosa che crea un servizio, come in Foo(@bar), diventa subito un riferimento. Una @service usata 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 @name o @Type pulito viene trasformato in un oggetto Reference. Questo riguarda solo le forme semplici; @service::CONST o 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 sezione services: dell'utente (che gira per ultima) non c'è. Una chiamata a getByType() 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: in loadConfiguration() 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, quindi getByType(), findByType() e findByTag() restituiscono tutti risposte affidabili. Gli argomenti non hanno ancora subito l'autowiring: è il passaggio immediatamente successivo (complete), dopo tutte le chiamate a beforeCompile(). Quando qui modificate una definizione, il successivo getByType() 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 sezione services: dell'utente gira dopo di voi) e getByType() provoca un resolve prematuro di un grafo parziale. Spostatelo in beforeCompile(). 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 chiamare getByType().“ No: solleva NotAllowedDuringResolvingException. La ricerca per tipo appartiene a beforeCompile() o a fasi successive, mai in mezzo al resolve.