Осваиваем сервис-контейнер Symfony с помощью современных PHP-атрибутов

Современный Symfony позволяет избавиться от горы YAML-конфигурации и управлять зависимостями прямо в коде с помощью PHP-атрибутов. Это делает контейнер зависимостей более очевидным, читаемым и легким в сопровождении.

Почему это важно

Раньше разработчики Symfony часто терялись между классами, YAML-файлами и тегами: почему сервис не внедряется? Почему не применяется тег? С PHP-атрибутами вся конфигурация сервиса может жить в одном месте прямо в классе. Это снижает когнитивную нагрузку и ускоряет разработку.

Базовая настройка: services.yaml

Прежде всего базовая конфигурация, которую всё равно оставляем в config/services.yaml:

services:
  _defaults:
    autowire: true
    autoconfigure: true

  App\:
    resource: '../src/'
    exclude: '../src/{Entity,Kernel.php}'

Эта настройка включает авто-внедрение зависимостей и автоматическую конфигурацию сервисов. Благодаря этому Symfony сам зарегистрирует все классы в src/ как сервисы, без явного указания в YAML.

Что такое Dependency Injection (DI)

DI - это принцип, при котором объект получает свои зависимости извне, а не создаёт их сам. Symfony-контейнер делает это автоматически:

class CreateProductService
{
    public function __construct(private ProductRepository $repository) {}
}

Если у вас включено автосканирование, Symfony сам найдёт ProductRepository и передаст его в конструктор.

Атрибуты для конфигурации сервиса

Атрибут #[Autowire] - внедрение переменных и сервисов

Вы можете внедрять не только другие сервисы, но и значения из .env или параметров:

use Symfony\Component\DependencyInjection\Attribute\Autowire;

public function __construct(
    private MailerInterface $mailer,
    #[Autowire(env: 'ADMIN_EMAIL')]
    private string $adminEmail,
    #[Autowire(env: 'EMAIL_FROM')]
    private string $fromEmail
) {}

Это заменяет старую необходимость определять параметры в YAML или XML и позволяет держать всё в одном месте.

Методовые зависимости: #[Required]

Если вам нужно внедрить зависимость через сеттер, а не через конструктор, можно пометить метод атрибутом #[Required]:

use Symfony\Contracts\Service\Attribute\Required;

#[Required]
public function setLogger(LoggerInterface $logger): void
{
    $this->logger = $logger;
}

Symfony вызовет этот метод и передаст нужный сервис, если он есть.

Теги сервисов с атрибутами

Symfony позволяет отмечать сервисы тегами с помощью атрибутов. Это полезно для расширяемых систем (например, обработчики, слушатели событий и пр.):

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag('app.vendor')]
interface VendorInterface {}

Теперь любой класс, реализующий этот интерфейс, автоматически получит тег app.vendor без YAML-конфигурации.

Внедрение всех сервисов по тегу

Чтобы получить все сервисы с определённым тегом, используйте аргумент с атрибутом:

use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;

public function __construct(
    #[AutowireIterator('app.vendor')]
    iterable $vendors
) {}

Теперь $vendors это коллекция всех сервисов, помеченных тегом app.vendor.

Декорирование сервисов

Если нужно изменить поведение сервиса, не меняя его код, Symfony поддерживает декораторный паттерн через атрибут #[AsDecorator]:

use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;

#[AsDecorator(decorates: VendorsHandler::class)]
class DiscountVendorHandler
{
    public function __construct(
        #[AutowireDecorated]
        private VendorsHandler $inner
    ) {}
}

Это говорит контейнеру заменить оригинальный сервис VendorsHandler на DiscountVendorHandler, передав оригинал как $inner.

Service Locator через атрибуты

Иногда нужно выбирать сервис динамически (например, в зависимости от ключа или расширения файла). Для таких случаев Symfony предлагает #[AutowireLocator]:

use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;

public function __construct(
    #[AutowireLocator([
        'csv' => CsvImporter::class,
        'json' => JsonImporter::class,
    ])]
    private ContainerInterface $locators
) {}

Это аналогично внедрению локатора, но без ручной конфигурации в YAML.

Что в итоге

Современные PHP-атрибуты позволяют:

  • Уменьшить конфигурацию в YAML.

  • Делать сервисы самодокументируемыми.

  • Избавиться от магии DI контейнера.

  • Писать более ясный, поддерживаемый и типобезопасный код.

Symfony остаётся мощным, но с атрибутами он стал ещё проще и приятнее.

Комментарии (0)

Войдите, чтобы оставить комментарий

Похожие статьи