Автозв'язування

Autowiring, або автозв'язування – це чудова функція, яка може автоматично передавати сервіси до конструктора та інших методів, так що нам зовсім не потрібно їх писати. Це заощадить вам багато часу.

Це дозволяє нам пропустити переважну більшість аргументів під час написання визначень сервісів. Замість:

services:
	articles: Model\ArticleRepository(@database, @cache.storage)

Просто напишіть:

services:
	articles: Model\ArticleRepository

Автозв'язування керується типами, тому клас ArticleRepository має бути визначений таким чином:

namespace Model;

class ArticleRepository
{
	public function __construct(\PDO $db, \Nette\Caching\Storage $storage)
	{}
}

Щоб використовувати автозв'язування, у контейнері має бути тільки один сервіс для кожного типу. Якби їх було більше, автозв'язування не знало б, який із них передавати, і викидало б виняток:

services:
	mainDb: PDO(%dsn%, %user%, %password%)
	tempDb: PDO('sqlite::memory:')
	articles: Model\ArticleRepository # EXCEPT, mainDb та tempDb співпадають

Рішенням може бути або обхід автопідключення, або явна вказівка імені сервісу (тобто articles: Model\ArticleRepository(@mainDb)). Однак зручніше відключити автозв'язування одного сервісу, або віддати перевагу конкретному сервісу.

Вимкнене автозв'язування

Ви можете вимкнути автоматичне визначення залежностей сервісів за допомогою параметра autowired: no:

services:
	mainDb: PDO(%dsn%, %user%, %password%)

	tempDb:
		create: PDO('sqlite::memory:')
		autowired: false # видаляє tempDb з автозв'язування

	articles: Model\ArticleRepository # передає mainDb у конструктор

Сервіс articles не викидає виняток про те, що є два відповідні сервіси типу PDO (тобто mainDb і tempDb), які можуть бути передані конструктору, оскільки він бачить тільки сервіс mainDb.

Налаштування autowiring у Nette працює інакше, ніж у Symfony, де опція autowire: false говорить, що autowiring не повинен використовуватися для аргументів конструктора сервісу. У Nette autowiring використовується завжди, будь то аргументи конструктора або будь-якого іншого методу. Опція autowired: false говорить, що екземпляр сервісу не повинен передаватися нікуди з використанням autowiring.

Переважне автозв'язування

Якщо у нас є кілька сервісів одного типу і один з них має опцію autowired, цей сервіс стає кращим:

services:
	mainDb:
		create: PDO(%dsn%, %user%, %password%)
		autowired: PDO # робить його кращим

	tempDb:
		create: PDO('sqlite::memory:')

	articles: Model\ArticleRepository

Сервіс articles не викидає виняток, якщо є два сервіси, що збігаються, PDO (тобто mainDb і tempDb), але використовує кращий сервіс, тобто mainDb.

Колекція сервісів

Автозв'язування також може передавати масив сервісів певного типу. Оскільки PHP не може нативно позначати тип елементів масиву, на додаток до типу array необхідно додати коментар phpDoc з типом елемента, наприклад ClassName[]:

namespace Model;

class ShipManager
{
	/**
	 * @param Shipper[] $shippers
	 */
	public function __construct(array $shippers)
	{}
}

Потім контейнер DI автоматично передає масив сервісів, що відповідають заданому типу. При цьому будуть пропущені сервіси, у яких відключено автозв'язування.

Тип у коментарі також може мати вигляд array<int, Class> або list<Class>. Якщо ви не можете контролювати форму коментаря phpDoc, ви можете передати масив сервісів безпосередньо в конфігурації за допомогою typed().

Скалярні аргументи

Autowiring може передавати тільки об'єкти та масиви об'єктів. Скалярні аргументи (наприклад, рядки, числа, булеви) записуються в конфігурації. Альтернативою може бути створення settings-object, який інкапсулює скалярне значення (або кілька значень) як об'єкт, який потім може бути переданий знову за допомогою autowiring.

class MySettings
{
	public function __construct(
		// readonly можна використовувати починаючи з PHP 8.1
		public readonly bool $value,
	)
	{}
}

Ви створюєте сервіс, додаючи його в конфігурацію:

services:
	- MySettings('любое значение')

