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

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 згенерує його реалізацію. Інтерфейс повинен мати рівно один метод з ім'ям 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 може приймати параметри, які він потім передає конструктору. Наприклад, давайте додамо ID автора статті в клас 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)

Аксесор

Крім фабрик, Nette також може генерувати так звані аксесори. Це об'єкти з методом get(), який повертає конкретний сервіс з DI-контейнера. Повторні виклики get() як і раніше повертають один і той самий екземпляр.

Аксесор забезпечує ледаче завантаження залежностей. Нехай у нас є клас, який записує помилки в спеціальну базу даних. Якби в цьому класі з'єднання з базою даних передавалося конструктором як залежність, то з'єднання доводилося б створювати завжди, хоча на практиці помилка виникає дуже рідко, і тому з'єднання зазвичай залишалося б невикористаним. Замість цього клас передає метод доступу, і тільки при виклику його 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 createFoo(): Model\Foo;
	function getDb(): PDO;
}

Тому замість того, щоб передавати кілька згенерованих фабрик і аксесорів, ми збираємося передати ще одну складну фабрику, яка може робити більше.

Як варіант, замість кількох методів можна використовувати параметри create() і get():

interface MultiFactoryAlt
{
	function create($name);
	function get($name): PDO;
}

Потім MultiFactory::createArticle() робить те саме, що і MultiFactoryAlt::create('article'). Однак альтернативна нотація має той недолік, що незрозуміло, які значення $name підтримуються, і логічно неможливо розрізнити різні значення, що повертаються, для різних $name в інтерфейсі.

Визначення списку

А як визначити множинну фабрику в конфігурації? Ми створимо три сервіси, які будуть створювати/повертати, а потім і саму фабрику:

services:
	article: Article
	- Model\Foo
	- PDO(%dsn%, %user%, %password%)
	- MultiFactory(
		article: @article  # createArticle()
		foo: @Model\Foo    # createFoo()
		db: @\PDO          # getDb()
	)

Визначення з використанням тегів

Другий варіант – використовувати теги для визначення:

services:
	- App\Router\RouterFactory::createRouter
	- App\Model\DatabaseAccessor(
		db1: @database.db1.context
	)
версію: 3.x