MidПрактика5 min

Autowiring и регистрация

Autowiring, autoconfigure, #[Autowire], #[AutowireLocator], service locators

Как работает Autowiring

Autowiring -- механизм автоматического разрешения зависимостей. Контейнер анализирует type-hints конструктора и автоматически находит подходящий сервис.

<?php

declare(strict_types=1);

namespace App\Service;

use Psr\Log\LoggerInterface;
use Doctrine\ORM\EntityManagerInterface;

// All dependencies resolved automatically via type-hints
final class OrderProcessor
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly LoggerInterface $logger,
        private readonly MailerInterface $mailer,
    ) {
    }

    public function process(Order $order): void
    {
        $order->setStatus(OrderStatus::Processing);
        $this->entityManager->flush();
        $this->logger->info('Order processed', ['id' => $order->getId()]);
    }
}

Алгоритм разрешения

  1. Контейнер читает type-hint параметра конструктора
  2. Ищет сервис с совпадающим интерфейсом/классом
  3. Если найден один -- внедряет
  4. Если найдено несколько -- ошибка (нужен alias или #[Autowire])
  5. Если не найден -- ошибка компиляции

Autoconfigure

Autoconfigure автоматически применяет конфигурацию на основе реализуемых интерфейсов.

# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true    # Enables autoconfiguration

    App\:
        resource: '../src/'

Что делает autoconfigure

Интерфейс/Класс Автоматическая конфигурация
EventSubscriberInterface Тег kernel.event_subscriber
CommandInterface Тег console.command
VoterInterface Тег security.voter
TwigExtension Тег twig.extension
ConstraintValidator Тег validator.constraint_validator
DataCollectorInterface Тег data_collector
MessageHandlerInterface Тег messenger.message_handler
<?php

declare(strict_types=1);

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

// Automatically tagged as kernel.event_subscriber thanks to autoconfigure
final class LocaleSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST => [['onKernelRequest', 20]],
        ];
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        $request = $event->getRequest();
        $locale = $request->query->get('_locale', 'en');
        $request->setLocale($locale);
    }
}

Атрибут #[Autowire]

Атрибут #[Autowire] позволяет точно управлять тем, что внедряется в параметр.

<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class ReportGenerator
{
    public function __construct(
        // Inject a specific service by ID
        #[Autowire(service: 'monolog.logger.report')]
        private readonly LoggerInterface $logger,

        // Inject a parameter value
        #[Autowire('%app.report_dir%')]
        private readonly string $reportDir,

        // Inject an environment variable
        #[Autowire('%env(REPORT_API_KEY)%')]
        private readonly string $apiKey,

        // Inject an expression
        #[Autowire(expression: 'service("kernel").getProjectDir()')]
        private readonly string $projectDir,
    ) {
    }
}

Варианты #[Autowire]

Синтаксис Описание
#[Autowire(service: 'id')] Конкретный сервис по ID
#[Autowire('%param%')] Значение параметра
#[Autowire('%env(VAR)%')] Переменная окружения
#[Autowire(expression: '...')] Expression Language
#[Autowire(lazy: true)] Ленивая загрузка сервиса

#[AutowireLocator] и Service Locator

Symfony 8.0: #[AutowireLocator] -- один из новых атрибутов, явно добавленных в экзамен Sf8. Service locators теперь выделенная тема.

Service Locator -- специальный контейнер, содержащий подмножество сервисов. Он полезен, когда нужен доступ к нескольким сервисам, но не ко всем сразу.

<?php

declare(strict_types=1);

namespace App\Service;

use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;

final class NotificationManager
{
    public function __construct(
        // Service locator with specific services
        #[AutowireLocator([
            'email' => EmailNotifier::class,
            'sms' => SmsNotifier::class,
            'push' => PushNotifier::class,
        ])]
        private readonly ContainerInterface $notifiers,
    ) {
    }

    public function notify(string $channel, string $message): void
    {
        if (!$this->notifiers->has($channel)) {
            throw new \InvalidArgumentException(
                sprintf('Unknown notification channel "%s"', $channel)
            );
        }

        $notifier = $this->notifiers->get($channel);
        $notifier->send($message);
    }
}

Service Locator через Tagged Services

<?php

declare(strict_types=1);

namespace App\Service;

use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;

final class HandlerRegistry
{
    public function __construct(
        // Locator from tagged services
        #[AutowireLocator('app.handler')]
        private readonly ContainerInterface $handlers,
    ) {
    }

