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