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