Автосвязывание
Autowiring, или автосвязывание — это отличная функция, которая может автоматически передавать сервисы в конструктор и другие методы, так что нам совсем не нужно их писать. Это сэкономит вам много времени.
Это позволяет нам пропустить подавляющее большинство аргументов при написании определений сервисов. Вместо:
services:
articles: Model\ArticleRepository(@database, @cache.storage)
Просто напишите:
services:
articles: Model\ArticleRepository
Автосвязывание управляется типами, поэтому класс ArticleRepository
должен быть определен следующим образом:
namespace Model;
class ArticleRepository
{
public function __construct(\PDO $db, \Nette\Caching\Storage $storage)
{}
}
Чтобы использовать автосвязывание, в контейнере должен быть только один сервис для каждого типа. Если бы их было больше, автосвязывание не знало бы, какой из них передавать, и выбрасывало бы исключение:
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb: PDO('sqlite::memory:')
articles: Model\ArticleRepository # ВЫБРАСЫВАЕТСЯ ИСКЛЮЧЕНИЕ, mainDb и tempDb совпадают
Решением может быть либо обход автоподключения, либо явное указание
имени сервиса (т. е. articles: Model\ArticleRepository(@mainDb)
). Однако удобнее отключить автосвязывание одного сервиса, или предпочесть конкретный сервис.
Отключенное автосвязывание
Вы можете отключить автоматическое определение зависимостей
сервисов с помощью параметра autowired: no
:
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb:
create: PDO('sqlite::memory:')
autowired: false # удаляет tempDb из автосвязывания
articles: Model\ArticleRepository # передает mainDb в конструктор
Сервис articles
не выбрасывает исключение о том, что есть два
соответствующих сервиса типа PDO
(т. е. mainDb
и tempDb
),
которые могут быть переданы конструктору, поскольку он видит только
сервис mainDb
.
Настройка autowiring в Nette работает иначе, чем в Symfony, где опция
autowire: false
говорит, что autowiring не должен использоваться для
аргументов конструктора сервиса. В Nette autowiring используется всегда, будь
то аргументы конструктора или любого другого метода. Опция
autowired: false
говорит, что экземпляр сервиса не должен передаваться
никуда с использованием autowiring.
Предпочтительное автосвязывание
Если у нас есть несколько сервисов одного типа и один из них имеет
опцию autowired
, этот сервис становится предпочтительным:
services:
mainDb:
create: PDO(%dsn%, %user%, %password%)
autowired: PDO # makes it preferred
tempDb:
create: PDO('sqlite::memory:')
articles: Model\ArticleRepository
Сервис articles
не выбрасывает исключение, если есть два
совпадающих сервиса PDO
(т. е. mainDb
и tempDb
), но
использует предпочтительный сервис, т. е. mainDb
.
Коллекция сервисов
Автосвязывание также может передавать массив сервисов
определенного типа. Так как PHP не может нативно обозначать тип
элементов массива, в дополнение к типу array
необходимо добавить
комментарий phpDoc с типом элемента, например ClassName[]
:
namespace Model;
class ShipManager
{
/**
* @param Shipper[] $shippers
*/
public function __construct(array $shippers)
{}
}
Затем контейнер DI автоматически передает массив сервисов, соответствующих заданному типу. При этом будут пропущены сервисы, у которых отключено автосвязывание.
Тип в комментарии также может иметь вид array<int, Class>
или
list<Class>
. Если нет возможности управлять формой комментария
phpDoc, то можно передать массив сервисов непосредственно в конфигурации,
используя typed()
.
Скалярные аргументы
Autowiring может передавать только объекты и массивы объектов. Скалярные аргументы (например, строки, числа, булевы) записываются в конфигурации. Альтернативой может быть создание settings-object, который инкапсулирует скалярное значение (или несколько значений) как объект, который затем может быть передан снова с помощью autowiring.
class MySettings
{
public function __construct(
// readonly можно использовать начиная с PHP 8.1
public readonly bool $value,
)
{}
}
Вы создаете сервис, добавляя его в конфигурацию:
services:
- MySettings('любое значение')
Затем все классы будут запрашивать его через autowiring.
Сужение автосвязывания
Для отдельных сервисов автоподключение может быть сужено до определенных классов или интерфейсов.
Обычно автосвязывание передает функцию каждому параметру метода, типу которого соответствует функция. Сужение означает, что мы указываем условия, которым должны удовлетворять типы, указанные для параметров метода, чтобы им была передана функция.
Рассмотрим пример:
class ParentClass
{}
class ChildClass extends ParentClass
{}
class ParentDependent
{
function __construct(ParentClass $obj)
{}
}
class ChildDependent
{
function __construct(ChildClass $obj)
{}
}
Если бы мы зарегистрировали их все как сервисы, автосвязывание было бы невозможно:
services:
parent: ParentClass
child: ChildClass
parentDep: ParentDependent # ВЫБРАСЫВАЕТ ИСКЛЮЧЕНИЕ, parent и child совпадают
childDep: ChildDependent # передает сервис 'child' конструктору
Сервис parentDep
выбрасывает исключение
Multiple services of type ParentClass found: parent, child
потому что и parent
, и
child
помещаются в его конструктор, и автосвязывание не может
принять решение о том, какой из них выбрать.
Поэтому для сервиса child
мы можем сузить его автосвязывание до
ChildClass
:
services:
parent: ParentClass
child:
create: ChildClass
autowired: ChildClass # альтернатива: 'autowired: self'
parentDep: ParentDependent # ВЫБРАСЫВАЕТ ИСКЛЮЧЕНИЕ, 'child' не может быть автоподключаемым
childDep: ChildDependent # передает сервис 'child' конструктору
Сервис parentDep
теперь передается в конструктор сервиса
parentDep
, поскольку теперь это единственный подходящий объект.
Сервис child
больше не передается через автосвязывание. Да,
функция 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
, а автосвязывание передаст
его туда.
Однако, если мы сузим автосвязывание до ChildClass
с помощью
autowired: ChildClass
(или self
), автосвязывание передает его только
конструктору ChildDependent
, поскольку для него требуется аргумент
типа ChildClass
и ChildClass
это тип ChildClass
. Ни один
другой тип, указанный для других параметров, не является заменой
ChildClass
, поэтому сервис не проходит.
Если мы ограничиваем его на ParentClass
с помощью
autowired: ParentClass
, то автосвязывание снова передаст его конструктору
ChildDependent
(потому что требуемый тип ChildClass
является
надмножеством ParentClass
) и конструктору ParentDependent
, так как
необходимый тип ParentClass
также соответствует.
Если мы ограничиваем его на FooInterface
, то он всё равно будет
подключаться для ParentDependent
(требуемый тип ParentClass
является
супертипом FooInterface
) и ChildDependent
, но дополнительно к
конструктору FooDependent
, но не BarDependent
, так как BarInterface
не супертип FooInterface
.
services:
child:
create: ChildClass
autowired: FooInterface
fooDep: FooDependent # передает сервис child конструктору
barDep: BarDependent # ВЫБРАСЫВАЕТ ИСКЛЮЧЕНИЕ, ни один сервис не пройдет
parentDep: ParentDependent # передает сервис child конструктору
childDep: ChildDependent # передает сервис child конструктору