Жизненный цикл разрешения
Вот что на самом деле происходит при вызове make() — прочитано из исходников, а не
по памяти. Понимание порядка объясняет правила кэширования, ошибку циклической зависимости и то,
почему overrides ведут себя именно так.
Конвейер make()
Каждый make($abstract, $overrides) проходит одну и ту же последовательность:
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() для уже строящегося абстракта — это цикл, и контейнер бросает:
ContainerException: Circular dependency detected while resolving [SmsSendService].Запись очищается в finally, поэтому неудачное разрешение никогда не оставляет устаревшую
защиту. #[Lazy] обходит это, внедряя proxy вместо рекурсии — см.
Разрыв циклических зависимостей.
5 · doResolve — конструирование
привязка есть?
├─ concrete — callable → concrete($container) (фабрика-замыкание)
└─ concrete — класс → resolver->resolve(concrete) (автовайринг конструктора)
привязки нет?
├─ class_exists → resolver->resolve($abstract) (автовайринг напрямую)
└─ иначе → бросить NotFoundExceptionresolve() читает параметры конструктора (через 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, атрибуты и значения по умолчанию:
для каждого параметра:
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()— нет.
Связанное
- Кэш рефлексии — как мемоизируются метаданные параметров
- Request-scope и Swoole — кэш корутины на шаге 3
- Ленивые proxy — ветка proxy на шагах 5–6
- Справочник API — сигнатуры
make()/call()