Генерирани фабрики

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 може да приема параметри, които след това предава на конструктора. Нека добавим например 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)

Accessor

Освен фабрики, Nette може да генерира и т.нар. аксесори. Това са обекти с метод 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