<? phpukraine СПІВБЕСІДИ
Пошук по платформі
SYMFONY · MIDDLE ЧАСТО ПИТАЮТЬ

Як працює dependency injection контейнер у Symfony?

Контейнер компілюється в PHP-клас на етапі cache warmup: autowiring читає типи в конструкторах, compiler passes змінюють визначення сервісів, а результат — статичний код без рефлексії в рантаймі.

Чим контейнер Symfony відрізняється від контейнера Laravel?
Навіщо потрібні compiler passes?
Як зібрати всі сервіси з певним тегом?
DI compiler passes autowiring

Контейнер Symfony живе у двох фазах. Перша — побудова визначень: services.yaml, атрибути на класах, автоматична реєстрація всього з src/, autowiring по типах конструкторів і autoconfigure, який навішує теги за інтерфейсами. Друга — компіляція: всі визначення перетворюються на один згенерований PHP-клас у var/cache, де кожен сервіс створюється звичайним new з уже відомими аргументами. У рантаймі немає ні YAML, ні рефлексії, а помилки залежностей видно ще на cache:warmup.

Між фазами працюють compiler passes. Це код, який бачить усі визначення й може їх змінювати: знайти сервіси з тегом і передати їх у реєстр, підмінити реалізацію, додати декоратор. Теги — головний механізм розширення: бандл оголошує тег, ваш код додає сервіс із цим тегом, і нічого чужого правити не треба. У типових випадках compiler pass не потрібен, достатньо #[AutowireIterator] або !tagged_iterator.

Autowiring розвʼязує залежності по типу й не вгадує. Якщо інтерфейс має дві реалізації, потрібен alias за замовчуванням або явний вибір через #[Target] чи #[Autowire]. Сервіси приватні: їх не дістати через $container->get(), і це навмисно, бо залежності мають бути оголошені в конструкторі, а не витягнуті з контейнера в довільному місці.

// config/services.yaml
// services:
//   _defaults: { autowire: true, autoconfigure: true }
//   App\: { resource: '../src/' }
//   App\Export\Exporter: '@App\Export\CsvExporter'   # alias для інтерфейсу з кількома реалізаціями

interface Exporter { public function supports(string $format): bool; }

#[AutoconfigureTag('app.exporter')]           // кожна реалізація отримує тег автоматично
final class CsvExporter implements Exporter { /* ... */ }
final class XlsxExporter implements Exporter { /* ... */ }

final class ExporterRegistry
{
    /** @param iterable<Exporter> $exporters */
    public function __construct(
        #[AutowireIterator('app.exporter')] private iterable $exporters,
    ) {}

    public function for(string $format): Exporter
    {
        foreach ($this->exporters as $exporter) {
            if ($exporter->supports($format)) {
                return $exporter;
            }
        }
        throw new UnsupportedFormat($format);
    }
}

// Той самий результат через compiler pass, коли потрібна складніша логіка
final class ExporterPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        $refs = array_map(fn ($id) => new Reference($id), array_keys($container->findTaggedServiceIds('app.exporter')));
        $container->getDefinition(ExporterRegistry::class)->setArgument('$exporters', $refs);
    }
}
Що контейнер має дві фази: побудова визначень (services.yaml, атрибути, autoconfigure) і компіляція в один PHP-клас, який у рантаймі просто викликає new.
Що autowiring працює по типу параметра, а для кількох реалізацій одного інтерфейсу потрібен alias, атрибут #[Autowire] або #[Target].
Що compiler pass це хук у момент компіляції: він бачить усі визначення й може їх змінювати, наприклад зібрати теговані сервіси в один реєстр.
Що autoconfigure автоматично навішує теги за інтерфейсом чи атрибутом, тому EventSubscriber або Command реєструються без конфігурації.
Що приватні сервіси не дістати через $container->get(), і це навмисно: залежності оголошуються в конструкторі, а не витягуються з контейнера.
Казати, що контейнер парсить YAML на кожен запит: у prod він скомпільований у var/cache і YAML не читається взагалі.
Плутати autowiring і autoconfigure: перший про підстановку залежностей, другий про автоматичні теги за інтерфейсами.
Не розуміти, що бінарний вибір між двома реалізаціями інтерфейсу autowiring не робить і кидає помилку, поки не додано alias.
Робити сервіси публічними, щоб діставати їх через контейнер у контролері: це ховає залежності й ламає ідею DI.
Реалізовувати логіку збирання плагінів через рефлексію в рантаймі замість тегів і compiler pass чи tagged_iterator.
ПОРАДА

Плюс до відповіді — згадка про compiler passes і теговані сервіси як механізм розширення без правки чужого коду. Наведіть приклад: набір експортерів, які підключаються тегом.

оновлено 3 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Скомпільований контейнер це статичний PHP-код, тому в рантаймі рефлексія не потрібна.