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 на конструктора