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

Як працюють autowiring і autoconfigure у Symfony і коли їх недостатньо?

Autowiring підставляє залежності за типом параметра конструктора, autoconfigure навішує теги за реалізованим інтерфейсом чи атрибутом; обидва мовчать там, де тип нічого не каже: скаляри й env, класи з vendor, дві реалізації одного інтерфейсу та теги з атрибутами вимагають явного запису в services.yaml або атрибута #[Autowire].

Що роблять autowire: true і autoconfigure: true у _defaults і чим вони відрізняються?
Створив клас у src/, тайп-хінчу його в контролері, а Symfony каже «Cannot autowire service». Чому?
Чому інтерфейс інжектиться сам, поки реалізація одна, і ламається, щойно зʼявляється друга?
Куди прописати клас із vendor, якому треба передати ключ API?
autowiring autoconfigure services.yaml alias теги

Autowiring робить рівно одне: дивиться на тип кожного параметра конструктора і шукає сервіс, id якого збігається з цим типом. Тому private OrderRepository $orders працює без конфігурації, адже PSR-4 реєстрація з App\: resource: '../src/' створила сервіс з id App\Repository\OrderRepository. З інтерфейсами трохи цікавіше: завантажувач конфігурації після реєстрації теки перевіряє, чи є серед знайдених класів рівно один, що реалізує інтерфейс, і в такому разі сам створює alias. Через це PaymentGateway інжектиться, поки реалізація одна, і перестає, щойно поряд зʼявляється друга.

