Nette Documentation Preview

syntax
Компиляция в подробностях
*************************

.[perex]
Эта страница раскрывает компиляцию контейнера: через какие фазы она проходит, когда разворачиваются параметры конфигурации, когда строки `@service` превращаются в настоящие ссылки и - вопрос, который авторы расширений задают чаще всего - в какой фазе можно спокойно искать сервисы по типу. Это углублённое дополнение к главе [Создание расширений |extensions].

Чтобы написать обычное приложение или даже обычное расширение, ничего из этого знать не нужно. Но как только ваше расширение начинает исследовать или перекраивать граф сервисов, момент становится решающим: один и тот же вызов `getByType()` в одной фазе даёт надёжный ответ, а в другой - обманчивый. Эта страница объясняет почему, чтобы вы всегда знали, где место вашему коду.


Два мира: компиляция и время выполнения
=======================================

Самое важное, что нужно понять: контейнер Nette **не собирается при каждом запросе**. Он однажды собирается в оптимизированный PHP-класс, этот класс сохраняется на диск, и каждый последующий запрос лишь подключает готовый файл через `include`. Вся описанная ниже механика - расширения, резолверы, генератор кода - работает **только во время (пере)компиляции**.

Это делит мир на два представления, которые никогда не сосуществуют:

| | во время компиляции | во время выполнения
|---|---|---
| Что существует | **определения** (рецепты) в `ContainerBuilder` | **экземпляры** сервисов в `Container`
| Ключевые классы | `Compiler`, `ContainerBuilder`, `Resolver`, `PhpGenerator` | `Container` (родитель порождённого класса)
| `%param%`, `@service` | текстовые пометки, ещё подлежащие переводу | уже переведены и вписаны в код

Порождённый класс расширяет `Nette\DI\Container` и содержит по методу `createServiceXxx()` для каждого сервиса. Его параметры и метаданные autowiring вычислены заранее, поэтому во время выполнения разрешать уже нечего - остаётся только создавать сервисы по требованию.

.[note]
В режиме разработки контейнер пересобирается автоматически при изменении конфигурационного файла или класса расширения; и то, и другое отслеживается как зависимость. В продакшене он компилируется однажды и больше не проверяется, откуда и берётся скорость.


Фазы вкратце
============

Компиляцией дирижирует `Compiler::compile()`, и сводится она к трём шагам:

```php
public function compile(): string
{
	$this->processExtensions();     // ФАЗА A: схемы + loadConfiguration()
	$this->processBeforeCompile();  // ФАЗА B: resolve + beforeCompile() + complete
	return $this->generateCode();   // ФАЗА C: генерация кода + afterCompile()
}
```

Вся мысленная модель умещается в одну идею: **каждая фаза знает больше предыдущей.**

- **Фаза A** наполняет граф определениями. Типы сервисов **ещё не известны надёжно**, потому что тип может происходить из возвращаемого значения фабрики, в которое никто ещё не заглядывал.
- **Фаза B** сначала разрешает все типы (`resolve`), затем позволяет расширениям перекроить граф (`beforeCompile`), а в конце заполняет аргументы через [autowiring |autowiring] (`complete`).
- **Фаза C** превращает готовый граф в PHP и позволяет расширениям поправить порождённый код.

Именно это нарастающее знание объясняет, почему одна и та же операция в одной фазе безопасна, а в другой ненадёжна. Остальная часть страницы проходит по фазам с этой мыслью в голове.


Фаза A: регистрация определений
===============================

В этой фазе Nette вызывает у каждого расширения три метода - `getConfigSchema()`, затем `setConfig()`, затем `loadConfiguration()`, - но в **тщательно выверенном порядке**, потому что здесь порядок действительно важен.


Почему порядок важен
--------------------

- **`ParametersExtension` и `ExtensionsExtension` идут первыми.** Первое должно отработать раньше всех, чтобы развернуть `%param%` по всей конфигурации: каждое следующее расширение получает свою секцию уже с подставленными значениями. Второе регистрирует дальнейшие расширения, перечисленные в секции `extensions:`, поэтому оно тоже должно существовать до обработки остальных.
- **`ServicesExtension` идёт последним.** Поэтому пользовательская секция `services:` всегда имеет последнее слово и может переопределить всё, что настроили расширения.
- **`InjectExtension` перенесено в самый конец**, чтобы его работа видела настройки, добавленные всеми остальными расширениями.

