Автокомуникация

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

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

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('любое значение')

След това всички класове ще го заявят чрез автоматично свързване.

Стесняване на автоматичното окабеляване

За отделни услуги автоматичното свързване може да бъде ограничено до конкретни класове или интерфейси.

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

Нека разгледаме един пример:

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   # ИЗКЛЮЧЕНИЕ, родител и дете са едно и също
	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         # предава подчинената услуга на конструктора
	barDep: BarDependent         # предава подчинената услуга на конструктора
	parentDep: ParentDependent   # Предава подчинената услуга на конструктора
	childDep: ChildDependent     # предава услугата на детето на конструктора