Контейнер 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);
}
}
Плюс до відповіді — згадка про compiler passes і теговані сервіси як механізм розширення без правки чужого коду. Наведіть приклад: набір експортерів, які підключаються тегом.