Пакет · di

Ленивые proxy

#[Lazy] внедряет не реальный объект, а нативный ленивый proxy PHP 8.4: совместимый по типу заместитель, который разрешает реальный экземпляр из контейнера при первом использовании. Это механизм разрыва циклических зависимостей; вот как он строится.

Ключевой вызов

Когда резолвер встречает #[Lazy]-зависимость, он строит proxy через нативные ленивые объекты PHP 8.4, а не через самописную обёртку:

php
$ref = ReflectionCache::classOf($type);

return $ref->newLazyProxy(
  static fn(object $proxy): object => $container->makeContextual($type, $consumer)
);

newLazyProxy() возвращает объект, который является экземпляром $type, но пока не несёт реального состояния. Замыкание — это инициализатор: PHP вызывает его при первом обращении к любому свойству или методу proxy, и то, что оно вернёт, становится backing-экземпляром. Здесь инициализатор — обычное разрешение из контейнера — makeContextual($type, $consumer) — поэтому лениво построенный объект проходит тот же конвейер, включая контекстные фабрики.

Хронология

text
момент внедрения  newLazyProxy(...)        → proxy создан, инициализатор НЕ запущен
                                          isUninitializedLazyObject() = true
 ...            (конструктор вернулся, цикл обойдён)
первое обращение  $proxy->method()         → инициализатор запущен: makeContextual($type)
                                          proxy теперь стоит на реальном экземпляре
                                          isUninitializedLazyObject() = false
позже             $proxy->method()          → перенаправлено прямо к реальному экземпляру

Поскольку конструирование реального объекта отложено за точку, где возвращается внедряющий конструктор, цикл зависимостей больше не рекурсирует бесконечно — ленивой нужна лишь одна сторона. В этом весь трюк разрыва циклических зависимостей.

Почему только конкретные классы

Proxy должен быть экземпляром целевого типа — PHP генерирует его как подобие подкласса. Для интерфейса или абстрактного класса наследовать нечего, поэтому резолвер защищается от обоих до вызова newLazyProxy():

php
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 должен замещать:

php
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.

Связанное