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