Компиляция в подробностях
Эта страница раскрывает компиляцию контейнера: через какие
фазы она проходит, когда разворачиваются параметры конфигурации,
когда строки @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()или позже, но никогда посреди разрешения.