Привет, Хабр!
DI‑контейнер — сердечко Symfony. Контроллеры, сервисы, слушатели событий, консольные команды, Voter, нормалайзеры — всё это сервисы, которые живут в контейнере и получают зависимости через него. Многие знают autowiring: поставил тайпхинт в конструктор — зависимость пришла. Но контейнер умеет гораздо больше.
Разберём три уровня глубины: autowiring для повседневной работы, теги для расширяемых архитектур, compiler passes для магии уровня фреймворка.
Autowiring
Контейнер на этапе компиляции смотрит на тайпхинты конструктора и находит сервис с подходящим типом.
class OrderService { public function __construct( private EntityManagerInterface $em, private MailerInterface $mailer, private LoggerInterface $logger, ) {} }
Для каждого параметра контейнер ищет: есть ли сервис с ID, совпадающим с FQCN интерфейса? Есть ли алиас?
Проблема начинается, когда сервисов с одним интерфейсом несколько. Два логгера (monolog.logger.app и monolog.logger.mailer)? Три кеша (cache.app, cache.system, cache.validator)? Autowiring не знает, какой выбрать, и бросает исключение при компиляции.
Решение находится в атрибуте #[Autowire]:
use Symfony\Component\DependencyInjection\Attribute\Autowire; class ReportService { public function __construct( // Конкретный сервис по ID #[Autowire(service: 'monolog.logger.reports')] private LoggerInterface $logger, // Параметр контейнера #[Autowire('%kernel.project_dir%/var/reports')] private string $reportsDir, // Переменная окружения #[Autowire(env: 'REPORT_API_KEY')] private string $apiKey, // Выражение (Expression Language) #[Autowire(expression: 'service("security.token_storage").getToken()?.getUser()')] private ?User $currentUser, ) {} }
#[Autowire] подставляет конкретный сервис, параметр, env‑переменную или результат выражения.
Несколько реализаций одного интерфейса
Когда у интерфейса две реализации — не лепите #[Autowire(service:)] на каждый потребитель.
Задайте алиас:
# config/services.yaml services: # По умолчанию PaymentGatewayInterface → Stripe App\Service\PaymentGatewayInterface: alias: App\Service\StripeGateway # Если параметр называется $backupGateway → PayPal App\Service\PaymentGatewayInterface $backupGateway: alias: App\Service\PayPalGateway
class CheckoutService { public function __construct( private PaymentGatewayInterface $gateway, // → Stripe private PaymentGatewayInterface $backupGateway, // → PayPal ) {} }
Контейнер выбирает реализацию по имени параметра. Чисто, явно, конфигурация в одном месте. Все потребители с параметром $gateway получат Stripe, все с $backupGateway — PayPal.
Другой подход — #[AsAlias] прямо на классе:
#[AsAlias(PaymentGatewayInterface::class)] class StripeGateway implements PaymentGatewayInterface { /* ... */ }
Autoconfigure: автоматические теги по интерфейсу
Symfony автоматически тегирует сервисы на основе реализуемых интерфейсов. Если класс реализует EventSubscriberInterface — получает тег kernel.event_subscriber. Если наследует Command — тег console.command. Если реализует VoterInterface — security.voter.
Это работает благодаря autoconfigure: true в services.yaml (включено по умолчанию):
services: _defaults: autowire: true autoconfigure: true # Вот это App\: resource: '../src/' exclude: '../src/{Entity,Kernel.php}'
Вам не нужно вручную тегировать стандартные сервисы. Создали класс, реализовали интерфейс — Symfony сама разберётся.
Теги: расширяемая архитектура
Стандартные теги — это удобно. Но суперсила тегов в ваших собственных расширяемых точках. Задача: система экспорта с несколькими форматами, где новые форматы добавляются без изменения существующего кода.
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; #[AutoconfigureTag('app.exporter')] interface ExporterInterface { public function supports(string $format): bool; public function export(array $data): string; }
#[AutoconfigureTag] на интерфейсе — и каждый класс, который его реализует, автоматически получает тег app.exporter. Без YAML и без ручной разметки.
Реализации:
class CsvExporter implements ExporterInterface { public function supports(string $format): bool { return $format === 'csv'; } public function export(array $data): string { $output = implode(';', array_keys($data[0])) . "\n"; foreach ($data as $row) { $output .= implode(';', $row) . "\n"; } return $output; } } class JsonExporter implements ExporterInterface { public function supports(string $format): bool { return $format === 'json'; } public function export(array $data): string { return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } } class XlsxExporter implements ExporterInterface { public function __construct(private SpreadsheetFactory $factory) {} public function supports(string $format): bool { return $format === 'xlsx'; } public function export(array $data): string { /* ... */ } }
Собираем через #[TaggedIterator]:
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; class ExportService { /** @var iterable<ExporterInterface> */ private iterable $exporters; public function __construct( #[TaggedIterator('app.exporter')] iterable $exporters ) { $this->exporters = $exporters; } public function export(array $data, string $format): string { foreach ($this->exporters as $exporter) { if ($exporter->supports($format)) { return $exporter->export($data); } } throw new \InvalidArgumentException( sprintf('Формат "%s" не поддерживается', $format) ); } public function supportedFormats(): array { $formats = []; foreach ($this->exporters as $exporter) { // Можно расширить интерфейс методом getFormat() } return $formats; } }
Добавить новый формат — создать класс, имплементировать ExporterInterface. Всё. ExportService не меняется, конфиг не меняется. Контейнер сам найдёт новый сервис по тегу.
TaggedLocator: ленивая загрузка по ключу
TaggedIterator инстанцирует все сервисы при первом обращении к итератору. Если экспортёров 20 и нужен только один, то создавать все 20 как будто бы расточительно.
TaggedLocator — это Service Locator, который создаёт сервис только при get():
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; #[AutoconfigureTag('app.exporter', attributes: ['key' => 'csv'])] class CsvExporter implements ExporterInterface { /* ... */ } #[AutoconfigureTag('app.exporter', attributes: ['key' => 'json'])] class JsonExporter implements ExporterInterface { /* ... */ } #[AutoconfigureTag('app.exporter', attributes: ['key' => 'xlsx'])] class XlsxExporter implements ExporterInterface { /* ... */ }
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; use Psr\Container\ContainerInterface; class ExportService { public function __construct( #[TaggedLocator('app.exporter', indexAttribute: 'key')] private ContainerInterface $exporters ) {} public function export(array $data, string $format): string { if (!$this->exporters->has($format)) { throw new \InvalidArgumentException("Формат '$format' не поддерживается"); } // Создаётся только нужный экспортёр return $this->exporters->get($format)->export($data); } }
get('csv') создаёт только CsvExporter. XlsxExporter с его тяжёлой зависимостью SpreadsheetFactory даже не трогается.
Приоритет тегов
Тегам можно задать приоритет — порядок, в котором сервисы появятся в итераторе:
#[AutoconfigureTag('app.exporter', attributes: ['priority' => 10])] class CsvExporter implements ExporterInterface { /* ... */ } #[AutoconfigureTag('app.exporter', attributes: ['priority' => 20])] class JsonExporter implements ExporterInterface { /* ... */ }
Чем выше priority, тем раньше сервис в итераторе. По умолчанию 0.
Compiler Pass: вмешиваемся в сборку контейнера
Compiler Pass — код, который выполняется один раз при компиляции контейнера. Это самый мощный инструмент, но и самый редко нужный в прикладном коде. Фреймворк использует их повсюду (регистрация Twig‑расширений, подключение Event Listener'ов, настройка маршрутов), но вам Compiler Pass понадобится, когда TaggedIterator/TaggedLocator не хватает.
Самый частый кейс видится в кастомной логике при сборке: валидация конфигурации, динамическое изменение определений, сложная сортировка:
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; class ValidateExportersPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { $tagged = $container->findTaggedServiceIds('app.exporter'); $formats = []; foreach ($tagged as $serviceId => $tags) { $format = $tags[0]['key'] ?? null; if (!$format) { throw new \LogicException( "Экспортёр $serviceId должен указать атрибут 'key' в теге" ); } if (isset($formats[$format])) { throw new \LogicException( "Дублирование формата '$format': $serviceId и {$formats[$format]}" ); } $formats[$format] = $serviceId; } } }
Этот pass проверяет при компиляции, что у каждого экспортёра есть ключ и нет дубликатов. Ошибка обнаружится при cache:clear, а не в рантайме при запросе пользователя.
Регистрация:
// src/Kernel.php use Symfony\Component\DependencyInjection\ContainerBuilder; class Kernel extends BaseKernel { protected function build(ContainerBuilder $container): void { $container->addCompilerPass(new ValidateExportersPass()); } }
Compiler Pass запускается при компиляции контейнера — в дев‑окружении при каждом изменении конфига/сервисов, в проде при cache:clear / деплое..
Отладка: что в контейнере?
Когда что‑то не работает — смотрите в контейнер:
# Все сервисы php bin/console debug:container # Поиск по имени php bin/console debug:container mailer # Информация о конкретном сервисе php bin/console debug:container App\\Service\\OrderService # Все сервисы с тегом php bin/console debug:container --tag=app.exporter # Autowiring — какие типы доступны php bin/console debug:autowiring # Поиск по типу php bin/console debug:autowiring Logger
debug:autowiring — самая полезная команда при проблемах с autowiring. Показывает все доступные типы и какой сервис за ними стоит.
Хотите понять, насколько вы готовы решать реальные задачи на Symfony, а не только разбираться в базовом синтаксисе? Пройдите тестирование: оно поможет быстро оценить уровень и понять, в каких темах стоит усилиться. |
Когда что использовать
Autowiring +
#[Autowire]80% задач.Теги +
TaggedIterator/TaggedLocatorдля плагинов, форматов стратегий, хендлеров, процессоры.Compiler Pass — валидация при сборке, динамическое изменение определений, интеграция с бандлами.

Когда бизнес‑логика начинает расползаться по if/else, а статусы и переходы становятся источником ошибок, это сигнал, что систему пора проектировать иначе. Курс Symfony Framework поможет разобраться, как строить приложения на Symfony так, чтобы логика оставалась прозрачной, расширяемой и удобной для тестирования.
➦ [Забрать курс Symfony со скидкой]
А если давно думали о системном обучении, лучше не тянуть: 30–31 марта действует скидка 10% по промокоду birthday на любые курсы OTUS, и она суммируется с другими скидками. Самое время забрать курс по более выгодной цене уже сейчас.
Для первого погружения подключайтесь к открытому уроку:
22 апреля в 20:00 — «Symfony Workflow: конечный автомат для реализации бизнес-логики».
На нём покажут подход, который особенно полезен там, где в приложении много статусов, переходов и правил.
➦ [Хочу на открытый урок]
