Автоматичне підключення
Автоматичне підключення (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
.
Конфігурація автоматичного підключення в Nette працює інакше,
ніж у Symfony, де опція autowire: false
вказує, що не слід використовувати
автоматичне підключення для аргументів конструктора даного сервісу. У
Nette автоматичне підключення використовується завжди, чи то для
аргументів конструктора, чи для будь-яких інших методів. Опція
autowired: false
вказує, що екземпляр даного сервісу не повинен
передаватися нікуди за допомогою автоматичного підключення.
Перевага автоматичного підключення
Якщо у нас є кілька сервісів одного типу і для одного з них ми
вказуємо опцію autowired
, цей сервіс стає пріоритетним:
services:
mainDb:
create: PDO(%dsn%, %user%, %password%)
autowired: PDO # стає пріоритетним
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()
.
Скалярні аргументи
Автоматичне підключення вміє підставляти лише об'єкти та масиви об'єктів. Скалярні аргументи (наприклад, рядки, числа, булеві значення) запишемо в конфігурації. Альтернативою є створення об'єкта налаштувань, який інкапсулює скалярне значення (або кілька значень) у вигляді об'єкта, і його потім можна знову передавати за допомогою автоматичного підключення.
class MySettings
{
public function __construct(
// readonly можна використовувати з PHP 8.1
public readonly bool $value,
)
{}
}
Ви створите з нього сервіс, додавши до конфігурації:
services:
- MySettings('any value')
Усі класи потім запитають його за допомогою автоматичного підключення.
Звуження автоматичного підключення
Для окремих сервісів можна звузити автоматичне підключення лише до певних класів або інтерфейсів.
Зазвичай автоматичне підключення передає сервіс до кожного параметра методу, типу якого сервіс відповідає. Звуження означає, що ми встановлюємо умови, яким повинні відповідати типи, зазначені у параметрах методів, щоб їм було передано сервіс.
Покажемо це на прикладі:
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 # автоматичне підключення передасть до конструктора сервіс parent
childDep: ChildDependent # автоматичне підключення передасть до конструктора сервіс child
Тепер до конструктора сервісу parentDep
передається сервіс
parent
, оскільки тепер це єдиний відповідний об'єкт. Сервіс
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