Продвинутые возможности
Автоконфигурация, теги, ленивые прокси, циклические зависимости и обработка ошибок.
Содержание
Автоконфигурация
Автоконфигурация позволяет автоматически тегировать и настраивать сервисы на основе реализуемых интерфейсов или применённых атрибутов — аналогично автоконфигурации Symfony.
Декларативная: #[AutoconfigureTag]
Разместите атрибут #[AutoconfigureTag] на интерфейсе или пользовательском атрибуте, чтобы автоматически тегировать все реализующие/декорированные классы.
На интерфейсе
use AsceticSoft\Wirebox\Attribute\AutoconfigureTag;
#[AutoconfigureTag('command.handler')]
interface CommandHandlerInterface
{
public function __invoke(object $command): void;
}
// Автоматически тегируется как 'command.handler'
class CreateUserHandler implements CommandHandlerInterface
{
public function __invoke(object $command): void { /* ... */ }
}
class DeleteUserHandler implements CommandHandlerInterface
{
public function __invoke(object $command): void { /* ... */ }
}
На пользовательском атрибуте
use AsceticSoft\Wirebox\Attribute\AutoconfigureTag;
#[Attribute(Attribute::TARGET_CLASS)]
#[AutoconfigureTag('scheduler.task')]
class AsScheduled {}
// Автоматически тегируется как 'scheduler.task'
#[AsScheduled]
class DailyReportTask
{
public function run(): void { /* ... */ }
}
#[AsScheduled]
class CleanupTask
{
public function run(): void { /* ... */ }
}
Программная: registerForAutoconfiguration()
Для более тонкой настройки (время жизни, lazy-режим, множественные теги):
$builder->registerForAutoconfiguration(EventListenerInterface::class)
->tag('event.listener')
->singleton()
->lazy();
Любой класс, реализующий EventListenerInterface, найденный при scan(), автоматически:
- Получит тег
event.listener - Будет настроен как синглтон
- Будет использовать ленивые прокси
Работает и с пользовательскими атрибутами:
$builder->registerForAutoconfiguration(AsScheduled::class)
->tag('scheduler.task')
->transient();
Пример CQRS
Полная настройка command/query bus с автоконфигурацией:
use AsceticSoft\Wirebox\Attribute\AutoconfigureTag;
// Определяем интерфейсы обработчиков с автотегированием
#[AutoconfigureTag('command.handler')]
interface CommandHandlerInterface
{
public function __invoke(object $command): void;
}
#[AutoconfigureTag('query.handler')]
interface QueryHandlerInterface
{
public function __invoke(object $query): mixed;
}
// Обработчики команд — автоматически тегируются
class CreateUserHandler implements CommandHandlerInterface
{
public function __invoke(object $command): void { /* ... */ }
}
class DeleteUserHandler implements CommandHandlerInterface
{
public function __invoke(object $command): void { /* ... */ }
}
// Обработчики запросов — автоматически тегируются
class GetUserHandler implements QueryHandlerInterface
{
public function __invoke(object $query): mixed { /* ... */ }
}
Сборка и использование:
$builder = new ContainerBuilder(projectDir: __DIR__);
$builder->scan(__DIR__ . '/src');
$container = $builder->build();
// Все обработчики команд
foreach ($container->getTagged('command.handler') as $handler) {
// CreateUserHandler, DeleteUserHandler
}
// Все обработчики запросов
foreach ($container->getTagged('query.handler') as $handler) {
// GetUserHandler
}
Автосконфигурированные интерфейсы исключаются из проверки на неоднозначную привязку. Несколько реализаций CommandHandlerInterface не вызовут ошибку — они предполагают выборку через теги.
Тегированные сервисы
Теги группируют сервисы для коллективной выборки. Они необходимы для паттернов вроде диспетчеризации событий, цепочек middleware и систем плагинов.
Добавление тегов
Через атрибут:
use AsceticSoft\Wirebox\Attribute\Tag;
#[Tag('event.listener')]
class OrderCreatedListener { /* ... */ }
#[Tag('event.listener')]
class UserCreatedListener { /* ... */ }
#[Tag('event.listener')]
#[Tag('audit')]
class AuditListener { /* ... */ }
Через fluent API:
$builder->register(OrderCreatedListener::class)->tag('event.listener');
$builder->register(AuditListener::class)->tag('event.listener', 'audit');
Получение тегированных сервисов
// Все слушатели событий
foreach ($container->getTagged('event.listener') as $listener) {
$listener->handle($event);
}
// Все аудит-сервисы
foreach ($container->getTagged('audit') as $service) {
// ...
}
Пример диспетчера событий
class EventDispatcher
{
/** @var iterable<EventListenerInterface> */
private iterable $listeners;
public function __construct(ContainerInterface $container)
{
// Ленивые — слушатели не создаются до итерации
$this->listeners = $container->getTagged('event.listener');
}
public function dispatch(object $event): void
{
foreach ($this->listeners as $listener) {
$listener->handle($event);
}
}
}
Ленивые прокси
Ленивые прокси откладывают создание сервиса до момента его фактического использования. Работают на нативных ReflectionClass::newLazyProxy() PHP 8.4.
Как это работает
- При запросе ленивого сервиса немедленно возвращается прокси-объект
- Прокси — реальный экземпляр класса (проходит
instanceof) - При обращении к свойству или методу создаётся реальный экземпляр
- Последующие обращения используют уже созданный экземпляр
Lazy-режим по умолчанию
По умолчанию ContainerBuilder включает lazy-режим. Все сервисы ленивые, кроме:
- Явно помеченных
#[Eager] - Настроенных через
->eager()в fluent API
// Все сервисы в этом контейнере ленивые по умолчанию
$builder = new ContainerBuilder(projectDir: __DIR__);
$builder->scan(__DIR__ . '/src');
$container = $builder->build();
Для отключения:
$builder->defaultLazy(false);
// Теперь ленивыми будут только сервисы с #[Lazy]
Выигрыш в производительности
Ленивые прокси особенно полезны, когда:
- Сервисы имеют тяжёлые конструкторы (подключения к БД, HTTP-клиенты)
- У вас много сервисов, но в каждом запросе используются лишь несколько
- Сервисы имеют сложные цепочки зависимостей, которые нужны не всегда
#[Lazy]
class ElasticsearchClient
{
public function __construct()
{
// Дорого: установка соединения, проверка здоровья кластера
$this->client = ClientBuilder::create()
->setHosts(['elasticsearch:9200'])
->build();
}
}
// Прокси возвращается мгновенно — соединения нет
$client = $container->get(ElasticsearchClient::class);
// Соединение устанавливается только сейчас
$client->search(['index' => 'products', 'body' => [...]]);
Циклические зависимости
Wirebox обнаруживает циклические зависимости во время сборки — до того, как контейнер начнёт использоваться.
Безопасные циклы
Циклическая зависимость безопасна только когда все сервисы в цикле — ленивые синглтоны:
#[Lazy]
class ServiceA
{
public function __construct(public readonly ServiceB $b) {}
}
#[Lazy]
class ServiceB
{
public function __construct(public readonly ServiceA $a) {}
}
$container = $builder->build(); // OK — оба ленивые синглтоны
$a = $container->get(ServiceA::class);
assert($a->b->a === $a); // Тот же прокси-экземпляр
Почему это работает: прокси кешируется в хранилище синглтонов до создания реального экземпляра. Когда цепочка зависимостей замыкается, она находит прокси в кеше, а не входит в повторное создание.
Небезопасные циклы
| Сценарий | Результат |
|---|---|
| Все сервисы — ленивые синглтоны | Безопасно — прокси кешируется до создания |
| Любой сервис eager | Небезопасно — Autowirer обращается к тому же классу дважды |
| Любой сервис — ленивый transient | Небезопасно — прокси не кешируется, бесконечная рекурсия |
Небезопасные циклы дают понятные сообщения об ошибках:
Circular dependency detected: ServiceA -> ServiceB -> ServiceA.
All services in a circular dependency must be lazy singletons.
Unsafe: ServiceB (not lazy)
Если вы столкнулись с ошибкой циклической зависимости, решения:
- Сделать все сервисы в цикле ленивыми синглтонами (самый простой вариант)
- Рефакторинг для разрыва цикла (вынести общую зависимость)
- Использовать setter-инъекцию для отложенной зависимости
Определения на основе фабрик (register(..., fn() => ...)) пропускаются при анализе циклов, так как их зависимости нельзя определить статически.
Setter-инъекция
Настройка вызова методов после создания сервиса:
$builder->register(Mailer::class)
->call('setTransport', [SmtpTransport::class])
->call('setLogger', [FileLogger::class]);
Контейнер разрешает аргументы-имена классов из контейнера, а скалярные аргументы передаёт как есть. Несколько вызовов call() выполняются по порядку.
Когда использовать
- Необязательные зависимости, которые не должны быть в конструкторе
- Интеграция с фреймворком, где сервисы настраиваются после создания
- Разрыв циклических зависимостей путём отложенного ребра
Саморегистрация
Контейнер регистрирует себя как Psr\Container\ContainerInterface. Его можно запросить через type hint:
use Psr\Container\ContainerInterface;
class ServiceLocator
{
public function __construct(
private ContainerInterface $container,
) {
}
public function getService(string $id): object
{
return $this->container->get($id);
}
}
Инъекция самого контейнера — это антипаттерн Service Locator. Предпочитайте конструкторную инъекцию конкретных зависимостей. Используйте саморегистрацию только когда действительно нужно динамическое разрешение сервисов (загрузчики плагинов, command bus).
Обработка ошибок
Wirebox выбрасывает специфичные, описательные исключения:
| Исключение | Когда возникает |
|---|---|
NotFoundException |
Сервис не найден и не может быть автовайрен |
AutowireException |
Не удаётся разрешить параметр конструктора (нет type hint, неразрешимый тип) |
CircularDependencyException |
Обнаружена небезопасная циклическая зависимость при сборке |
ContainerException |
Общие ошибки контейнера (неоднозначные привязки, неверная конфигурация) |
Все исключения реализуют Psr\Container\ContainerExceptionInterface.
Пример: перехват ошибок
use AsceticSoft\Wirebox\Exception\CircularDependencyException;
use AsceticSoft\Wirebox\Exception\ContainerException;
use Psr\Container\NotFoundExceptionInterface;
try {
$container = $builder->build();
} catch (CircularDependencyException $e) {
// "Circular dependency detected: A -> B -> A. ..."
error_log($e->getMessage());
} catch (ContainerException $e) {
// "Ambiguous auto-binding for PaymentInterface: StripePayment, PayPalPayment"
error_log($e->getMessage());
}
try {
$service = $container->get('NonExistentService');
} catch (NotFoundExceptionInterface $e) {
// Сервис не найден
}
Все исключения содержат подробные сообщения с полным путём зависимости, что упрощает диагностику проблем.