Autoconfigure стосується тегів. Бандли під час завантаження оголошують правила через registerForAutoconfiguration(): усе, що реалізує EventSubscriberInterface, отримує kernel.event_subscriber, нащадки Command отримують console.command, реалізації VoterInterface отримують security.voter. Паралельно працюють атрибути (#[AsEventListener], #[AsMessageHandler], #[AutoconfigureTag]), які обробляє окремий compiler pass. Локальний аналог для власних інтерфейсів це секція _instanceof у services.yaml. Деталь, на якій спотикаються: правило спрацьовує лише для визначень з увімкненим autoconfigure, тобто для вашого src/, а сервіси, вже зареєстровані бандлами, воно не чіпає.

Далі починається територія, де контейнеру нічого вивести з типів. Скалярний аргумент (string $apiKey, int $timeout) не має сервісу-кандидата, тому потрібен #[Autowire('%env(API_KEY)%')] на параметрі або bind у _defaults. Клас з vendor не потрапляє під resource, тому S3-клієнт, Redis-конекція чи будь-який SDK описуються окремим визначенням з arguments. Два екземпляри того самого класу з різною конфігурацією вимагають двох id і вибору через #[Autowire(service: ...)] або named alias з #[Target]. Фабрики, декоратори, lazy: true, теги з атрибутами на кшталт priority теж лишаються ручною роботою.

Розподіл між YAML і атрибутами найкраще тримати за принципом «чиє це знання». Пріоритет слухача або назва події описують поведінку самого класу, тому їм місце в атрибуті поруч з кодом. Регіон S3-бакета, таймаути й вибір реалізації для конкретного середовища це конфігурація інфраструктури, і в config/services.yaml вона і має жити, бо там її легко перевизначити в services_test.yaml або в when@dev. Ентіті, DTO і value objects взагалі не сервіси: тримайте Entity в exclude, інакше Doctrine-сутність стане сервісом, аргументи якого контейнер не зможе розвʼязати.

Помилки такого роду видно не в рантаймі, а на компіляції контейнера, тобто на cache:warmup. У проді контейнер це згенерований PHP-клас у var/cache, тому зламана залежність зупиняє деплой, а не падає посеред робочого дня. У CI поруч з тестами варто тримати php bin/console lint:container, а під час розробки звикнути до debug:autowiring з фрагментом імені: команда показує список типів, які реально можна тайп-хінтити, і який id за ними стоїть. Коли autowiring раптом перестав вгадувати, ця команда відповідає швидше, ніж читання стектрейсу.

// config/services.yaml (частина з рецепту symfony/framework-bundle)
// services:
//   _defaults:
//     autowire: true          # аргументи конструктора за типом
//     autoconfigure: true     # теги за інтерфейсом та атрибутами
//     bind:
//       string $uploadsDir: '%kernel.project_dir%/var/uploads'
//   App\:
//     resource: '../src/'
//     exclude: '../src/{Entity,Kernel.php}'
//   # клас із vendor: у src/ його немає, тому опис явний
//   Aws\S3\S3Client:
//     arguments: [{ region: '%env(AWS_REGION)%', version: 'latest' }]

interface PaymentGateway
{
    public function charge(Money $amount): PaymentId;
}

// Дві реалізації в src/ прибирають автоматичний alias, тому вибір робимо самі
#[AsAlias(PaymentGateway::class)]
final class StripeGateway implements PaymentGateway { /* ... */ }

final class SandboxGateway implements PaymentGateway { /* ... */ }

// Тег kernel.event_listener autoconfigure навісив би й сам,
// але подію та priority доводиться вказати руками
#[AsEventListener(event: KernelEvents::REQUEST, priority: 64)]
final class LocaleListener
{
    public function __invoke(RequestEvent $event): void { /* ... */ }
}

final readonly class CheckoutHandler
{
    public function __construct(
        private PaymentGateway $gateway,                              // через alias
        #[Target('paymentLogger')] private LoggerInterface $logger,   // канал monolog
        #[Autowire('%env(int:PAYMENT_TIMEOUT)%')] private int $timeout, // скаляр
        #[Autowire(service: SandboxGateway::class)] private PaymentGateway $fallback,
        private string $uploadsDir,                                   // прийшов з bind
    ) {}
}
Що autowiring читає типи параметрів конструктора і шукає сервіс з таким id, тому працює лише для класів і інтерфейсів, а не для string, int чи array.
Що PSR-4 реєстрація (App\ з resource: '../src/') сама створює alias інтерфейсу на реалізацію, поки реалізація рівно одна; друга реалізація прибирає alias і дає помилку неоднозначності.
Що autoconfigure це registerForAutoconfiguration і атрибути: EventSubscriberInterface отримує kernel.event_subscriber, Command - console.command, VoterInterface - security.voter, і саме тому підписник працює без жодного рядка конфігурації.
Що тег з атрибутами (priority, event, channel) autoconfigure не вигадає: потрібен #[AsEventListener] з параметрами або tags у YAML.
Що явний сервіс потрібен для vendor-класів, для другого екземпляра того самого класу з іншою конфігурацією, для factory і для decorator.
Плутати два прапорці: autowire відповідає за аргументи конструктора, autoconfigure - за теги, і вимкнення одного не змінює поведінку іншого.
Очікувати, що Symfony сама обере реалізацію інтерфейсу, коли їх дві, замість alias, #[AsAlias] або #[Target].
Передавати рядок з конфігурації через сеттер чи через $_ENV у конструкторі замість #[Autowire('%env(...)%')] або bind.
Додавати клас з vendor у autoconfigure-секцію App\ і дивуватися, що його не видно: resource сканує лише src/, вендорські класи описуються окремим визначенням.
Робити сервіси public: true, щоб дістати їх у тесті, хоча static::getContainer() у тестовому середовищі і так віддає приватні сервіси, які не були видалені компілятором.
Тримати Entity і DTO серед сервісів: їх треба лишити в exclude, інакше Doctrine-сутність стає сервісом з незрозумілими аргументами.
ПОРАДА

Замість «воно якось саме працює» покажіть інструмент: `php bin/console debug:autowiring PaymentGateway` показує, який саме id підставиться під тайп-хінт, а `debug:container --tag=kernel.event_subscriber` - що дав autoconfigure. Той, хто називає ці дві команди, одразу виглядає як людина, яка розбирала реальні помилки контейнера, а не читала документацію.

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

Autowiring оперує типами, тому string і int для нього порожній звук; теги приходять з registerForAutoconfiguration та атрибутів, а вибір між двома реалізаціями завжди робить людина.