Пакет · 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. защита от цикла    если building[$abstract]  → бросить ContainerException
│                     иначе building[$abstract] = true

5. doResolve()        построить экземпляр (см. ниже)
6. injectProperties() заполнить свойства #[Autowired] / #[Inject] / #[Lazy]
7. cache()            если $overrides пуст: сохранить по scope
│                     finally: unset building[$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 · Защита от циклических зависимостей

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

text
ContainerException: Circular dependency detected while resolving [SmsSendService].

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

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

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

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

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

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

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

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

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

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

Шаги 1, 3 и 7 все проверяют, что $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() — нет.

Связанное