Ленивые proxy
#[Lazy] внедряет не реальный объект, а нативный ленивый proxy PHP 8.4:
совместимый по типу заместитель, который разрешает реальный экземпляр из контейнера при первом
использовании. Это механизм разрыва циклических зависимостей; вот как он строится.
Ключевой вызов
Когда резолвер встречает #[Lazy]-зависимость, он строит proxy через нативные ленивые объекты
PHP 8.4, а не через самописную обёртку:
$ref = ReflectionCache::classOf($type);
return $ref->newLazyProxy(
static fn(object $proxy): object => $container->makeContextual($type, $consumer)
);newLazyProxy() возвращает объект, который является экземпляром $type, но пока не несёт
реального состояния. Замыкание — это инициализатор: PHP вызывает его при первом обращении к
любому свойству или методу proxy, и то, что оно вернёт, становится backing-экземпляром. Здесь
инициализатор — обычное разрешение из контейнера — makeContextual($type, $consumer) — поэтому
лениво построенный объект проходит тот же конвейер, включая контекстные фабрики.
Хронология
момент внедрения newLazyProxy(...) → proxy создан, инициализатор НЕ запущен
isUninitializedLazyObject() = true
... (конструктор вернулся, цикл обойдён)
первое обращение $proxy->method() → инициализатор запущен: makeContextual($type)
proxy теперь стоит на реальном экземпляре
isUninitializedLazyObject() = false
позже $proxy->method() → перенаправлено прямо к реальному экземпляруПоскольку конструирование реального объекта отложено за точку, где возвращается внедряющий конструктор, цикл зависимостей больше не рекурсирует бесконечно — ленивой нужна лишь одна сторона. В этом весь трюк разрыва циклических зависимостей.
Почему только конкретные классы
Proxy должен быть экземпляром целевого типа — PHP генерирует его как подобие подкласса. Для
интерфейса или абстрактного класса наследовать нечего, поэтому резолвер защищается от обоих до
вызова newLazyProxy():
if (!class_exists($type)) {
throw new ContainerException(
"#[Lazy] requires a concrete class to proxy, got [{$type}]. "
. 'Pair it with #[Inject(Concrete::class)] for interface-typed dependencies.'
);
}
if (ReflectionCache::classOf($type)->isAbstract()) {
throw new ContainerException("#[Lazy] cannot proxy an abstract class [{$type}].");
}Поэтому для зависимости с типом-интерфейсом вы называете конкретный класс, который proxy должен замещать:
public function __construct(
#[Inject(SmsSendService::class), Lazy] private SendInterface $peer,
) {}Здесь #[Inject] предоставляет конкретный $type (SmsSendService), из которого строится
proxy, тогда как параметр остаётся типизированным интерфейсом.
Гарантии прозрачности
Proxy спроектирован быть неотличимым от реального объекта при обычном использовании:
instanceofпроходит — proxy является реальным экземпляром$type, поэтому проверки типов и сигнатуры его принимают.- Неинициализирован до использования —
ReflectionClass::isUninitializedLazyObject($proxy)возвращаетtrue, пока первое обращение не запустит инициализатор. - Разрешается один раз — после запуска инициализатора proxy навсегда стоит на этом экземпляре; последующие вызовы перенаправляются напрямую без повторного разрешения.
Нативный, не userland
Здесь нет самописного класса proxy, магии __call или кодогенерации, которую надо
поддерживать — это фича ленивых объектов уровня движка, добавленная в PHP 8.4, что и есть
причина, по которой пакету нужен PHP ≥ 8.4.
Всё же это выход, а не решение
Ленивый proxy лечит симптом. Настоящую циклическую зависимость обычно лучше исправить,
вынеся общую заботу в третий сервис, от которого зависят обе стороны. Прибегайте к #[Lazy],
когда этот рефакторинг того не стоит — та же позиция, что у Spring по @Lazy.
Связанное
- Разрыв циклических зависимостей — как это делать
- Жизненный цикл разрешения — где находится ветка proxy
- Атрибуты —
#[Lazy]и#[Inject]