    public function handle(string $type, mixed $data): mixed
    {
        return $this->handlers->get($type)->handle($data);
    }
}

Разница: Service Locator vs Итерация

<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Psr\Container\ContainerInterface;

final class ProcessorManager
{
    public function __construct(
        // Iterator -- iterates ALL services (eager by default)
        #[AutowireIterator('app.processor')]
        private readonly iterable $allProcessors,

        // Locator -- gets specific service by key (lazy)
        #[AutowireLocator('app.processor')]
        private readonly ContainerInterface $processorLocator,
    ) {
    }

    // Use iterator when you need ALL
    public function processAll(mixed $data): void
    {
        foreach ($this->allProcessors as $processor) {
            $processor->process($data);
        }
    }

    // Use locator when you need ONE specific
    public function processBy(string $name, mixed $data): mixed
    {
        return $this->processorLocator->get($name)->process($data);
    }
}

Когда Autowiring не срабатывает

Подвох экзамена: Autowiring не работает в нескольких конкретных ситуациях. На экзамене часто проверяют понимание этих случаев.

Случай 1: Несколько реализаций интерфейса

<?php

declare(strict_types=1);

namespace App\Service;

interface NotifierInterface
{
    public function send(string $message): void;
}

final class EmailNotifier implements NotifierInterface
{
    public function send(string $message): void { /* ... */ }
}

final class SmsNotifier implements NotifierInterface
{
    public function send(string $message): void { /* ... */ }
}

// ERROR: Cannot autowire -- multiple implementations found
final class OrderService
{
    public function __construct(
        private readonly NotifierInterface $notifier, // Which one?
    ) {
    }
}

Решение 1 -- alias:

services:
    App\Service\NotifierInterface: '@App\Service\EmailNotifier'

Решение 2 -- #[Autowire]:

<?php

declare(strict_types=1);

use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class OrderService
{
    public function __construct(
        #[Autowire(service: EmailNotifier::class)]
        private readonly NotifierInterface $notifier,
    ) {
    }
}

Решение 3 -- Named autowiring (по имени параметра):

services:
    App\Service\NotifierInterface $emailNotifier: '@App\Service\EmailNotifier'
    App\Service\NotifierInterface $smsNotifier: '@App\Service\SmsNotifier'

Случай 2: Скалярные аргументы

<?php

declare(strict_types=1);

// Cannot autowire scalar types
final class ApiClient
{
    public function __construct(
        private readonly string $apiUrl,   // No type-hint match
        private readonly int $timeout,      // No type-hint match
    ) {
    }
}

Решение:

services:
    App\Service\ApiClient:
        arguments:
            $apiUrl: '%env(API_URL)%'
            $timeout: '%env(int:API_TIMEOUT)%'

Или через атрибут:

<?php

declare(strict_types=1);

use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class ApiClient
{
    public function __construct(
        #[Autowire('%env(API_URL)%')]
        private readonly string $apiUrl,
        #[Autowire('%env(int:API_TIMEOUT)%')]
        private readonly int $timeout,
    ) {
    }
}

Случай 3: Класс не в src/

Если класс находится вне директории, указанной в resource, он не будет зарегистрирован автоматически.

services:
    App\:
        resource: '../src/'
        exclude:
            - '../src/Entity/'   # Entities excluded!

#[Autoconfigure]

Атрибут для настройки поведения сервиса через PHP.

<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

#[Autoconfigure(
    lazy: true,           // Lazy-proxy
    shared: false,        // New instance each time
    public: true,         // Accessible via $container->get()
    bind: ['$timeout' => 30],
)]
final class HeavyProcessor
{
    public function __construct(
        private readonly int $timeout,
    ) {
    }
}

Target-атрибут для именованного autowiring

<?php

declare(strict_types=1);

namespace App\DependencyInjection;

use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
use Symfony\Component\DependencyInjection\Attribute\Target;

// Define a custom target
#[\Attribute(\Attribute::TARGET_PARAMETER)]
final class SlowTransport extends Target
{
}

// Usage
final class MessageDispatcher
{
    public function __construct(
        #[SlowTransport]
        private readonly TransportInterface $transport,
    ) {
    }
}

Итоги

  • Autowiring разрешает зависимости по type-hints конструктора
  • Autoconfigure автоматически тегирует сервисы по интерфейсам
  • #[Autowire] даёт точный контроль над внедрением
  • #[AutowireLocator] создаёт ленивый Service Locator (Sf8)
  • Autowiring ломается при множественных реализациях и скалярных типах
  • Named autowiring и #[Target] решают проблему множественных реализаций