Вывод для вас: к моменту, когда выполняется `loadConfiguration()` вашего расширения, параметры уже развёрнуты, но пользовательских сервисов ещё нет. Этот единственный факт определяет большинство правил о моментах ниже.


Превращение services: в определения
-----------------------------------

Пользовательская секция `services:` превращается в [объекты определений |extensions#Типы определений] здесь, на последнем шаге фазы A. Каждая запись NEON нормализуется (краткие записи приводятся к единому виду), определяется её вид (обычный сервис, фабрика, аксессор, ...), и в билдере создаётся соответствующее определение. Это же первый момент, когда простые аргументы `@name` / `@Type` становятся ссылками, см. [ниже |#Ссылки: когда @service становится ссылкой].

К концу фазы A все определения на месте - каждое расширение и пользователь зарегистрировали то, что хотели, - но картина ещё не резкая:

- **типы не разрешены** у определений, тип которых происходит из возвращаемого значения фабрики,
- **аргументы не заполнены autowiring**,
- часть ссылок `@service` всё ещё остаётся обычными строками.

Именно поэтому поиск по типу здесь ненадёжен, подробнее [ниже |#Исследование ContainerBuilder: когда это безопасно].


Параметры: когда разворачивается %param%
========================================

Один из двух главных вопросов. Ответ короткий: **однажды, в самом начале фазы A, по всему дереву конфигурации.**

`ParametersExtension` выполняется первым, и одно из первых его дел - развернуть подстановки `%param%`: сначала внутри самих параметров (параметр может ссылаться на другой), затем по всей остальной конфигурации. Так что к моменту, когда любое другое расширение, включая `ServicesExtension`, получает свою секцию, подстановок уже нет. Расширения работают с конкретными значениями, а не с `%...%`.

Когда подстановка занимает всю строку, её значение возвращается *как есть*, включая массивы и объекты, поэтому `%mailer%` может развернуться в целый массив. В любом другом месте оно склеивается в строку, а запись через точку `%foo.bar%` добирается до вложенных массивов.


Статические и динамические параметры
------------------------------------

Не всякое значение можно впечь в код. Параметр, значение которого различается в разных средах, - переменная окружения, `baseUrl`, выведенный из запроса, - должен остаться **динамическим**. Такие параметры вы объявляете через `setDynamicParameterNames()` или `Expect::...->dynamic()` в схеме; подробнее в разделе [Динамические параметры |application:bootstrapping#Динамические параметры].

Динамический параметр заменяется не значением, а выражением, которое читает его *во время выполнения*. Поэтому `%env.DB_HOST%` не застывает в строку, а становится обращением во время выполнения в порождённом контейнере. Всё остальное статично и застывает во время компиляции, откуда и берётся обычное удивление "моё значение `getenv()` одинаково во всех средах": параметр просто был статическим.

Обратная операция - **экранирование**: чтобы буквальные `%` или `@` не были истолкованы, они удваиваются (`%%`, `@@`). Nette делает это автоматически для параметров, которые подставляет за вас, поэтому их значения никогда не примут за подстановки или ссылки.


Ссылки: когда @service становится ссылкой
=========================================

Второй главный вопрос. Перевод `@service` происходит **в несколько шагов в разных фазах**, в зависимости от того, насколько сложна строка. Отслеживать это вручную приходится редко, но знание шагов объясняет, почему одни ссылки разрешаются раньше других.

- **Разбор (загрузка конфигурации).** `@service`, использованный *как сущность* - то, что создаёт сервис, как в `Foo(@bar)`, - становится ссылкой сразу. `@service`, использованный *как аргумент*, пока остаётся обычной строкой. `@` в кавычках экранируется в `@@`, поэтому считается буквальным текстом, а не ссылкой.
- **Фаза A (`loadConfiguration`).** При обработке определений чистый аргумент `@name` или `@Type` превращается в объект `Reference`. Это ловит только простые формы; `@service::CONST` или `@` внутри более крупного выражения остаются на потом.
- **Фаза B (`complete`).** Здесь происходит настоящий "умный" перевод: `@service` → ссылка, `@service::CONSTANT` → буквальная константа класса, `@service::property` → чтение этого свойства, `@@x` → буквальный текст `@x`.

В самом слове *ссылка* скрыт ещё один перевод. `Reference` может указывать либо по **имени**, либо по **типу** (`@Namespace\Type`). Ссылка по типу **ещё не является именем сервиса** - в конкретное имя её разрешает autowiring, а это происходит только на шаге **complete**, когда построен индекс autowiring. Это мостик к следующему разделу: обращения к autowiring намеренно откладываются до готовности индекса.

| Форма | Становится ссылкой или выражением в | Разрешается в конкретный сервис в
|---|---|---
| сущность (`@foo` как фабрика) | разборе | complete
| аргумент `@foo`, `@Type` | фазе A | complete
| `@foo::CONST`, `@foo::prop` | фазе B | complete
| ссылка по типу `@Type` | фазе A/B | complete (autowiring)


Исследование ContainerBuilder: когда это безопасно
==================================================

Теперь вопрос, который авторы расширений задают чаще всего: **в каком методе можно искать сервисы по типу?** Ответ вытекает из одного простого правила о том, как билдер следит за собственным состоянием.

Поиск **по типу** (`getByType()`, `getDefinitionByType()`, `findByType()`) требует, чтобы граф сервисов был *разрешён*: все типы известны, индекс autowiring построен. Поэтому, когда вы вызываете один из этих методов, а граф изменился с последнего разрешения, билдер **тут же разрешает весь известный граф**. Во время самого разрешения любой поиск по типу запрещён и выбрасывает `NotAllowedDuringResolvingException`.

У поиска **по тегу** (`findByTag()`) такого требования нет: теги не зависят от типов, поэтому он работает в **любой фазе**.

По фазам:

- **`loadConfiguration()` (фаза A) - поиск по типу ненадёжен.** Граф неполон: расширения, которые выполняются позже, ещё не зарегистрировали свои сервисы, а главное - нет пользовательской секции `services:` (она идёт последней). Вызов `getByType()` сработает, он вызовет преждевременное разрешение частичного графа, но ответ придёт из неполной картины, а само преждевременное разрешение потратит силы впустую. Правило: **в `loadConfiguration()` только регистрируйте определения, не ищите по типу.** `findByTag()` здесь допустим.
- **`beforeCompile()` (фаза B) - правильное место для исследования.** К этому моменту существуют **все** определения (включая пользовательские), **типы разрешены** и **индекс autowiring построен**, поэтому `getByType()`, `findByType()` и `findByTag()` возвращают **надёжные** ответы. Аргументы ещё *не* заполнены autowiring: это следующий шаг (`complete`), после всех вызовов `beforeCompile()`. Когда вы меняете здесь определение, следующий `getByType()` незаметно перерешает граф, так что вы можете свободно чередовать правки и запросы.
- **`afterCompile()` (фаза C) - только код.** Он работает над порождённым классом, а не над билдером. Граф уже готов; здесь вы формируете итоговый PHP.

| Я хочу... | Фаза
|---|---
| зарегистрировать сервис | `loadConfiguration()`
| искать по **тегу** и менять определения | `loadConfiguration()` или `beforeCompile()`
| искать по **типу** (`getByType`/`findByType`) | **`beforeCompile()`**
| зависеть от того, какие сервисы autowiring выбрал для аргументов | не во время компиляции - смотрите это во время выполнения
| поправить порождённый код | `afterCompile()`
| выполнить код после старта контейнера | [код инициализации |extensions#Код инициализации]


Внутри фазы B: resolve и complete
=================================

Фаза B состоит из двух проходов, между которыми зажаты вызовы `beforeCompile()`:

```php
$this->builder->resolve();     // типы разрешены, индекс autowiring построен
foreach ($this->extensions as $extension) {
	$extension->beforeCompile();
}
$this->builder->complete();    // ТОЛЬКО ТЕПЕРЬ аргументы заполняются autowiring
```

**`resolve()`** определяет тип каждого сервиса - берёт его из объявленного `type` либо выводит из фабрики: тип возвращаемого значения фабричного метода, класс, который она создаёт, или сервис, на который указывает ссылка, - и затем строит индекс autowiring, сопоставляющий каждому типу (классу вместе с его родителями и интерфейсами) имя сервиса. Сервис, помеченный `autowired: false`, в индекс не попадает; `autowired: [A, B]` сужает набор типов, под которыми он виден. Принципиально важно, что resolve улаживает *типы*, а не *аргументы*: заполнению аргументов через autowiring нужен готовый индекс, который появляется только после этого прохода.

**`complete()`** - место, где на самом деле происходит заполнение аргументов через autowiring. Для каждого определения он подставляет недостающие аргументы конструктора и настроек, разыскивая их типы в уже готовом индексе. Именно поэтому ссылки по типу оставались неразрешёнными во время resolve: этот поиск - дело complete, когда есть надёжный индекс, в котором можно искать.


Фаза C: генерация кода
======================

`generateCode()` передаёт готовый граф в `PhpGenerator`, который порождает класс, расширяющий `Container`, с методом `createServiceXxx()` на каждый сервис, а также заранее вычисленные метаданные `aliases`, `tags` и `wiring`. Каждый `Statement` становится текстом PHP (`new Foo(...)`, вызовы методов, обращения к свойствам), а каждая `Reference` - вызовом `$this->getService(...)`.

Затем расширения получают заключительный проход `afterCompile()` над порождённым классом: именно здесь, например, выводятся геттеры статических и динамических параметров, - а вместе с ним возможность добавить [код инициализации |extensions#Код инициализации], выполняемый при каждом запросе.


Вся картина одним рисунком
==========================

```
КОМПИЛЯЦИЯ (однажды, в кеш)
│
├─ загрузка конфигурации          NEON -> Statement/массив; слияние файлов
│                                 @ в кавычках -> @@ ; сущности -> Statement
│
▼ Compiler::compile()
│
├─ ФАЗА A  processExtensions()
│   ├─ ParametersExtension (ПЕРВОЕ)   ── %param% РАЗВЁРНУТ по всей конфигурации
│   │                                    динамические -> выражение времени выполнения
│   ├─ ExtensionsExtension (ПЕРВОЕ)   ── регистрирует дальнейшие расширения
│   ├─ ...остальные расширения...     ── loadConfiguration(): только регистрация определений
│   └─ ServicesExtension (ПОСЛЕДНЕЕ)  ── services: -> объекты Definition
│                                        @name/@Type -> Reference
│   [граф полон по количеству; ТИПЫ и АРГУМЕНТЫ ещё нет; поиск по типу ненадёжен]
│
├─ ФАЗА B  processBeforeCompile()
│   ├─ builder.resolve()          ── разрешить все типы; построить индекс autowiring
│   │                                [типы готовы; индекс готов]
│   ├─ beforeCompile() расширения ── здесь getByType/findByType/findByTag БЕЗОПАСНЫ
│   │                                (аргументы ещё не заполнены autowiring)
│   └─ builder.complete()         ── заполнить АРГУМЕНТЫ; завершить перевод ссылок
│                                    ссылки по типу -> имена сервисов
│
└─ ФАЗА C  generateCode()
    ├─ PhpGenerator.generate()    ── Statement -> PHP; методы createServiceXxx()
    ├─ afterCompile() расширения  ── поправить код; вывести геттеры параметров
    └─ toString()                 ── итоговый PHP-код -> кеш

────────────────────────────────────────────────────────────

ВРЕМЯ ВЫПОЛНЕНИЯ (каждый запрос)
│
├─ new Container($dynamicParams)
├─ initialize()                   ── загрузочный код расширений (сессия, заголовки, валидация)
└─ getService()/getByType()       ── ленивые экземпляры из заранее вычисленных метаданных
```


Распространённые заблуждения
============================

- "В `loadConfiguration()` я поищу сервисы по типу." Нет: граф неполон (пользовательская секция `services:` выполняется после вас), и `getByType()` вызывает преждевременное разрешение частичного графа. Перенесите это в `beforeCompile()`. `findByTag()` здесь допустим.
- "Значение из `getenv()` в параметре будет разным в каждой среде." Только если параметр динамический. Иначе оно впекается во время компиляции и остаётся везде одинаковым.
- "Ссылка `@Type` - это уже имя сервиса." Нет: это ссылка по типу, которую autowiring разрешает в конкретное имя только на шаге complete.
- "Моё расширение читает вспомогательный файл, но изменения не проявляются." Зарегистрируйте его через `$builder->addDependency($file)`, иначе кеш о нём не знает и пересобираться не будет.
- "Во время `resolve()` я могу вызвать `getByType()`." Нет: он выбросит `NotAllowedDuringResolvingException`. Поиску по типу место в `beforeCompile()` или позже, но никогда посреди разрешения.

Компиляция в подробностях

Эта страница раскрывает компиляцию контейнера: через какие фазы она проходит, когда разворачиваются параметры конфигурации, когда строки @service превращаются в настоящие ссылки и – вопрос, который авторы расширений задают чаще всего – в какой фазе можно спокойно искать сервисы по типу. Это углублённое дополнение к главе Создание расширений.

Чтобы написать обычное приложение или даже обычное расширение, ничего из этого знать не нужно. Но как только ваше расширение начинает исследовать или перекраивать граф сервисов, момент становится решающим: один и тот же вызов getByType() в одной фазе даёт надёжный ответ, а в другой – обманчивый. Эта страница объясняет почему, чтобы вы всегда знали, где место вашему коду.

Два мира: компиляция и время выполнения

Самое важное, что нужно понять: контейнер Nette не собирается при каждом запросе. Он однажды собирается в оптимизированный PHP-класс, этот класс сохраняется на диск, и каждый последующий запрос лишь подключает готовый файл через include. Вся описанная ниже механика – расширения, резолверы, генератор кода – работает только во время (пере)компиляции.

Это делит мир на два представления, которые никогда не сосуществуют:

  во время компиляции во время выполнения
Что существует определения (рецепты) в ContainerBuilder экземпляры сервисов в Container
Ключевые классы Compiler, ContainerBuilder, Resolver, PhpGenerator Container (родитель порождённого класса)
%param%, @service текстовые пометки, ещё подлежащие переводу уже переведены и вписаны в код

Порождённый класс расширяет Nette\DI\Container и содержит по методу createServiceXxx() для каждого сервиса. Его параметры и метаданные autowiring вычислены заранее, поэтому во время выполнения разрешать уже нечего – остаётся только создавать сервисы по требованию.

В режиме разработки контейнер пересобирается автоматически при изменении конфигурационного файла или класса расширения; и то, и другое отслеживается как зависимость. В продакшене он компилируется однажды и больше не проверяется, откуда и берётся скорость.

Фазы вкратце

Компиляцией дирижирует Compiler::compile(), и сводится она к трём шагам:

public function compile(): string
{
	$this->processExtensions();     // ФАЗА A: схемы + loadConfiguration()
	$this->processBeforeCompile();  // ФАЗА B: resolve + beforeCompile() + complete
	return $this->generateCode();   // ФАЗА C: генерация кода + afterCompile()
}

Вся мысленная модель умещается в одну идею: каждая фаза знает больше предыдущей.

  • Фаза A наполняет граф определениями. Типы сервисов ещё не известны надёжно, потому что тип может происходить из возвращаемого значения фабрики, в которое никто ещё не заглядывал.
  • Фаза B сначала разрешает все типы (resolve), затем позволяет расширениям перекроить граф (beforeCompile), а в конце заполняет аргументы через autowiring (complete).
  • Фаза C превращает готовый граф в PHP и позволяет расширениям поправить порождённый код.

Именно это нарастающее знание объясняет, почему одна и та же операция в одной фазе безопасна, а в другой ненадёжна. Остальная часть страницы проходит по фазам с этой мыслью в голове.

Фаза A: регистрация определений

В этой фазе Nette вызывает у каждого расширения три метода – getConfigSchema(), затем setConfig(), затем loadConfiguration(), – но в тщательно выверенном порядке, потому что здесь порядок действительно важен.

Почему порядок важен

  • ParametersExtension и ExtensionsExtension идут первыми. Первое должно отработать раньше всех, чтобы развернуть %param% по всей конфигурации: каждое следующее расширение получает свою секцию уже с подставленными значениями. Второе регистрирует дальнейшие расширения, перечисленные в секции extensions:, поэтому оно тоже должно существовать до обработки остальных.
  • ServicesExtension идёт последним. Поэтому пользовательская секция services: всегда имеет последнее слово и может переопределить всё, что настроили расширения.
  • InjectExtension перенесено в самый конец, чтобы его работа видела настройки, добавленные всеми остальными расширениями.

Вывод для вас: к моменту, когда выполняется loadConfiguration() вашего расширения, параметры уже развёрнуты, но пользовательских сервисов ещё нет. Этот единственный факт определяет большинство правил о моментах ниже.

Превращение services: в определения

Пользовательская секция services: превращается в объекты определений здесь, на последнем шаге фазы A. Каждая запись NEON нормализуется (краткие записи приводятся к единому виду), определяется её вид (обычный сервис, фабрика, аксессор, …), и в билдере создаётся соответствующее определение. Это же первый момент, когда простые аргументы @name / @Type становятся ссылками, см. ниже.

К концу фазы A все определения на месте – каждое расширение и пользователь зарегистрировали то, что хотели, – но картина ещё не резкая:

  • типы не разрешены у определений, тип которых происходит из возвращаемого значения фабрики,
  • аргументы не заполнены autowiring,
  • часть ссылок @service всё ещё остаётся обычными строками.

Именно поэтому поиск по типу здесь ненадёжен, подробнее ниже.

Параметры: когда разворачивается %param%

Один из двух главных вопросов. Ответ короткий: однажды, в самом начале фазы A, по всему дереву конфигурации.

ParametersExtension выполняется первым, и одно из первых его дел – развернуть подстановки %param%: сначала внутри самих параметров (параметр может ссылаться на другой), затем по всей остальной конфигурации. Так что к моменту, когда любое другое расширение, включая ServicesExtension, получает свою секцию, подстановок уже нет. Расширения работают с конкретными значениями, а не с %...%.

Когда подстановка занимает всю строку, её значение возвращается как есть, включая массивы и объекты, поэтому %mailer% может развернуться в целый массив. В любом другом месте оно склеивается в строку, а запись через точку %foo.bar% добирается до вложенных массивов.

Статические и динамические параметры

Не всякое значение можно впечь в код. Параметр, значение которого различается в разных средах, – переменная окружения, baseUrl, выведенный из запроса, – должен остаться динамическим. Такие параметры вы объявляете через setDynamicParameterNames() или Expect::...->dynamic() в схеме; подробнее в разделе Динамические параметры.

Динамический параметр заменяется не значением, а выражением, которое читает его во время выполнения. Поэтому %env.DB_HOST% не застывает в строку, а становится обращением во время выполнения в порождённом контейнере. Всё остальное статично и застывает во время компиляции, откуда и берётся обычное удивление „моё значение getenv() одинаково во всех средах“: параметр просто был статическим.

Обратная операция – экранирование: чтобы буквальные % или @ не были истолкованы, они удваиваются (%%, @@). Nette делает это автоматически для параметров, которые подставляет за вас, поэтому их значения никогда не примут за подстановки или ссылки.

Ссылки: когда @service становится ссылкой

Второй главный вопрос. Перевод @service происходит в несколько шагов в разных фазах, в зависимости от того, насколько сложна строка. Отслеживать это вручную приходится редко, но знание шагов объясняет, почему одни ссылки разрешаются раньше других.

  • Разбор (загрузка конфигурации). @service, использованный как сущность – то, что создаёт сервис, как в Foo(@bar), – становится ссылкой сразу. @service, использованный как аргумент, пока остаётся обычной строкой. @ в кавычках экранируется в @@, поэтому считается буквальным текстом, а не ссылкой.
  • Фаза A (loadConfiguration). При обработке определений чистый аргумент @name или @Type превращается в объект Reference. Это ловит только простые формы; @service::CONST или @ внутри более крупного выражения остаются на потом.
  • Фаза B (complete). Здесь происходит настоящий „умный“ перевод: @service → ссылка, @service::CONSTANT → буквальная константа класса, @service::property → чтение этого свойства, @@x → буквальный текст @x.

В самом слове ссылка скрыт ещё один перевод. Reference может указывать либо по имени, либо по типу (@Namespace\Type). Ссылка по типу ещё не является именем сервиса – в конкретное имя её разрешает autowiring, а это происходит только на шаге complete, когда построен индекс autowiring. Это мостик к следующему разделу: обращения к autowiring намеренно откладываются до готовности индекса.

Форма Становится ссылкой или выражением в Разрешается в конкретный сервис в
сущность (@foo как фабрика) разборе complete
аргумент @foo, @Type фазе A complete
@foo::CONST, @foo::prop фазе B complete
ссылка по типу @Type фазе A/B complete (autowiring)

Исследование ContainerBuilder: когда это безопасно

Теперь вопрос, который авторы расширений задают чаще всего: в каком методе можно искать сервисы по типу? Ответ вытекает из одного простого правила о том, как билдер следит за собственным состоянием.

Поиск по типу (getByType(), getDefinitionByType(), findByType()) требует, чтобы граф сервисов был разрешён: все типы известны, индекс autowiring построен. Поэтому, когда вы вызываете один из этих методов, а граф изменился с последнего разрешения, билдер тут же разрешает весь известный граф. Во время самого разрешения любой поиск по типу запрещён и выбрасывает NotAllowedDuringResolvingException.

У поиска по тегу (findByTag()) такого требования нет: теги не зависят от типов, поэтому он работает в любой фазе.

По фазам:

  • loadConfiguration() (фаза A) – поиск по типу ненадёжен. Граф неполон: расширения, которые выполняются позже, ещё не зарегистрировали свои сервисы, а главное – нет пользовательской секции services: (она идёт последней). Вызов getByType() сработает, он вызовет преждевременное разрешение частичного графа, но ответ придёт из неполной картины, а само преждевременное разрешение потратит силы впустую. Правило: в loadConfiguration() только регистрируйте определения, не ищите по типу. findByTag() здесь допустим.
  • beforeCompile() (фаза B) – правильное место для исследования. К этому моменту существуют все определения (включая пользовательские), типы разрешены и индекс autowiring построен, поэтому getByType(), findByType() и findByTag() возвращают надёжные ответы. Аргументы ещё не заполнены autowiring: это следующий шаг (complete), после всех вызовов beforeCompile(). Когда вы меняете здесь определение, следующий getByType() незаметно перерешает граф, так что вы можете свободно чередовать правки и запросы.
  • afterCompile() (фаза C) – только код. Он работает над порождённым классом, а не над билдером. Граф уже готов; здесь вы формируете итоговый PHP.
Я хочу… Фаза
зарегистрировать сервис loadConfiguration()
искать по тегу и менять определения loadConfiguration() или beforeCompile()
искать по типу (getByType/findByType) beforeCompile()
зависеть от того, какие сервисы autowiring выбрал для аргументов не во время компиляции – смотрите это во время выполнения
поправить порождённый код afterCompile()
выполнить код после старта контейнера код инициализации

Внутри фазы B: resolve и complete

Фаза B состоит из двух проходов, между которыми зажаты вызовы beforeCompile():

$this->builder->resolve();     // типы разрешены, индекс autowiring построен
foreach ($this->extensions as $extension) {
	$extension->beforeCompile();
}
$this->builder->complete();    // ТОЛЬКО ТЕПЕРЬ аргументы заполняются autowiring

resolve() определяет тип каждого сервиса – берёт его из объявленного type либо выводит из фабрики: тип возвращаемого значения фабричного метода, класс, который она создаёт, или сервис, на который указывает ссылка, – и затем строит индекс autowiring, сопоставляющий каждому типу (классу вместе с его родителями и интерфейсами) имя сервиса. Сервис, помеченный autowired: false, в индекс не попадает; autowired: [A, B] сужает набор типов, под которыми он виден. Принципиально важно, что resolve улаживает типы, а не аргументы: заполнению аргументов через autowiring нужен готовый индекс, который появляется только после этого прохода.

complete() – место, где на самом деле происходит заполнение аргументов через autowiring. Для каждого определения он подставляет недостающие аргументы конструктора и настроек, разыскивая их типы в уже готовом индексе. Именно поэтому ссылки по типу оставались неразрешёнными во время resolve: этот поиск – дело complete, когда есть надёжный индекс, в котором можно искать.

Фаза C: генерация кода

generateCode() передаёт готовый граф в PhpGenerator, который порождает класс, расширяющий Container, с методом createServiceXxx() на каждый сервис, а также заранее вычисленные метаданные aliases, tags и wiring. Каждый Statement становится текстом PHP (new Foo(...), вызовы методов, обращения к свойствам), а каждая Reference – вызовом $this->getService(...).

Затем расширения получают заключительный проход afterCompile() над порождённым классом: именно здесь, например, выводятся геттеры статических и динамических параметров, – а вместе с ним возможность добавить код инициализации, выполняемый при каждом запросе.

Вся картина одним рисунком

КОМПИЛЯЦИЯ (однажды, в кеш)
│
├─ загрузка конфигурации          NEON -> Statement/массив; слияние файлов
│                                 @ в кавычках -> @@ ; сущности -> Statement
│
▼ Compiler::compile()
│
├─ ФАЗА A  processExtensions()
│   ├─ ParametersExtension (ПЕРВОЕ)   ── %param% РАЗВЁРНУТ по всей конфигурации
│   │                                    динамические -> выражение времени выполнения
│   ├─ ExtensionsExtension (ПЕРВОЕ)   ── регистрирует дальнейшие расширения
│   ├─ ...остальные расширения...     ── loadConfiguration(): только регистрация определений
│   └─ ServicesExtension (ПОСЛЕДНЕЕ)  ── services: -> объекты Definition
│                                        @name/@Type -> Reference
│   [граф полон по количеству; ТИПЫ и АРГУМЕНТЫ ещё нет; поиск по типу ненадёжен]
│
├─ ФАЗА B  processBeforeCompile()
│   ├─ builder.resolve()          ── разрешить все типы; построить индекс autowiring
│   │                                [типы готовы; индекс готов]
│   ├─ beforeCompile() расширения ── здесь getByType/findByType/findByTag БЕЗОПАСНЫ
│   │                                (аргументы ещё не заполнены autowiring)
│   └─ builder.complete()         ── заполнить АРГУМЕНТЫ; завершить перевод ссылок
│                                    ссылки по типу -> имена сервисов
│
└─ ФАЗА C  generateCode()
    ├─ PhpGenerator.generate()    ── Statement -> PHP; методы createServiceXxx()
    ├─ afterCompile() расширения  ── поправить код; вывести геттеры параметров
    └─ toString()                 ── итоговый PHP-код -> кеш

────────────────────────────────────────────────────────────

ВРЕМЯ ВЫПОЛНЕНИЯ (каждый запрос)
│
├─ new Container($dynamicParams)
├─ initialize()                   ── загрузочный код расширений (сессия, заголовки, валидация)
└─ getService()/getByType()       ── ленивые экземпляры из заранее вычисленных метаданных

Распространённые заблуждения

  • „В loadConfiguration() я поищу сервисы по типу.“ Нет: граф неполон (пользовательская секция services: выполняется после вас), и getByType() вызывает преждевременное разрешение частичного графа. Перенесите это в beforeCompile(). findByTag() здесь допустим.
  • „Значение из getenv() в параметре будет разным в каждой среде.“ Только если параметр динамический. Иначе оно впекается во время компиляции и остаётся везде одинаковым.
  • „Ссылка @Type – это уже имя сервиса.“ Нет: это ссылка по типу, которую autowiring разрешает в конкретное имя только на шаге complete.
  • „Моё расширение читает вспомогательный файл, но изменения не проявляются.“ Зарегистрируйте его через $builder->addDependency($file), иначе кеш о нём не знает и пересобираться не будет.
  • „Во время resolve() я могу вызвать getByType().“ Нет: он выбросит NotAllowedDuringResolvingException. Поиску по типу место в beforeCompile() или позже, но никогда посреди разрешения.