La compilation du conteneur en détail
Cette page ouvre le capot de la compilation du conteneur : les phases qu'elle traverse, le moment où les
paramètres de configuration sont développés, celui où les chaînes @service deviennent de vraies références
et – la question que les auteurs d'extensions posent le plus souvent – la phase dans laquelle vous pouvez rechercher sans
risque les services par type. C'est le complément approfondi de Créer des extensions.
Vous n'avez besoin de rien de tout cela pour écrire une application normale, ni même une extension normale. Mais dès que
votre extension se met à inspecter ou à remodeler le graphe des services, le timing devient déterminant : le même appel à
getByType() donne une réponse fiable dans une phase et une réponse trompeuse dans une autre. Cette page explique
pourquoi, afin que vous sachiez toujours où placer votre code.
Deux mondes : compilation et exécution
La chose la plus importante à comprendre est qu'un conteneur Nette n'est pas assemblé à chaque requête. Il est
construit une seule fois sous forme de classe PHP optimisée, cette classe est stockée sur le disque et chaque requête suivante
se contente d'include le fichier terminé. Toute la machinerie décrite ci-dessous – extensions, resolvers,
générateur de code – ne tourne que pendant la (re)compilation.
Cela sépare le monde en deux représentations qui ne coexistent jamais :
| pendant la compilation | à l'exécution | |
|---|---|---|
| Ce qui existe | définitions (recettes) dans ContainerBuilder |
instances des services dans Container |
| Classes clés | Compiler, ContainerBuilder, Resolver, PhpGenerator |
Container (parent de la classe générée) |
%param%, @service |
marqueurs textuels encore en cours de traduction | déjà traduits / figés dans le code |
La classe générée étend Nette\DI\Container et possède une méthode createServiceXxx() pour
chaque service. Ses paramètres et les métadonnées de l'autowiring sont précalculés, si bien qu'à l'exécution il n'y a plus
rien à résoudre – seulement des services à instancier à la demande.
En mode développement, le conteneur est reconstruit automatiquement dès qu'un fichier de configuration ou une classe d'extension change ; les deux sont suivis comme dépendances. En production, il est compilé une fois et jamais revérifié, et c'est de là que vient la vitesse.
Les phases en un coup d'œil
La compilation est orchestrée par Compiler::compile() et se ramène à trois étapes :
public function compile(): string
{
$this->processExtensions(); // PHASE A : schémas + loadConfiguration()
$this->processBeforeCompile(); // PHASE B : resolve + beforeCompile() + complete
return $this->generateCode(); // PHASE C : génération du code + afterCompile()
}
Tout le modèle mental tient en une seule idée : chaque phase en sait plus que la précédente.
- La phase A remplit le graphe de définitions. Les types des services ne sont pas encore connus de façon fiable, car un type peut provenir de la valeur de retour d'une factory que personne n'a encore examinée.
- La phase B résout d'abord tous les types (
resolve), laisse ensuite les extensions remodeler le graphe (beforeCompile) et enfin autowire les arguments (complete). - La phase C transforme le graphe terminé en PHP et laisse les extensions retoucher le code généré.
C'est précisément cette connaissance croissante qui fait que la même opération est sûre dans une phase et peu fiable dans une autre. Le reste de cette page parcourt les phases en gardant cette idée à l'esprit.
Phase A : enregistrement des définitions
Dans cette phase, Nette appelle trois méthodes sur chaque extension – getConfigSchema(), puis
setConfig(), puis loadConfiguration() – mais dans un ordre soigneusement contrôlé, car ici
l'ordre compte vraiment.
Pourquoi l'ordre compte
ParametersExtensionetExtensionsExtensionpassent en premier. La première doit s'exécuter avant tout le reste afin de pouvoir développer%param%dans toute la configuration – chaque autre extension reçoit ensuite sa propre section avec les valeurs déjà remplies. La seconde enregistre les extensions supplémentaires listées dans la sectionextensions:, elle doit donc elle aussi exister avant que les autres soient traitées.ServicesExtensionpasse en dernier. La sectionservices:de l'utilisateur a donc toujours le dernier mot et peut redéfinir tout ce que les extensions ont mis en place.InjectExtensionest déplacée tout à la fin pour que son travail tienne compte des setups ajoutés par toutes les autres extensions.
Ce qu'il faut en retenir : au moment où le loadConfiguration() de votre extension s'exécute, les paramètres
sont déjà développés, mais les services de l'utilisateur ne sont pas encore là. Ce simple fait explique la plupart des
règles de timing qui suivent.
De services: aux définitions
La section services: de l'utilisateur est transformée en objets
de définition ici, à la dernière étape de la phase A. Chaque entrée NEON est normalisée (les notations abrégées sont
unifiées), son genre est détecté (service ordinaire, factory, accesseur, …) et une définition correspondante est créée
dans le builder. C'est aussi le premier moment où les arguments simples @name / @Type deviennent des
références – voir ci-dessous.
À la fin de la phase A, toutes les définitions sont présentes – chaque extension et l'utilisateur ont enregistré ce qu'ils voulaient – mais l'image n'est pas encore nette :
- les types ne sont pas résolus pour les définitions dont le type provient de la valeur de retour d'une factory,
- les arguments ne sont pas autowirés,
- certaines références
@servicesont encore de simples chaînes.
C'est exactement pour cela que la recherche par type n'est pas fiable ici – voir ci-dessous.
Paramètres : quand %param% est développé
L'une des deux grandes questions. La réponse est courte : une seule fois, tout au début de la phase A, dans tout l'arbre de configuration.
ParametersExtension s'exécute en premier et l'une des premières choses qu'elle fait est de développer les
placeholders %param% – d'abord à l'intérieur des paramètres eux-mêmes (un paramètre peut en référencer un
autre), puis dans tout le reste de la configuration. Ainsi, au moment où n'importe quelle autre extension, y compris
ServicesExtension, reçoit sa section, les placeholders ont déjà disparu. Les extensions travaillent avec des
valeurs concrètes, jamais avec des %...%.
Quand un placeholder constitue toute la chaîne, sa valeur est renvoyée telle quelle – y compris les tableaux et
les objets – si bien que %mailer% peut se développer en un tableau entier. Partout ailleurs, il est concaténé
dans une chaîne, et la notation pointée %foo.bar% atteint les tableaux imbriqués.
Paramètres statiques et dynamiques
Toutes les valeurs ne peuvent pas être figées dans le code. Un paramètre dont la valeur diffère selon l'environnement –
une variable d'environnement, la baseUrl déduite de la requête – doit rester dynamique. Vous déclarez de
tels paramètres avec setDynamicParameterNames() ou Expect::...->dynamic() dans un schéma ; plus
d'informations dans Paramètres
dynamiques.
Un paramètre dynamique n'est pas remplacé par une valeur, mais par une expression qui la lit à l'exécution.
%env.DB_HOST% ne se fige donc pas en une chaîne ; il devient une lecture effectuée à l'exécution dans le
conteneur généré. Tout le reste est statique et se fige au moment de la compilation – c'est la source habituelle de la
surprise „ma valeur getenv() est la même dans tous les environnements“ : le paramètre était tout simplement
statique.
L'opération inverse est l'échappement : pour éviter qu'un % ou un @ littéral soit
interprété, on le double (%%, @@). Nette le fait automatiquement pour les paramètres qu'il injecte à
votre place, si bien que leurs valeurs ne sont jamais prises pour des placeholders ou des références.
Références : quand @service devient une référence
La deuxième grande question. La traduction de @service se fait en plusieurs étapes réparties sur
différentes phases, selon la complexité de la chaîne. Vous avez rarement besoin de suivre cela à la main, mais connaître
les étapes explique pourquoi certaines références se résolvent plus tôt que d'autres.
- Analyse (chargement de la configuration). Un
@serviceutilisé comme entité – ce qui crée un service, comme dansFoo(@bar)– devient immédiatement une référence. Un@serviceutilisé comme argument reste pour l'instant une simple chaîne. Un@entre guillemets est échappé en@@et compte donc comme du texte littéral, pas comme une référence. - Phase A (
loadConfiguration). Lors du traitement des définitions, un argument@nameou@Typepropre est transformé en objetReference. Cela ne capte que les formes simples ;@service::CONSTou un@au sein d'une expression plus large est laissé pour plus tard. - Phase B (
complete). La véritable traduction „intelligente“ a lieu ici :@service→ référence,@service::CONSTANT→ une constante de classe littérale,@service::property→ la lecture de cette propriété,@@x→ le texte littéral@x.
Une seconde traduction se cache dans le mot référence lui-même. Une Reference peut pointer soit par
nom, soit par type (@Namespace\Type). Une référence par type n'est pas encore un nom de
service : elle est résolue en un nom concret par l'autowiring, et cela ne se produit qu'à l'étape complete, une fois
l'index de l'autowiring construit. C'est le pont vers la section suivante : les recherches d'autowiring sont délibérément
repoussées jusqu'à ce que l'index soit prêt.
| Forme | Devient une référence/expression en | Résolue en un service concret en |
|---|---|---|
entité (@foo comme factory) |
analyse | complete |
argument @foo, @Type |
phase A | complete |
@foo::CONST, @foo::prop |
phase B | complete |
référence par type @Type |
phase A/B | complete (autowiring) |
Inspecter ContainerBuilder : quand est-ce sûr
Voici maintenant la question que les auteurs d'extensions posent le plus souvent : dans quelle méthode puis-je rechercher les services par type ? La réponse découle d'une règle simple sur la façon dont le builder suit son propre état.
La recherche par type (getByType(), getDefinitionByType(), findByType()) exige
que le graphe des services soit résolu : chaque type connu, l'index de l'autowiring construit. Aussi, dès que vous
appelez l'une de ces méthodes alors que le graphe a changé depuis le dernier resolve, le builder résout sur-le-champ tout le
graphe connu. Pendant le resolve lui-même, toute recherche par type est interdite et lève
NotAllowedDuringResolvingException.
La recherche par tag (findByTag()) n'a pas cette exigence – les tags ne dépendent pas des types, cela
fonctionne donc dans toutes les phases.
Phase par phase :
loadConfiguration()(phase A) – la recherche par type n'est pas fiable. Le graphe est incomplet : les extensions qui s'exécutent plus tard n'ont pas encore enregistré leurs services et, surtout, la sectionservices:de l'utilisateur (qui passe en dernier) n'est pas là. Un appel àgetByType()fonctionne bien – il déclenche un resolve prématuré d'un graphe partiel – mais la réponse provient d'une image incomplète, et ce resolve prématuré gaspille du travail. Règle empirique : dansloadConfiguration(), contentez-vous d'enregistrer des définitions ; ne cherchez pas par type.findByTag()ne pose pas de problème.beforeCompile()(phase B) – le bon endroit pour l'introspection. À ce stade, toutes les définitions existent (y compris celles de l'utilisateur), les types sont résolus et l'index de l'autowiring est construit ;getByType(),findByType()etfindByTag()renvoient donc des réponses fiables. Les arguments ne sont pas encore autowirés – c'est justement l'étape suivante (complete), après tous les appels àbeforeCompile(). Quand vous modifiez une définition ici, legetByType()suivant re-résout le graphe de façon transparente, vous pouvez donc alterner librement modifications et requêtes.afterCompile()(phase C) – le code seulement. Elle travaille sur la classe générée, pas sur le builder. Le graphe est terminé ; ici vous façonnez le PHP obtenu.
| Je veux… | Phase |
|---|---|
| enregistrer un service | loadConfiguration() |
| chercher par tag et modifier des définitions | loadConfiguration() ou beforeCompile() |
chercher par type (getByType/findByType) |
beforeCompile() |
| dépendre des services que l'autowiring a choisis pour les arguments | pas à la compilation – inspectez-le à l'exécution |
| toucher au code généré | afterCompile() |
| exécuter du code après le démarrage du conteneur | code d'initialisation |
Dans la phase B : resolve et complete
La phase B se compose de deux passes, avec les appels à beforeCompile() intercalés entre elles :
$this->builder->resolve(); // types résolus, index de l'autowiring construit
foreach ($this->extensions as $extension) {
$extension->beforeCompile();
}
$this->builder->complete(); // SEULEMENT MAINTENANT les arguments sont autowirés
resolve() détermine le type de chaque service – repris de son type déclaré, ou déduit
de sa factory : le type de retour d'une méthode fabrique, la classe qu'elle instancie, ou le service vers lequel pointe une
référence – puis construit l'index de l'autowiring, qui associe chaque type (la classe ainsi que ses parents et interfaces)
à un nom de service. Un service marqué autowired: false est laissé hors de l'index ;
autowired: [A, B] restreint les types sous lesquels il est visible. Point crucial : resolve fixe les types,
pas les arguments – autowirer les arguments demanderait l'index terminé, qui n'existe qu'après cette passe.
complete() est l'endroit où l'autowiring des arguments a réellement lieu. Pour chaque définition, il
complète les arguments manquants du constructeur et du setup en cherchant leurs types dans l'index désormais complet. Voilà
pourquoi les références par type sont restées non résolues pendant resolve : la recherche a sa place ici, une fois qu'il
existe un index fiable où chercher.
Phase C : génération du code
generateCode() confie le graphe terminé à PhpGenerator, qui produit une classe étendant
Container avec une méthode createServiceXxx() par service, ainsi que les métadonnées précalculées
aliases, tags et wiring. Chaque Statement devient du texte PHP
(new Foo(...), appels de méthodes, accès aux propriétés) et chaque Reference devient un appel
$this->getService(...).
Les extensions bénéficient ensuite d'une dernière passe afterCompile() sur la classe générée – c'est là
que sont émis, par exemple, les getters des paramètres statiques et dynamiques – ainsi que de la possibilité d'ajouter du code d'initialisation exécuté à chaque requête.
Le déroulement en une image
COMPILATION (une fois, dans le cache)
│
├─ charger les fichiers de config NEON -> Statement/tableau ; fusion des fichiers
│ @ entre guillemets -> @@ ; entités -> Statement
│
▼ Compiler::compile()
│
├─ PHASE A processExtensions()
│ ├─ ParametersExtension (1re) ── %param% DÉVELOPPÉ dans toute la configuration
│ │ les dynamiques -> expression à l'exécution
│ ├─ ExtensionsExtension (1re) ── enregistre d'autres extensions
│ ├─ ...autres extensions... ── loadConfiguration() : enregistrer seulement des définitions
│ └─ ServicesExtension (DERNIÈRE) ── services: -> objets Definition
│ @name/@Type -> Reference
│ [graphe complet en nombre ; TYPES et ARGUMENTS pas encore ; recherche par type non fiable]
│
├─ PHASE B processBeforeCompile()
│ ├─ builder.resolve() ── résoudre tous les types ; construire l'index d'autowiring
│ │ [types prêts ; index prêt]
│ ├─ beforeCompile() extensions ── getByType/findByType/findByTag SÛRS ici
│ │ (arguments pas encore autowirés)
│ └─ builder.complete() ── autowirer les ARGUMENTS ; finir la traduction des références
│ références par type -> noms de services
│
└─ PHASE C generateCode()
├─ PhpGenerator.generate() ── Statement -> PHP ; méthodes createServiceXxx()
├─ afterCompile() extensions ── retoucher le code ; émettre les getters de paramètres
└─ toString() ── code PHP final -> cache
────────────────────────────────────────────────────────────
EXÉCUTION (chaque requête)
│
├─ new Container($dynamicParams)
├─ initialize() ── code de démarrage des extensions (session, en-têtes, validation)
└─ getService()/getByType() ── instances paresseuses depuis les métadonnées précalculées
Idées fausses courantes
- „Dans
loadConfiguration(), je vais chercher les services par type.“ Non – le graphe est incomplet (la sectionservices:de l'utilisateur s'exécute après vous) etgetByType()déclenche un resolve prématuré d'un graphe partiel. Déplacez cela dansbeforeCompile().findByTag()ne pose pas de problème, même ici. - „Une valeur
getenv()dans un paramètre sera différente dans chaque environnement.“ Seulement si le paramètre est dynamique. Sinon, elle est figée à la compilation et reste la même partout. - „Une référence
@Typeest déjà un nom de service.“ Non – c'est une référence par type, résolue en un nom concret par l'autowiring, et seulement à l'étape complete. - „Mon extension lit un fichier auxiliaire, mais les changements n'apparaissent pas.“ Enregistrez-le avec
$builder->addDependency($file), sinon le cache n'est pas au courant et ne se reconstruira pas. - „Pendant
resolve(), je peux appelergetByType().“ Non – cela lèveNotAllowedDuringResolvingException. La recherche par type appartient àbeforeCompile()ou plus tard, jamais au milieu du resolve.