HardТеория3 min

Подвохи DI Container

Autowiring edge cases, abstract services, lazy services, кеширование контейнера, synthetic services

Подвохи Dependency Injection

Подвох 1: Private services и доступ из контроллера

По умолчанию все сервисы в Symfony -- private. Это значит, что вы не можете получить их через $container->get(). Однако контроллеры -- исключение.

<?php

declare(strict_types=1);

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

final class DemoController extends AbstractController
{
    public function index(): Response
    {
        // AbstractController имеет доступ к service locator
        // Но ТОЛЬКО к определённым сервисам!
        // Это НЕ полный контейнер, а ограниченный ServiceLocator

        // Работает -- контроллер имеет доступ к router, twig и т.д.
        $router = $this->container->get('router');

        // НЕ работает -- App\Service\MyService нет в service locator контроллера!
        // $this->container->get(MyService::class); // ServiceNotFoundException!

        return new Response('ok');
    }
}

На экзамене: Контроллер наследующий AbstractController имеет доступ НЕ ко всему контейнеру, а к ограниченному ServiceLocator. Единственный правильный способ получить произвольный сервис -- constructor injection или method injection через #[Autowire].

Подвох 2: Autowiring и интерфейсы

Autowiring работает по type-hint. Но если один интерфейс реализован несколькими сервисами -- autowiring ломается.

<?php

declare(strict_types=1);

namespace App\Service;

interface NotificationSenderInterface
{
    public function send(string $message): void;
}

final readonly class EmailSender implements NotificationSenderInterface
{
    public function send(string $message): void { /* ... */ }
}

final readonly class SmsSender implements NotificationSenderInterface
{
    public function send(string $message): void { /* ... */ }
}

// ОШИБКА: Symfony не знает какой из двух сервисов подставить!
// Cannot autowire service: argument "$sender" of method "__construct()"
// references interface "NotificationSenderInterface" but no such
// service exists. You should maybe alias this interface to one
// of these existing services: "EmailSender", "SmsSender".

Решение -- alias или #[AutowireLocator]:

# services.yaml
services:
    App\Service\NotificationSenderInterface: '@App\Service\EmailSender'
<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Psr\Container\ContainerInterface;

final readonly class NotificationService
{
    public function __construct(
        #[AutowireLocator([
            'email' => EmailSender::class,
            'sms' => SmsSender::class,
        ])]
        private ContainerInterface $senders,
    ) {
    }

    public function notify(string $channel, string $message): void
    {
        $this->senders->get($channel)->send($message);
    }
}

Подвох 3: Lazy Services -- когда и как

Lazy services создаются только при первом вызове метода. Но есть подвох -- конструктор НЕ вызывается при инъекции!

<?php

declare(strict_types=1);

namespace App\Service;

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

#[Lazy]
final class HeavyService
{
    public function __construct()
    {
        // Этот код НЕ выполнится при инъекции сервиса!
        // Он выполнится только при первом вызове ЛЮБОГО метода.
        echo "Heavy initialization..."; // вызовется лениво
    }

    public function process(): string
    {
        return 'done';
    }
}

final readonly class ConsumerService
{
    public function __construct(
        private HeavyService $heavy, // Proxy объект, НЕ реальный HeavyService
    ) {
        // $this->heavy -- это ghost proxy, конструктор HeavyService ещё НЕ вызван!
    }

    public function doWork(): string
    {
        // Вот СЕЙЧАС вызовется конструктор HeavyService
        return $this->heavy->process();
    }
}

На экзамене: Lazy proxy использует ghost-объекты (symfony/var-exporter). Конструктор lazy-сервиса вызывается при первом обращении к любому методу или свойству, а НЕ при инъекции.

Подвох 4: Компиляция контейнера и кеширование

Контейнер компилируется в PHP-класс и кеширует. Но есть нюансы.

<?php

declare(strict_types=1);

// Что кешируется при компиляции контейнера:
// 1. Дерево сервисов (какой сервис от чего зависит)
// 2. Параметры контейнера (parameters в services.yaml)
// 3. Теги сервисов (уже обработанные compiler passes)
// 4. Алиасы и привязки