Потім усі класи будуть запитувати його через autowiring.

Звуження автозв'язування

Для окремих сервісів автопідключення може бути звужене до певних класів або інтерфейсів.

Зазвичай автозв'язування передає функцію кожному параметру методу, типу якого відповідає функція. Звуження означає, що ми вказуємо умови, яким мають задовольняти типи, зазначені для параметрів методу, щоб їм було передано функцію.

Розглянемо приклад:

class ParentClass
{}

class ChildClass extends ParentClass
{}

class ParentDependent
{
	function __construct(ParentClass $obj)
	{}
}

class ChildDependent
{
	function __construct(ChildClass $obj)
	{}
}

Якби ми зареєстрували їх усі як сервіси, автозв'язування було б неможливим:

services:
	parent: ParentClass
	child: ChildClass
	parentDep: ParentDependent # ВИБОРУЄ ВИНЯТОК, parent і child збігаються
	childDep: ChildDependent   # передає сервіс 'child' конструктору

Сервіс parentDep викидає виняток Multiple services of type ParentClass found: parent, child тому що і parent, і child поміщаються в його конструктор, і автозв'язування не може ухвалити рішення про те, який із них вибрати.

Тому для сервісу child ми можемо звузити його автозв'язування до ChildClass:

services:
	parent: ParentClass
	child:
		create: ChildClass
		autowired: ChildClass  # альтернатива: 'autowired: self'

	parentDep: ParentDependent # ВИБИРАЄ ВИНЯТОК, 'child' не може бути автопідключуваним
	childDep: ChildDependent   # передає сервіс 'child' конструктору

Сервіс parentDep тепер передається в конструктор сервісу parentDep, оскільки тепер це єдиний відповідний об'єкт. Сервіс child більше не передається через автозв'язування. Так, функція child, як і раніше, має тип ParentClass, але умова звуження, задана для типу параметра, більше не застосовується, тобто більше не вірно, що ParentClass є супертипом ChildClass.

У випадку child, autowired: ChildClass можна записати як autowired: self, оскільки self означає поточний тип сервісу.

Ключ autowired може включати кілька класів та інтерфейсів як масив:

autowired: [BarClass, FooInterface]

Давайте спробуємо додати інтерфейси в приклад:

interface FooInterface
{}

interface BarInterface
{}

class ParentClass implements FooInterface
{}

class ChildClass extends ParentClass implements BarInterface
{}

class FooDependent
{
	function __construct(FooInterface $obj)
	{}
}

class BarDependent
{
	function __construct(BarInterface $obj)
	{}
}

class ParentDependent
{
	function __construct(ParentClass $obj)
	{}
}

class ChildDependent
{
	function __construct(ChildClass $obj)
	{}
}

Якщо ми не обмежуємо сервіс child, він відповідатиме конструкторам усіх класів FooDependent, BarDependent, ParentDependent і ChildDependent, а автозв'язування передасть його туди.

Однак, якщо ми звузимо автозв'язування до ChildClass за допомогою autowired: ChildClass (або self), автозв'язування передає його тільки конструктору ChildDependent, оскільки для нього потрібен аргумент типу ChildClass і ChildClass це тип ChildClass. Жоден інший тип, вказаний для інших параметрів, не є заміною ChildClass, тому сервіс не проходить.

Якщо ми обмежуємо його на ParentClass за допомогою autowired: ParentClass, то автозв'язування знову передасть його конструктору ChildDependent (оскільки потрібний тип ChildClass є надмножиною ParentClass) і конструктору ParentDependent, оскільки необхідний тип ParentClass також відповідає.

Якщо ми обмежуємо його на FooInterface, то він все одно буде підключатися для ParentDependent (необхідний тип ParentClass є супертипом FooInterface) і ChildDependent, але додатково до конструктора FooDependent, але не BarDependent, тому що BarInterface не супертип FooInterface.

services:
	child:
		create: ChildClass
		autowired: FooInterface

	fooDep: FooDependent       # передає сервіс child конструктору
	barDep: BarDependent       # ВИБИРАЄ ВИНЯТОК, жоден сервіс не пройде
	parentDep: ParentDependent # передає сервіс child конструктору
	childDep: ChildDependent   # передає сервіс child конструктору
версію: 3.x