Autowiring

Autowiring е страхотна функция, която може автоматично да предава необходимите сървиси към конструктора и други методи, така че изобщо не е необходимо да ги пишем. Ще ви спести много време.

Благодарение на това можем да пропуснем по-голямата част от аргументите при писане на дефиниции на сървиси. Вместо:

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

Достатъчно е да напишете:

services:
	articles: Model\ArticleRepository

Autowiring се ръководи от типовете, така че за да работи, класът ArticleRepository трябва да бъде дефиниран приблизително така:

namespace Model;

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

За да може да се използва autowiring, за всеки тип трябва да има точно един сървис в контейнера. Ако има повече, autowiring няма да знае кой от тях да предаде и ще хвърли изключение:

services:
	mainDb: PDO(%dsn%, %user%, %password%)
	tempDb: PDO('sqlite::memory:')
	articles: Model\ArticleRepository  # ХВЪРЛЯ ИЗКЛЮЧЕНИЕ, отговарят и mainDb, и tempDb

Решението би било или да се заобиколи autowiring и изрично да се посочи името на сървиса (т.е. articles: Model\ArticleRepository(@mainDb)). По-удобно обаче е autowiring-ът на един от сървисите да се изключи или първият сървис да се предпочете.

Изключване на autowiring

Можем да изключим autowiring-а на сървис с помощта на опцията autowired: no:

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

	tempDb:
		create: PDO('sqlite::memory:')
		autowired: false               # сървисът tempDb е изключен от autowiring

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

Сървисът articles няма да хвърли изключение, че съществуват два подходящи сървиса от тип PDO (т.е. mainDb и tempDb), които могат да бъдат предадени на конструктора, защото вижда само сървиса mainDb.

Конфигурацията на autowiring в Nette работи различно от тази в Symfony, където опцията autowire: false указва, че autowiring не трябва да се използва за аргументите на конструктора на дадения сървис. В Nette autowiring се използва винаги, независимо дали за аргументите на конструктора, или за които и да било други методи. Опцията autowired: false указва, че инстанцията на дадения сървис не трябва да бъде предавана никъде чрез autowiring.

Предпочитание за autowiring

Ако имаме няколко сървиса от един и същи тип и за един от тях посочим опцията autowired, този сървис става предпочитан:

services:
	mainDb:
		create: PDO(%dsn%, %user%, %password%)
		autowired: PDO    # става предпочитан

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

	articles: Model\ArticleRepository

Сървисът articles няма да хвърли изключение, че съществуват два подходящи сървиса от тип PDO (т.е. mainDb и tempDb), а ще използва предпочитания сървис, т.е. mainDb.

Масив от сървиси

Autowiring може да предава и масиви от сървиси от определен тип. Тъй като в PHP не може нативно да се запише типът на елементите на масива, е необходимо освен типа array да се добави и phpDoc коментар с типа на елемента във формата ClassName[]:

namespace Model;

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

След това DI контейнерът автоматично предава масив от сървиси, съответстващи на дадения тип. Пропуска сървисите, които имат изключен autowiring.

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

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

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

class MySettings
{
	public function __construct(
		// readonly може да се използва от PHP 8.1
		public readonly bool $value,
	)
	{}
}

Създавате сървис от него, като го добавите към конфигурацията:

services:
	- MySettings('any value')

След това всички класове го изискват чрез autowiring.

Стесняване на autowiring

За отделни сървиси autowiring може да бъде стеснен само до определени класове или интерфейси.

Обикновено autowiring предава сървиса на всеки параметър на метод, чийто тип съответства на сървиса. Стесняването означава, че задаваме условия, на които трябва да отговарят типовете, посочени в параметрите на методите, за да им бъде предаден сървисът.

Ще го покажем с пример:

class ParentClass
{}

class ChildClass extends ParentClass
{}

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

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

Ако ги регистрираме всички като сървиси, autowiring ще се провали:

services:
	parent: ParentClass
	child: ChildClass
	parentDep: ParentDependent  # ХВЪРЛЯ ИЗКЛЮЧЕНИЕ, отговарят сървисите parent и child
	childDep: ChildDependent    # autowiring предава сървиса child на конструктора

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

Затова можем да стесним autowiring-а на сървиса child до тип ChildClass:

services:
	parent: ParentClass
	child:
		create: ChildClass
		autowired: ChildClass   # може да се напише и 'autowired: self'

	parentDep: ParentDependent  # autowiring предава сървиса parent на конструктора
	childDep: ChildDependent    # autowiring предава сървиса child на конструктора

Сега на конструктора на сървиса parentDep се предава сървисът parent, защото сега той е единственият подходящ обект. Autowiring вече не предава сървиса 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 и autowiring ще го предаде там.

Но ако стесним неговия autowiring до ChildClass с помощта на autowired: ChildClass (или self), autowiring ще го предаде само на конструктора на ChildDependent, тъй като той изисква аргумент от тип ChildClass и е вярно, че ChildClass е от тип ChildClass. Никой друг тип, посочен в другите параметри, не е надтип на ChildClass, така че сървисът не се предава.

Ако го ограничим до ParentClass с помощта на autowired: ParentClass, autowiring отново ще го предаде на конструктора на ChildDependent (тъй като изискваният ChildClass е надтип на ParentClass), а също и на конструктора на ParentDependent, тъй като изискваният тип ParentClass също е подходящ.

Ако го ограничим до FooInterface, той все още ще бъде автоматично инжектиран в ParentDependent (изискваният ParentClass е надтип на FooInterface) и ChildDependent, но освен това и в конструктора на FooDependent, но не и в BarDependent, тъй като BarInterface не е надтип на FooInterface.

services:
	child:
		create: ChildClass
		autowired: FooInterface

	fooDep: FooDependent        # autowiring предава child на конструктора
	barDep: BarDependent        # ХВЪРЛЯ ИЗКЛЮЧЕНИЕ, нито един сървис не отговаря
	parentDep: ParentDependent  # autowiring предава child на конструктора
	childDep: ChildDependent    # autowiring предава child на конструктора