Згенеровані фабрики

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
	)
версія: 3.x