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

Autowiring в Symfony делает большую часть работы за вас: тайп-хинт в конструкторе — и контейнер сам подставит нужный объект. Но как только у вас два класса реализуют один интерфейс, или в конструктор нужно передать строку из .env, или сервис тянет полтора десятка зависимостей ради одного публичного метода — автоматика замолкает, и начинаются ошибки вида Cannot autowire service. Разберём приёмы, которые закрывают именно этот хвост. Версии — Symfony 6.4 LTS и 7.x, конфигурация в примерах — YAML.
Базовая настройка, от которой отталкиваемся
По умолчанию в config/services.yaml уже стоят три ключа под _defaults, которые определяют всё поведение. Их полезно понимать, прежде чем что-то переопределять.
# config/services.yaml
services:
_defaults:
autowire: true # внедрять зависимости по тайп-хинтам
autoconfigure: true # сам вешать теги по реализованным интерфейсам
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
Строка App\: регистрирует все классы из src/ как сервисы. Это не значит, что все они попадут в контейнер: Symfony 6.4+ удаляет неиспользуемые сервисы при сборке, так что регистрация ничего не стоит, пока сервис никем не запрошен.
Приём 1. Два класса под одним интерфейсом
Классическая ситуация: интерфейс NotifierInterface, две реализации — EmailNotifier и SmsNotifier. Тайп-хинт по интерфейсу вызовет ошибку: контейнер не знает, какую из двух подставить.
Первый вариант — назначить реализацию по умолчанию через алиас, а вторую внедрять именованно.
# config/services.yaml
services:
# реализация по умолчанию для тайп-хинта NotifierInterface
App\Notifier\NotifierInterface:
alias: App\Notifier\EmailNotifier
# именованный сервис для второго варианта
App\Notifier\SmsNotifier: ~
Теперь любой сервис с тайп-хинтом NotifierInterface получит EmailNotifier. А если конкретному классу нужен именно SMS — используйте атрибут #[Autowire] с указанием сервиса:
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use App\Notifier\NotifierInterface;
use App\Notifier\SmsNotifier;
class OrderConfirmation
{
public function __construct(
// придёт EmailNotifier по алиасу
private NotifierInterface $notifier,
// а сюда явно попросим SMS
#[Autowire(service: SmsNotifier::class)]
private NotifierInterface $urgentNotifier,
) {
}
}
Атрибут #[Autowire] из пространства имён Symfony\Component\DependencyInjection\Attribute доступен начиная с Symfony 6.1 и остаётся рабочим способом в 7.x. Он читается прямо в коде — не нужно прыгать в YAML, чтобы понять, что откуда берётся.
Приём 2. Скалярный параметр в конструкторе
Autowiring умеет подставлять объекты, но не строки и не числа. Если конструктору нужен API-ключ, таймаут или путь — источник придётся указать вручную. Хранить сам ключ в коде не нужно, только ссылку на переменную окружения.
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class PaymentGateway
{
public function __construct(
// значение придёт из переменной окружения PAYMENT_API_URL
#[Autowire(env: 'PAYMENT_API_URL')]
private string $apiUrl,
// а это — из параметра контейнера
#[Autowire(param: 'payment.timeout')]
private int $timeout,
) {
}
}
Параметр payment.timeout при этом объявляется в блоке parameters:
# config/services.yaml
parameters:
payment.timeout: 30
Разница между env и param принципиальная: env читается при каждом запросе и подходит для секретов и того, что меняется между окружениями; param зашивается в скомпилированный контейнер и меняется только при пересборке кеша.
Если конструктор требует одновременно и объекты, и три строковых настройки — это сигнал вынести настройки в отдельный объект-конфиг и внедрять уже его. Пять аргументов в конструкторе почти всегда означают, что у класса больше одной ответственности.
Приём 3. Коллекция сервисов по тегу
Когда реализаций интерфейса не две, а десять, и вам нужны все сразу — например, набор обработчиков экспорта в разные форматы — вручную перечислять их в конструкторе бессмысленно. Symfony собирает их за вас через #[AutowireIterator] или #[TaggedIterator].
Сначала пометьте интерфейс атрибутом #[AutoconfigureTag], чтобы все реализации автоматически получили тег:
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('app.export_handler')]
interface ExportHandlerInterface
{
public function supports(string $format): bool;
public function export(array $rows): string;
}
Теперь любой класс, реализующий этот интерфейс, попадёт в коллекцию. Собираем её в конструкторе:
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
class ExportManager
{
/**
* @param iterable<ExportHandlerInterface> $handlers
*/
public function __construct(
#[AutowireIterator('app.export_handler')]
private iterable $handlers,
) {
}
public function export(string $format, array $rows): string
{
foreach ($this->handlers as $handler) {
if ($handler->supports($format)) {
return $handler->export($rows);
}
}
throw new \InvalidArgumentException("Нет обработчика для формата: $format");
}
}
iterable вместо array здесь не косметика: сервисы в итераторе создаются лениво, по мере обхода. Если из десяти обработчиков сработает первый, остальные девять даже не будут инстанцированы.
Приём 4. Ленивая загрузка тяжёлого сервиса
Иногда сервис дорого создавать — он открывает соединение, читает файл, строит большой объект — а нужен он не при каждом запросе. Внедрять его напрямую значит платить за создание всегда. Помогает #[Autowire(lazy: true)]: контейнер подставит прокси, а реальный объект соберётся при первом обращении к методу.
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class ReportBuilder
{
public function __construct(
// прокси создаётся мгновенно, тяжёлый сервис — при первом вызове метода
#[Autowire(lazy: true)]
private HeavyPdfRenderer $renderer,
) {
}
}
Для генерации ленивых прокси в Symfony 6.4+ достаточно PHP 8.4 либо установленного пакета symfony/var-exporter — отдельный ProxyManager, который требовался в старых версиях, больше не нужен. Точный набор условий для вашей версии сверьте в разделе документации о ленивых сервисах.
Когда что выбирать
| Задача | Приём | Чем платите |
|---|---|---|
| Одна реализация интерфейса | Ничего, autowiring сам | — |
| Две реализации, одна главная | alias + #[Autowire(service:)] | Явная привязка в коде |
| Строка или число в конструкторе | #[Autowire(env:)] / (param:) | Нужно объявить источник |
| Все реализации сразу | #[AutoconfigureTag] + #[AutowireIterator] | Порядок обхода не гарантирован без priority |
| Дорогой редкий сервис | #[Autowire(lazy: true)] | Прокси-обёртка, чуть сложнее отладка |
Что ломается на практике
- Cannot autowire service: argument references interface but no such service exists. Забыли алиас для интерфейса с несколькими реализациями. Добавьте
aliasили укажите конкретный сервис через#[Autowire(service:)]. - Скалярный аргумент внезапно null. Переменная окружения не объявлена в
.env, а#[Autowire(env:)]тихо отдал пустое значение. Проверьтеphp bin/console debug:container --env-vars. - Итератор по тегу пустой. Имя тега в
#[AutoconfigureTag]и в#[AutowireIterator]должно совпадать буква в букву. Проверьте связку командойphp bin/console debug:container --tag=app.export_handler. - Изменил services.yaml — ничего не поменялось. Контейнер компилируется и кешируется. В prod после правки конфигурации нужен
php bin/console cache:clear.
Инвалидация и прогрев контейнера в проде — отдельная тема, здесь мы её не разворачиваем.
Что делать дальше
Освойте php bin/console debug:autowiring — команда показывает, какие интерфейсы и классы контейнер готов внедрить по тайп-хинту, и какой сервис стоит за каждым алиасом. Это первое, куда стоит смотреть при ошибке autowiring, а не гадать по YAML. Дальше по мере роста проекта посмотрите в сторону ServiceSubscriberInterface и service locator — они дают ленивый доступ к набору сервисов без раздувания конструктора. И держите в голове, что каждая явная привязка в YAML или атрибуте — это то, что кто-то будет искать при отладке: чем ближе объявление к коду, тем быстрее его найдут.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: Symfony — Service Container, Symfony — Autowiring
Частые вопросы
Атрибут #[Autowire] или конфигурация в YAML — что предпочесть?
У меня Symfony 5.4, эти атрибуты работают?
Ленивый сервис не создаётся — я что-то настроил не так?
Как задать порядок обработчиков в коллекции по тегу?
Стоит ли регистрировать все классы через App\: resource или объявлять сервисы поштучно?
Почему после правки services.yaml на боевом сервере ничего не изменилось?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.