// Что НЕ кешируется:
// 1. Содержимое .env файлов (читается каждый раз)
// 2. Runtime параметры
// 3. Синтетические сервисы (устанавливаются в runtime)
# services.yaml
parameters:
    # Этот параметр скомпилируется в контейнер
    app.static_param: 'cached_value'

    # Этот параметр читается из ENV при каждом запросе
    app.dynamic_param: '%env(DATABASE_URL)%'

На экзамене: %env(...)% обрабатывается через EnvVarProcessor в runtime, а НЕ при компиляции. Если изменить .env -- значение обновится без очистки кеша. Но обычные %parameter% -- фиксируются при компиляции.

Подвох 5: Synthetic Services

Синтетические сервисы не создаются контейнером -- они устанавливаются извне.

# services.yaml
services:
    app.synthetic_service:
        synthetic: true
<?php

declare(strict_types=1);

// Синтетический сервис устанавливается вручную:
$container->set('app.synthetic_service', $someObject);

// Пример из Symfony: 'kernel' -- синтетический сервис!
// Он создаётся до контейнера и устанавливается в него.

На экзамене: Synthetic services нельзя автоматически создать через DI. Они используются, когда объект создаётся вне контейнера (например, сам Kernel).

Подвох 6: Autoconfigure vs Ручные теги

Autoconfigure автоматически добавляет теги к сервисам, реализующим определённые интерфейсы. Но ручной тег ПЕРЕЗАПИСЫВАЕТ autoconfigure!

services:
    _defaults:
        autoconfigure: true

    # Autoconfigure добавит тег kernel.event_listener автоматически
    # если класс использует #[AsEventListener]

    App\EventListener\MyListener:
        tags:
            # Ручной тег ЗАМЕНЯЕТ автоматический!
            - { name: kernel.event_listener, event: kernel.request, priority: 100 }
<?php

declare(strict_types=1);

namespace App\EventListener;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;

// Если в services.yaml есть ручной тег -- этот атрибут ИГНОРИРУЕТСЯ!
#[AsEventListener(event: RequestEvent::class, priority: 10)]
final readonly class MyListener
{
    public function __invoke(RequestEvent $event): void
    {
        // ...
    }
}

На экзамене: Если сервис имеет вручную прописанный тег того же типа, autoconfigure НЕ добавит свой тег. Ручная конфигурация имеет приоритет.

Подвох 7: Abstract Services и наследование определений

Abstract services не могут быть инстанцированы, но служат шаблоном для других.

services:
    app.abstract_mailer:
        abstract: true
        arguments:
            $transport: 'smtp'
            $from: '[email protected]'

    App\Service\UserMailer:
        parent: app.abstract_mailer
        # Наследует $transport и $from от parent
        # Но НЕ наследует autowire и autoconfigure!
        autowire: true
        arguments:
            $userRepo: '@App\Repository\UserRepository'

На экзамене: Сервисы с parent НЕ наследуют autowire, autoconfigure, public от _defaults. Они наследуют только от своего parent-сервиса. Нужно задавать явно.

Подвох 8: #[Autowire] для скалярных параметров

<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

final readonly class PaymentGateway
{
    public function __construct(
        // Инъекция параметра контейнера
        #[Autowire('%env(STRIPE_API_KEY)%')]
        private string $apiKey,

        // Инъекция выражения
        #[Autowire(expression: 'service("router").generate("homepage")')]
        private string $homepageUrl,

        // Инъекция конкретного сервиса (когда несколько реализаций)
        #[Autowire(service: 'app.mailer.transactional')]
        private MailerInterface $mailer,
    ) {
    }
}

На экзамене: #[Autowire] может инжектировать параметры (%param%), env-переменные (%env(...)%), выражения (expression:) и конкретные сервисы (service:). Не путайте синтаксис.

Проверь себя

5 из 8

Какой атрибут позволяет инжектировать конкретный сервис, когда интерфейс имеет несколько реализаций?

Вы изменили значение переменной в .env файле. Нужно ли очищать кеш контейнера?

Что такое синтетический сервис (synthetic: true) в Symfony DI?

Контроллер наследует AbstractController. Можно ли получить произвольный private сервис через $this->container->get()?

Сервис помечен атрибутом #[Lazy]. Когда будет вызван его конструктор?