Автоматичне підключення

Автоматичне підключення (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  # ВИКИНЕ ВИНЯТОК, підходять 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.

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

Перевага автоматичного підключення

Якщо у нас є кілька сервісів одного типу і для одного з них ми вказуємо опцію 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().

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

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

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

Ви створите з нього сервіс, додавши до конфігурації:

services:
	- MySettings('any value')

Усі класи потім запитають його за допомогою автоматичного підключення.

Звуження автоматичного підключення

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

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

Покажемо це на прикладі:

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  # автоматичне підключення передасть до конструктора сервіс parent
	childDep: ChildDependent    # автоматичне підключення передасть до конструктора сервіс child

Тепер до конструктора сервісу parentDep передається сервіс parent, оскільки тепер це єдиний відповідний об'єкт. Сервіс 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