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