Как работает 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()]);
}
}
Алгоритм разрешения
- Контейнер читает type-hint параметра конструктора
- Ищет сервис с совпадающим интерфейсом/классом
- Если найден один -- внедряет
- Если найдено несколько -- ошибка (нужен alias или
#[Autowire]) - Если не найден -- ошибка компиляции
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]решают проблему множественных реализаций