Сервис-контейнер в Symfony: autowiring, теги, фабрики
Autowiring закрывает 90% случаев регистрации сервисов в Symfony, но ломается на скалярах, интерфейсах с двумя реализациями и коллекциях по тегам. Разбираем, когда чего хватает и чем платишь.

Вы добавили новый сервис, прописали ему зависимость через конструктор — и всё заработало само, без единой строчки в services.yaml. Это autowiring, и он закрывает большинство случаев в проекте на Symfony 7.x. Но как только у интерфейса появляется две реализации, в конструктор нужно передать строку из параметра или собрать все обработчики в одну коллекцию — контейнер перестаёт угадывать. Дальше начинается ручная настройка, и вариантов у неё больше, чем кажется.
Разбираем четыре способа связывать зависимости: чистый autowiring, привязка по имени аргумента, теги с коллекциями и фабрики. У каждого своя цена — где вы выигрываете читаемость, где теряете явность, а где рискуете получить неочевидную ошибку на этапе компиляции контейнера.
Как контейнер вообще связывает зависимости
Стандартный services.yaml в Symfony 7.x открывается блоком _defaults с autowire: true и autoconfigure: true. Первый включает автоматический подбор зависимостей по типам аргументов конструктора. Второй сам навешивает теги на классы, реализующие известные контейнеру интерфейсы — например, помечает подписчика событий, если он реализует EventSubscriberInterface.
Секция App\: с resource регистрирует все классы каталога как сервисы разом. Пока у каждой зависимости ровно один кандидат по типу — вам не нужно писать ничего. Контейнер компилируется в PHP-класс при прогреве кеша, и все связи резолвятся один раз, а не на каждый запрос.
Autowiring не магия во время выполнения. Это разрешение зависимостей на этапе компиляции контейнера. Если аргумент неоднозначен, вы узнаете об этом при cache:clear, а не в проде под нагрузкой.
Вариант 1. Чистый autowiring
Работает, когда тип аргумента однозначно указывает на один сервис. Класс, конкретный тип-хинт, единственная реализация интерфейса — всё резолвится само.
// src/Service/OrderNotifier.php
namespace App\Service;
class OrderNotifier
{
// MailerInterface — единственная реализация в контейнере,
// подставится автоматически
public function __construct(
private readonly MailerSender $sender,
) {
}
}
Цена: ничего не платите за настройку, но теряете явность. По services.yaml нельзя понять, что и куда подставится — читать нужно конструкторы. Ломается на скалярах (строка, число, массив) и на интерфейсе с несколькими реализациями.
Вариант 2. Привязка аргументов: bind и явный service
Когда в конструктор нужно передать строку из параметра или выбрать конкретную реализацию, есть два уровня.
Глобальный bind в _defaults — привязывает значение к имени аргумента для всех сервисов сразу:
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
bind:
# у любого сервиса аргумент $projectDir получит это значение
string $projectDir: '%kernel.project_dir%'
# интерфейс -> конкретный сервис для аргумента $primaryStorage
App\Storage\StorageInterface $primaryStorage: '@App\Storage\S3Storage'
Локальная настройка одного сервиса — когда исключение нужно только для конкретного класса:
# config/services.yaml
services:
App\Service\ReportBuilder:
arguments:
$format: 'pdf'
$storage: '@App\Storage\LocalStorage'
Начиная с Symfony 6.1 то же самое делается атрибутами прямо в коде, без правки YAML. Для скаляра из параметра — #[Autowire]:
use Symfony\Component\DependencyInjection\Attribute\Autowire;
public function __construct(
#[Autowire(param: 'kernel.project_dir')]
private readonly string $projectDir,
) {
}
Цена: bind экономит повторы, но привязка живёт в YAML отдельно от кода — легко забыть про неё при рефакторинге имени аргумента, и тогда молча подставится не то или упадёт компиляция. Атрибуты держат настройку рядом с зависимостью, но размазывают конфигурацию по классам: обзорной картины в одном файле уже нет.
Вариант 3. Теги и коллекции сервисов
Классическая задача: есть интерфейс обработчика, реализаций несколько, и все нужно собрать в одну точку — например, пройтись циклом по всем валидаторам. Здесь autowiring бессилен, потому что вам нужен не один сервис, а их набор.
С Symfony 5.3 есть #[AutowireIterator] и atributed-tags, но самый читаемый способ — #[AutoconfigureTag] на интерфейсе плюс #[TaggedIterator] (в 6.3+ переименован в AutowireIterator) на потребителе.
// src/Payment/PaymentHandlerInterface.php
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('app.payment_handler')]
interface PaymentHandlerInterface
{
public function supports(string $method): bool;
public function process(Payment $payment): void;
}
// src/Payment/PaymentProcessor.php
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
class PaymentProcessor
{
/**
* @param iterable<PaymentHandlerInterface> $handlers
*/
public function __construct(
#[AutowireIterator('app.payment_handler')]
private readonly iterable $handlers,
) {
}
public function handle(Payment $payment): void
{
foreach ($this->handlers as $handler) {
if ($handler->supports($payment->getMethod())) {
$handler->process($payment);
return;
}
}
throw new \RuntimeException('Нет обработчика для метода оплаты');
}
}
Каждый класс, реализующий PaymentHandlerInterface, автоматически получит тег и попадёт в коллекцию — добавлять новый обработчик можно, не трогая процессор.
Цена: порядок в iterable по умолчанию не гарантирован явно — если он важен, задавайте приоритет тегу через priority. Точное имя атрибута зависит от версии: до 6.3 это TaggedIterator, после — AutowireIterator; сверяйтесь с документацией под свою версию, а не копируйте вслепую.
Вариант 4. Фабрика
Когда объект нельзя просто «собрать из зависимостей» — его создание требует логики, внешнего вызова или условия — используют фабрику. Контейнер вместо new зовёт указанный метод.
# config/services.yaml
services:
App\Client\ApiClient:
factory: ['@App\Client\ApiClientFactory', 'create']
arguments:
$environment: '%kernel.environment%'
С Symfony 6.1 то же делается атрибутом #[AsDecorator] для декораторов или #[Autowire(expression: ...)] для выражений, но фабрика через YAML остаётся самым прозрачным вариантом для нетривиального создания.
Цена: фабрика прячет процесс создания за методом — отладка становится длиннее, а зависимости самой фабрики нужно тоже прописать. Не тащите фабрику туда, где хватает конструктора.
Когда что выбирать
| Ситуация | Что использовать | Почему |
|---|---|---|
| Один тип — одна реализация | Чистый autowiring | Ноль настройки, контейнер угадывает сам |
| Скаляр или параметр в конструктор | #[Autowire] или bind | autowiring по типу строку не резолвит |
| Интерфейс с двумя реализациями | Явный arguments или именованный bind | Нужно указать, какую именно подставить |
| Набор реализаций одного интерфейса | Теги + AutowireIterator | Нужна коллекция, а не один сервис |
| Создание требует логики или условия | Фабрика | Простой конструктор не справляется |
Правило по возрастанию сложности: пока хватает autowiring — не пишите ничего. Понадобился скаляр — атрибут рядом с аргументом. Появилась вторая реализация — явная привязка. Нужен набор — теги. И только когда объект нельзя собрать из зависимостей — фабрика.
Что ломается на практике
- Cannot autowire argument — контейнер не нашёл единственного кандидата по типу. Либо интерфейс без реализации, либо реализаций несколько. Читаете сообщение целиком: там перечислены найденные кандидаты.
- Скаляр без привязки —
string $apiUrlбез#[Autowire]или bind даст ошибку компиляции. autowiring по типуstringв принципе не работает. - Тихая подмена через bind — переименовали аргумент в конструкторе, а bind остался со старым именем. Привязка перестаёт срабатывать, подставляется дефолт, ошибки нет — баг находится только в рантайме.
- Приватные сервисы — по умолчанию сервисы приватны и недоступны через
$container->get(). Если тянете сервис вручную из контейнера — либо инжектите через конструктор, либо помечайтеpublic: trueосознанно.
Что делать дальше
Проверьте, что и как контейнер разрешил, через консоль: php bin/console debug:autowiring покажет доступные для автосвязывания типы, а debug:container — все зарегистрированные сервисы и их аргументы. Для конкретного сервиса — debug:container App\\Service\\OrderNotifier: увидите ровно то, что подставится. Прогоняйте эти команды перед деплоем — они ловят неоднозначности до того, как их поймает прод.
Источники: Symfony — Service Container, Symfony — Autowiring Services
Частые вопросы
У меня Symfony 5.4, атрибуты #[Autowire] не работают. Что делать?
Autowiring падает с Cannot autowire, хотя реализация есть. Почему?
Стоит ли отключать autowiring ради явности?
Как гарантировать порядок сервисов в коллекции по тегу?
Влияет ли autowiring на производительность в проде?
Можно ли смешивать атрибуты и настройку в YAML для одного сервиса?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.