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 умеет подставлять только объекты и массивы объектов. Скалярные аргументы (например, строки, числа, булевы значения) запишем в конфигурации. Альтернативой является создание объекта настроек, который инкапсулирует скалярное значение (или несколько значений) в виде объекта, и его затем можно снова передавать с помощью 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 не может решить,
какой из них выбрать.
Поэтому для сервиса child
мы можем сузить его autowiring до типа
ChildClass
:
services:
parent: ParentClass
child:
create: ChildClass
autowired: ChildClass # можно также написать 'autowired: self'
parentDep: ParentDependent # autowiring передаст сервис parent в конструктор
childDep: ChildDependent # autowiring передаст сервис child в конструктор
Теперь в конструктор сервиса parentDep
передается сервис
parent
, потому что теперь это единственный подходящий объект.
Сервис child
autowiring туда больше не передаст. Да, сервис 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 в конструктор