MidТеория4 min

Service Container

DependencyInjection component, концепции контейнера, встроенные сервисы, конфигурация параметров

Компонент DependencyInjection

Service Container -- ядро Symfony. Каждый объект в приложении (контроллер, репозиторий, сервис) управляется контейнером. Контейнер отвечает за создание объектов и внедрение их зависимостей.

Symfony 8.0: Компонент DependencyInjection теперь явно указан в списке тем экзамена. Убедитесь, что понимаете не только конфигурацию, но и сам компонент как отдельную библиотеку.

<?php

declare(strict_types=1);

// Standalone usage of DependencyInjection component
use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();

// Register a service manually
$container->register('mailer', Mailer::class)
    ->addArgument('smtp://localhost');

$container->compile();

$mailer = $container->get('mailer');

Основные концепции

Сервис

Сервис -- любой PHP-объект, который выполняет определённую задачу. В Symfony практически всё является сервисом: логгер, mailer, entity manager.

<?php

declare(strict_types=1);

namespace App\Service;

final class InvoiceGenerator
{
    public function __construct(
        private readonly PdfRenderer $renderer,
        private readonly MailerInterface $mailer,
    ) {
    }

    public function generateAndSend(Order $order): void
    {
        $pdf = $this->renderer->render($order);
        $this->mailer->send($pdf, $order->getCustomerEmail());
    }
}

Service Container (Контейнер сервисов)

Контейнер -- специальный объект, который:

  1. Создаёт сервисы (инстанцирование)
  2. Внедряет зависимости (injection)
  3. Управляет жизненным циклом (shared/non-shared)
  4. Компилирует конфигурацию для производительности

Жизненный цикл контейнера

Этап Описание
Загрузка конфигурации Чтение YAML/PHP-файлов, атрибутов
Компиляция Оптимизация, разрешение зависимостей
Дамп Генерация PHP-класса контейнера
Использование Получение сервисов из скомпилированного контейнера

Регистрация сервисов

YAML-конфигурация

# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true

    # Автоматическая регистрация всех классов из директории
    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    # Явная регистрация с аргументами
    App\Service\InvoiceGenerator:
        arguments:
            $adminEmail: '%app.admin_email%'

PHP-атрибуты для регистрации

Symfony 8.0: PHP-атрибуты стали основным способом конфигурации сервисов. XML и аннотации удалены из экзамена. Остались только YAML и PHP-атрибуты.

<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class NewsletterManager
{
    public function __construct(
        private readonly MailerInterface $mailer,
        #[Autowire('%app.newsletter_sender%')]
        private readonly string $senderEmail,
        #[Autowire('%kernel.debug%')]
        private readonly bool $debug,
    ) {
    }
}

Встроенные сервисы и параметры

Ключевые встроенные сервисы

Сервис ID Описание
Logger logger PSR-3 LoggerInterface
EventDispatcher event_dispatcher Диспетчер событий
Router router Маршрутизация
Request Stack request_stack Текущий HTTP-запрос
Security security.helper Проверка авторизации

Встроенные параметры

# Доступны автоматически
kernel.project_dir: '/path/to/project'
kernel.environment: 'dev'     # dev, test, prod
kernel.debug: true
kernel.cache_dir: '/path/to/var/cache/dev'
kernel.logs_dir: '/path/to/var/log'
kernel.bundles: { ... }
kernel.charset: 'UTF-8'
kernel.default_locale: 'en'
<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class FileUploader
{
    public function __construct(
        #[Autowire('%kernel.project_dir%/public/uploads')]
        private readonly string $uploadDir,
        #[Autowire('%kernel.environment%')]
        private readonly string $environment,
    ) {
    }

    public function upload(UploadedFile $file): string
    {
        $filename = uniqid() . '.' . $file->guessExtension();
        $file->move($this->uploadDir, $filename);

        return $filename;
    }
}

Конфигурация параметров

Определение параметров

# config/services.yaml
parameters:
    app.admin_email: '[email protected]'
    app.items_per_page: 20
    app.supported_locales: ['en', 'fr', 'de']

    # Environment variable binding
    app.database_url: '%env(DATABASE_URL)%'
    app.redis_port: '%env(int:REDIS_PORT)%'
    app.feature_flags: '%env(json:FEATURE_FLAGS)%'

Процессоры переменных окружения

Процессор Пример Описание
string %env(string:VAR)% По умолчанию, строка
bool %env(bool:VAR)% Приведение к boolean
int %env(int:VAR)% Приведение к integer
float %env(float:VAR)% Приведение к float
json %env(json:VAR)% JSON-декодирование
csv %env(csv:VAR)% CSV в массив
file %env(file:VAR)% Чтение файла
base64 %env(base64:VAR)% Base64-декодирование
resolve %env(resolve:VAR)% Разрешение вложенных параметров
<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class PaymentGateway
{
    public function __construct(
        // Environment variable processors
        #[Autowire('%env(PAYMENT_API_KEY)%')]
        private readonly string $apiKey,
        #[Autowire('%env(bool:PAYMENT_SANDBOX)%')]
        private readonly bool $sandbox,
        #[Autowire('%env(int:PAYMENT_TIMEOUT)%')]
        private readonly int $timeout,
    ) {
    }
}

Shared и Non-shared сервисы

По умолчанию все сервисы shared (singleton) -- контейнер возвращает один и тот же экземпляр при каждом запросе.

services:
    # Shared (default) -- один экземпляр
    App\Service\Logger: ~

    # Non-shared -- новый экземпляр при каждом запросе
    App\Service\FormBuilder:
        shared: false
<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

// Non-shared through attribute
#[\Symfony\Component\DependencyInjection\Attribute\Autoconfigure(shared: false)]
final class FormBuilder
{
    private array $fields = [];

    public function addField(string $name, string $type): self
    {
        $this->fields[$name] = $type;
        return $this;
    }
}

Подвох экзамена: По умолчанию сервисы являются shared (singleton). Если вопрос спрашивает, сколько раз создаётся сервис при нескольких вызовах get() -- ответ 1 (один раз). Для non-shared сервисов -- каждый раз новый экземпляр.

Public и Private сервисы

services:
    _defaults:
        autowire: true
        autoconfigure: true
        public: false   # По умолчанию private

    # Явно public -- можно получить через $container->get()
    App\Service\PaymentProcessor:
        public: true

Подвох экзамена: С Symfony 4+ все сервисы по умолчанию private. Их нельзя получить через $container->get() напрямую. Они доступны только через autowiring. Контроллеры -- исключение, так как они регистрируются как public через AbstractController.

Alias сервисов

services:
    # Alias -- когда интерфейс реализуется одним классом
    App\Contract\PaymentInterface: '@App\Service\StripePayment'

    # Autowiring alias
    App\Service\StripePayment: ~
    Psr\Log\LoggerInterface $paymentLogger: '@monolog.logger.payment'
<?php

declare(strict_types=1);

namespace App\Controller;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class PaymentController
{
    public function __construct(
        // Named autowiring -- will inject monolog.logger.payment
        #[Autowire(service: 'monolog.logger.payment')]
        private readonly LoggerInterface $paymentLogger,
    ) {
    }
}

Итоги

  • Service Container управляет созданием и зависимостями всех объектов
  • Сервисы по умолчанию shared (singleton) и private
  • Конфигурация через YAML и PHP-атрибуты (Sf8 -- без XML/аннотаций)
  • Параметры поддерживают переменные окружения с процессорами
  • #[Autowire] позволяет точно указать, что внедрять