Как работает service container (контейнер dependency injection) в Symfony?
Service Container в Symfony автоматически создаёт объекты-сервисы и разрешает их зависимости (autowiring) по типам конструктора; конфигурируется в services.yaml, где задаются параметры, алиасы интерфейсов и теги; все сервисы приватны по умолчанию и создаются в единственном экземпляре.
Service Container (DI-контейнер) в Symfony
Service Container — это объект, который создаёт и хранит сервисы (объекты). Вместо ручного вызова new MyService(new Dependency()) контейнер сам знает, как собрать нужный объект, включая все зависимости. Это реализация паттерна Dependency Injection.
Автоматическая конфигурация (autowiring)
По умолчанию Symfony автоматически определяет зависимости конструктора по их типам и внедряет нужные сервисы:
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
class OrderService
{
// Symfony сам найдёт и передаст EntityManager и Logger
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger,
) {}
public function createOrder(array $items): Order
{
$this->logger->info('Creating order', ['items_count' => count($items)]);
$order = new Order($items);
$this->entityManager->persist($order);
$this->entityManager->flush();
return $order;
}
}
Конфигурация сервисов в services.yaml
# config/services.yaml
services:
# Дефолтная конфигурация для всех сервисов
_defaults:
autowire: true # автоматическое разрешение зависимостей
autoconfigure: true # автоматическое добавление тегов (EventSubscriber, Command и т.д.)
# Регистрация всего из src/ как сервисы
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# Ручная настройка конкретного сервиса
App\Service\PaymentService:
arguments:
$apiKey: '%env(PAYMENT_API_KEY)%'
$timeout: 30
# Алиас интерфейса на конкретную реализацию
App\Repository\UserRepositoryInterface: '@App\Repository\DoctrineUserRepository'
# Именованный сервис для внедрения
mailer.with_logging:
class: App\Service\LoggingMailer
arguments:
$mailer: '@mailer'
$logger: '@monolog.logger.mailer'
Параметры контейнера
# config/services.yaml
parameters:
app.admin_email: 'admin@example.com'
app.upload_dir: '%kernel.project_dir%/public/uploads'
app.supported_locales: ['en', 'ru', 'de']
services:
App\Service\UploadService:
arguments:
$uploadDir: '%app.upload_dir%'
$allowedLocales: '%app.supported_locales%'
<?php
// Или получить параметр прямо в классе через атрибут
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class UploadService
{
public function __construct(
#[Autowire('%app.upload_dir%')]
private readonly string $uploadDir,
#[Autowire(env: 'PAYMENT_API_KEY')]
private readonly string $apiKey,
) {}
}
Публичные и приватные сервисы
По умолчанию все сервисы приватные — их нельзя получить через $container->get(). Публичными делают только те, которые нужны вне контейнера:
services:
App\Service\SpecialService:
public: true # теперь доступен через $container->get()
Отладка контейнера
# Список всех сервисов
php bin/console debug:container
# Найти конкретный сервис
php bin/console debug:container mailer
# Посмотреть, как разрешается тип
php bin/console debug:autowiring MailerInterface
# Список всех параметров
php bin/console debug:container --parameters
Внедрение коллекций сервисов
<?php
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
class NotificationManager
{
/** @param iterable<NotificationChannelInterface> $channels */
public function __construct(
#[TaggedIterator('app.notification_channel')]
private readonly iterable $channels,
) {}
public function notify(string $message): void
{
foreach ($this->channels as $channel) {
$channel->send($message);
}
}
}
Подводные камни
- Сервисы создаются лениво и только один раз (singleton by default) — не храните изменяемое состояние в сервисах, если к ним обращаются несколько раз за запрос.
- При использовании
autoconfigure: trueклассы, реализующие EventSubscriberInterface, автоматически получают тег kernel.event_subscriber — не добавляйте тег вручную, иначе листенер зарегистрируется дважды. - Приватные сервисы нельзя передавать через ServiceLocator или получать из тестового контейнера без
public: true— в тестах это часто становится сюрпризом. - Опечатка в имени параметра (одинарные vs двойные знаки процента) приведёт к
%env(FOO)%трактовке как строки, а не переменной окружения. - Circular dependency между сервисами вызывает исключение при компиляции — разрывайте цикл через ServiceLocator или ленивую инъекцию.
- Контейнер компилируется и кэшируется; изменения в services.yaml без
cache:clearв production не применятся.
Common mistakes
- Сводить service container к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Symfony 7/8 строит request lifecycle вокруг HttpKernel, events, routing, controller resolver и response listeners.
- Не отделять validation, authorization, transaction boundary и business logic.
What the interviewer is testing
- Объясняет service container через конкретную точку lifecycle в Symfony.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.