Разрыв циклических зависимостей
Когда A нужен B, а B нужен A, жадное разрешение не может завершиться — контейнер обнаруживает
цикл и бросает исключение. #[Lazy] — это выход: он внедряет proxy вместо реального объекта,
откладывая разрешение до первого использования, так что менять нужно лишь одну сторону цикла.
Симптом
Два сервиса, ссылающиеся друг на друга, срабатывают на защите от циклических зависимостей:
class SmsSendService
{
public function __construct(private FakeSendService $peer) {}
}
class FakeSendService
{
public function __construct(private SmsSendService $peer) {}
}
$container->make(SmsSendService::class);
// ContainerException: Circular dependency detected while resolving [SmsSendService].Разрешение SmsSendService требует FakeSendService, которому снова нужен SmsSendService —
бесконечный регресс, в который контейнер входить отказывается.
Решение — сделайте одну сторону ленивой
Добавьте #[Lazy] к одной стороне цикла. Он внедряет нативный ленивый proxy: заместитель,
который выглядит и ведёт себя точно как реальный класс, но не строит его до первого вызова
метода. К этому моменту конструктор уже вернулся, поэтому цикл не рекурсирует.
<?php
use Flytachi\Winter\DI\Attribute\Lazy;
use Flytachi\Winter\DI\Attribute\Autowired;
class SmsSendService
{
#[Lazy]
private FakeSendService $peer; // proxy сейчас; реальный make() при первом $this->peer->...()
public function send(string $to): void
{
$this->peer->deliver($to); // proxy разрешается здесь, при первом обращении
}
}
class FakeSendService
{
#[Autowired]
private SmsSendService $peer; // жадная обратная ссылка — цикл больше не рекурсирует
}#[Lazy] работает и на свойствах, и на параметрах конструктора, отдельно или в сочетании с
#[Autowired] / #[Inject].
Интерфейсам нужен конкретный класс
Proxy должен замещать конкретный класс — он не может проксировать интерфейс или абстрактный
класс, потому что нечего наследовать. Когда зависимость типизирована интерфейсом, укажите
конкретный класс через #[Inject]:
public function __construct(
#[Inject(SmsSendService::class), Lazy] private SendInterface $peer,
) {}Пустой #[Lazy] на зависимости с типом-интерфейсом бросает понятное ContainerException с
подсказкой сочетать его с #[Inject(Concrete::class)]. То же — для абстрактного класса.
Proxy прозрачен
Внедрённый proxy совместим по типу с реальным классом (instanceof проходит) и остаётся
неинициализированным до первого использования — ReflectionClass::isUninitializedLazyObject()
возвращает true до этого момента. Ваш код не отличит его от реального объекта. Как работает
нативный proxy — в Ленивых proxy.
Лучше переработать цикл
Циклическая зависимость обычно — запах дизайна. Самое чистое решение — вынести общую часть в
третий сервис, от которого зависят обе стороны. #[Lazy] — прагматичный выход, когда этот
рефакторинг того не стоит — та же позиция, что у Spring по @Lazy.
Связанное
- Ленивые proxy — как строится нативный proxy PHP 8.4
- Жизненный цикл разрешения — где находится защита от циклов
- Атрибуты —
#[Lazy],#[Inject],#[Autowired]