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 никогда не использует имена сервисов. Он руководствуется исключительно системой типов PHP, поэтому знает и то, что класс удовлетворяет интерфейсам, которые он реализует, и классам, от которых он наследуется. Благодаря этому имя сервиса – лишь вспомогательный идентификатор, и его переименование ничего в приложении не сломает.

Чтобы 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: false:

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 можно отключить и глобально для целых типов параметром конфигурации di › excluded, где перечисляются типы (и их потомки), которые никогда не должны попадать в autowiring.

Настройка autowiring в Nette отличается от Symfony. В Symfony autowire: false означает, что autowiring не нужно использовать для аргументов конструктора сервиса. В Nette autowiring относится к аргументам конструктора и любых других методов, вызываемых через контейнер (например, при внедрении через сеттер). Параметр autowired: false не даёт контейнеру автоматически передавать экземпляр этого сервиса как зависимость другим сервисам.

Предпочтительный сервис для 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 отключён, и никогда не включает создаваемый в данный момент сервис в его собственный набор. В отличие от передачи отдельного сервиса, сужение 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 не выбрасывает исключение: он просто пропускает аргумент, и используется значение по умолчанию. Так объявляются необязательные зависимости:

class Foo
{
	public function __construct(
		private ?Logger $logger = null,
	) {}
}

Напротив, для параметра без значения по умолчанию отсутствие сервиса всегда приводит к исключению.

Сужение 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: child, parent, потому что в его конструктор подходят и сервис 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, но условие сужения autowired: ChildClass означает, что он будет передаваться только в параметры, явно объявленные как ChildClass (или его подтипы). Поскольку ParentDependent требует ParentClass, сервис child больше не считается там кандидатом на autowiring.

Для сервиса child запись autowired: ChildClass можно было бы заменить на autowired: self, потому что self – это подстановка для класса текущего сервиса.

В ключе autowired можно указать и несколько классов или интерфейсов массивом:

autowired: [ParentClass, 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
версия: 3.x