Згенеровані фабрики
Nette DI вміє автоматично генерувати код фабрик на основі інтерфейсів, що заощаджує вам написання коду.
Фабрика — це клас, який виробляє та конфігурує об'єкти. Отже, вона передає їм і їхні залежності. Будь ласка, не плутайте з патерном проектування factory method, який описує специфічний спосіб використання фабрик і не пов'язаний з цією темою.
Як виглядає така фабрика, ми показали у вступному розділі:
class ArticleFactory
{
public function __construct(
private Nette\Database\Connection $db,
) {
}
public function create(): Article
{
return new Article($this->db);
}
}
Nette DI вміє автоматично генерувати код фабрик. Все, що вам потрібно
зробити, це створити інтерфейс, і Nette DI згенерує реалізацію. Інтерфейс
повинен мати рівно один метод з назвою create
та декларувати тип
повернення:
interface ArticleFactory
{
function create(): Article;
}
Отже, фабрика ArticleFactory
має метод create
, який створює
об'єкти Article
. Клас Article
може виглядати, наприклад, так:
class Article
{
public function __construct(
private Nette\Database\Connection $db,
) {
}
}
Фабрику додаємо до конфігураційного файлу:
services:
- ArticleFactory
Nette DI згенерує відповідну реалізацію фабрики.
У коді, який використовує фабрику, ми запитуємо об'єкт за інтерфейсом, і Nette DI використає згенеровану реалізацію:
class UserController
{
public function __construct(
private ArticleFactory $articleFactory,
) {
}
public function foo()
{
// дозволимо фабриці створити об'єкт
$article = $this->articleFactory->create();
}
}
Параметризована фабрика
Фабричний метод create
може приймати параметри, які потім
передасть до конструктора. Доповнимо, наприклад, клас Article
ідентифікатором автора статті:
class Article
{
public function __construct(
private Nette\Database\Connection $db,
private int $authorId,
) {
}
}
Параметр додамо також до фабрики:
interface ArticleFactory
{
function create(int $authorId): Article;
}
Завдяки тому, що параметр у конструкторі та параметр у фабриці називаються однаково, Nette DI їх повністю автоматично передасть.
Розширена дефініція
Визначення можна записати і в багаторядковому вигляді за допомогою
ключа implement
:
services:
articleFactory:
implement: ArticleFactory
При записі цим довшим способом можна вказати додаткові аргументи для
конструктора в ключі arguments
та додаткову конфігурацію за
допомогою setup
, так само, як для звичайних сервісів.
Приклад: якби метод create()
не приймав параметр $authorId
, ми
могли б вказати фіксоване значення в конфігурації, яке передавалося б
до конструктора Article
:
services:
articleFactory:
implement: ArticleFactory
arguments:
authorId: 123
Або навпаки, якби create()
приймав параметр $authorId
, але він
не був би частиною конструктора і передавався б методом
Article::setAuthorId()
, ми б посилалися на нього в секції setup
:
services:
articleFactory:
implement: ArticleFactory
setup:
- setAuthorId($authorId)
Accessor
Nette, крім фабрик, вміє генерувати так звані аксесори (accessors). Це об'єкти
з методом get()
, який повертає певний сервіс з DI-контейнера.
Повторний виклик get()
повертає завжди той самий екземпляр.
Аксесори забезпечують ліниве завантаження (lazy-loading) залежностей.
Уявімо клас, який записує помилки до спеціальної бази даних. Якби цей
клас отримував підключення до бази даних як залежність через
конструктор, підключення завжди б створювалося, хоча на практиці
помилка виникає лише зрідка, і тому здебільшого з'єднання залишалося б
невикористаним. Замість цього клас передає аксесор, і лише коли
викликається його get()
, відбувається створення об'єкта
бази даних:
Як створити аксесор? Достатньо написати інтерфейс, і Nette DI згенерує
реалізацію. Інтерфейс повинен мати рівно один метод з назвою get
та декларувати тип повернення:
interface PDOAccessor
{
function get(): PDO;
}
Аксесор додаємо до конфігураційного файлу, де також є визначення сервісу, який він буде повертати:
services:
- PDOAccessor
- PDO(%dsn%, %user%, %password%)
Оскільки аксесор повертає сервіс типу PDO
, а в конфігурації є
лише один такий сервіс, він повертатиме саме його. Якщо сервісів даного
типу було б більше, ми б визначили сервіс, що повертається, за допомогою
назви, наприклад, - PDOAccessor(@db1)
.
Багаторазова фабрика/аксесор
Наші фабрики та аксесори досі вміли завжди виробляти або повертати
лише один об'єкт. Але можна дуже легко створити і багаторазові фабрики,
комбіновані з аксесорами. Інтерфейс такого класу міститиме довільну
кількість методів з назвами create<name>()
та get<name>()
,
наприклад:
interface MultiFactory
{
function createArticle(): Article;
function getDb(): PDO;
}
Отже, замість того, щоб передавати кілька згенерованих фабрик та аксесорів, ми передамо одну більш комплексну фабрику, яка вміє більше.
Альтернативно, замість кількох методів можна використовувати
get()
з параметром:
interface MultiFactoryAlt
{
function get($name): PDO;
}
Тоді виконується, що MultiFactory::getArticle()
робить те саме, що й
MultiFactoryAlt::get('article')
. Однак альтернативний запис має той недолік,
що незрозуміло, які значення $name
підтримуються, і логічно також
неможливо в інтерфейсі розрізнити різні значення, що повертаються, для
різних $name
.
Визначення списком
Таким чином можна визначити багаторазову фабрику в конфігурації:
services:
- MultiFactory(
article: Article # визначає createArticle()
db: PDO(%dsn%, %user%, %password%) # визначає getDb()
)
Або ми можемо у визначенні фабрики посилатися на існуючі сервіси за допомогою посилання:
services:
article: Article
- PDO(%dsn%, %user%, %password%)
- MultiFactory(
article: @article # визначає createArticle()
db: @\PDO # визначає getDb()
)
Визначення за допомогою тегів
Другою можливістю є використання для визначення тегів:
services:
- App\Core\RouterFactory::createRouter
- App\Model\DatabaseAccessor(
db1: @database.db1.explorer
)