Пакет · di

Жизненный цикл разрешения

Вот что на самом деле происходит при вызове make() — прочитано из исходников, а не по памяти. Понимание порядка объясняет правила кэширования, ошибку циклической зависимости и то, почему overrides ведут себя именно так.

Конвейер make()

Каждый make($abstract, $overrides) проходит одну и ту же последовательность:

text
make($abstract, $overrides)

1. кэш resolved?      если $overrides пуст И $abstract есть в resolved  → вернуть
│                     (синглтоны и set()-значения живут здесь)
2. scopeOf($abstract) scope привязки → атрибут класса → 'transient'

3. кэш request?       если scope 'request' И $overrides пуст
│                     И контекст корутины содержит его                  → вернуть
4. защита от цикла    если $abstract уже в стеке ЭТОЙ единицы работы
│                     → бросить ContainerException с цепочкой
5. ждать синглтон?    если scope 'singleton' И его строит другая корутина
│                     → дождаться и вернуть её экземпляр
6. отметить сборку    положить $abstract в стек единицы работы

7. doResolve()        построить экземпляр (см. ниже)
8. injectProperties() заполнить свойства #[Autowired] / #[Inject] / #[Lazy]
9. cache()            если $overrides пуст: сохранить по scope
│                     finally: убрать $abstract из стека
└─ вернуть экземпляр

1 · Кэш resolved

Если overrides нет и абстракт уже в карте resolved, он возвращается сразу — без рефлексии. Эта карта хранит реализованные синглтоны, set()-значения и саморегистрированный контейнер (Container / ContainerInterface оба указывают на $this).

2 · Определение scope

scopeOf() определяет время жизни, по порядку: побеждает scope явной привязки; иначе scope-атрибут класса (#[Singleton] / #[Request] / #[Transient]); иначе transient.

3 · Кэш request-scope

Для request-scoped абстракта без overrides контейнер проверяет контекст корутины Swoole (Coroutine::getContext()['__di']) перед построением. Вне корутины такого контекста нет, и проверка проваливается насквозь — см. Request-scope и Swoole.

4 · Защита от циклических зависимостей

Перед построением абстракт кладётся в стек разрешения текущей единицы работы — под Swoole это контекст корутины, вне корутины обычное свойство контейнера. Если разрешение снова входит в make() для абстракта, который уже есть в этом стеке, — это цикл, и контейнер бросает исключение, называя всю цепочку:

text
ContainerException: Circular dependency detected while resolving
[App\SmsSendService] → [App\FakeSendService] → [App\SmsSendService].

Стек — именно свой, а не общий на воркер: соседний запрос, собирающий тот же класс в тот же момент, не считается циклом. Почему так и что было бы иначе — на странице Конкурентное разрешение.

Запись убирается в finally, поэтому неудачное разрешение никогда не оставляет устаревшую защиту. #[Lazy] обходит цикл, внедряя proxy вместо рекурсии — см. Разрыв циклических зависимостей.

5 · Ожидание синглтона

Если запрошен singleton, которого ещё нет в кэше, и его прямо сейчас строит другая корутина — make() дожидается её результата и возвращает тот же экземпляр вместо того, чтобы собирать второй. Так фабрика синглтона выполняется один раз на воркер даже при одновременном обращении. Ожидание возможно только на первом разрешении: дальше срабатывает шаг 1.

6 · Отметка сборки

Абстракт попадает в стек текущей единицы работы — именно эту запись увидит шаг 4, если разрешение вложенной зависимости вернётся к тому же классу.

7 · doResolve — конструирование

text
привязка есть?
├─ concrete — callable  → concrete($container)              (фабрика-замыкание)
└─ concrete — класс     → resolver->resolve(concrete)       (автовайринг конструктора)
привязки нет?
├─ class_exists         → resolver->resolve($abstract)      (автовайринг напрямую)
└─ иначе                → бросить NotFoundException

resolve() читает параметры конструктора (через ReflectionCache, мемоизированно) и строит каждый аргумент. Конструктор без параметров — это просто new $class().

8 · Внедрение в свойства

После конструирования injectProperties() сканирует непродвинутые (non-promoted) свойства класса на #[Autowired], #[Inject] или #[Lazy] и заполняет каждое. Свойства, продвинутые через конструктор, пропускаются — они уже установлены при конструировании.

9 · Кэширование

Только когда $overrides пуст:

Scope Кэшируется в
singleton карте resolved на уровне процесса
request контексте корутины (Swoole) или карте resolved (FPM / CLI)
transient не кэшируется

Почему overrides никогда не кэшируются

Шаги 1, 3 и 9 все проверяют, что $overrides пуст. Экземпляр, построенный с overrides, — не канонический объект для этого абстракта, поэтому его кэширование отравило бы каждое последующее разрешение. Это намеренное правило: overrides всегда строят свежее и никогда не разделяются.

Как разрешается один аргумент

Внутри resolve() / call() каждый параметр определяется первым подходящим правилом — это тот приоритет, на который вы полагаетесь, смешивая overrides, атрибуты и значения по умолчанию:

text
для каждого параметра:
1. имя в $overrides?            → использовать значение override
2. есть #[Inject]?              → разрешить id (или объявленный тип); lazy → proxy
3. типизирован (не builtin)?    → makeContextual(type)
                                   └─ NotFoundException + есть default → использовать default
                                   └─ #[Lazy] → внедрить proxy вместо этого
4. есть значение по умолчанию?  → использовать его
5. опциональный?                → пропустить
6. иначе                        → бросить ContainerException

Два следствия, которые стоит усвоить:

  • Разрешимая зависимость со значением по умолчанию всё равно внедряется. Значение по умолчанию — это запасной вариант, используемый только когда тип разрешить нельзя, а не повод пропускать внедрение.
  • makeContextual() — точка входа внедрения, а не make(). Именно это позволяет фабрике contextual() видеть потребителя при внедрении, тогда как прямой make() — нет.

Связанное