Как работает service container (контейнер dependency injection) в Symfony?
Сначала попробуйте ответить сами30 секунд
Service Container в Symfony автоматически создаёт объекты-сервисы и разрешает их зависимости (autowiring) по типам конструктора; конфигурируется в services.yaml, где задаются параметры, алиасы интерфейсов и теги; все сервисы приватны по умолчанию и создаются в единственном экземпляре.
Автоматическая конфигурация (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;
}
}
Service Container (DI-контейнер) в Symfony
Service Container — это объект, который создаёт и хранит сервисы (объекты). Вместо ручного вызова new MyService(new Dependency()) контейнер сам знает, как собрать нужный объект, включая все зависимости. Это реализация паттерна Dependency Injection.
Конфигурация сервисов в 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 не применятся.
Частые вопросы на собеседовании
Типичные ошибки
- Сводить service container к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Symfony 7/8 строит request lifecycle вокруг HttpKernel, events, routing, controller resolver и response listeners.
- Не отделять validation, authorization, transaction boundary и business logic.
Что проверяет интервьюер
- Объясняет service container через конкретную точку lifecycle в Symfony.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.