Генерируемые фабрики

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 ID автора статьи:

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 умеет кроме фабрик генерировать и так называемые accessory. Это объекты с методом get(), который возвращает определенный сервис из DI-контейнера. Повторный вызов get() возвращает все тот же экземпляр.

Accessor предоставляют зависимостям lazy-loading. Представим класс, который записывает ошибки в специальную базу данных. Если бы этот класс получал подключение к базе данных как зависимость через конструктор, подключение всегда должно было бы создаваться, хотя на практике ошибка возникает лишь изредка, и, следовательно, в большинстве случаев соединение оставалось бы неиспользованным. Вместо этого класс передаст себе accessor, и только когда будет вызван его get(), произойдет создание объекта базы данных:

Как создать accessor? Достаточно написать интерфейс, и Nette DI сгенерирует реализацию. Интерфейс должен иметь ровно один метод с именем get и объявлять возвращаемый тип:

interface PDOAccessor
{
	function get(): PDO;
}

Accessor добавим в файл конфигурации, где также находится определение сервиса, который он будет возвращать:

services:
	- PDOAccessor
	- PDO(%dsn%, %user%, %password%)

Поскольку accessor возвращает сервис типа PDO, а в конфигурации есть единственный такой сервис, он будет возвращать именно его. Если бы сервисов данного типа было больше, мы бы определили возвращаемый сервис с помощью имени, например, - PDOAccessor(@db1).

Множественная фабрика/аксессор

Наши фабрики и accessory до сих пор умели всегда производить или возвращать только один объект. Но можно очень легко создать и множественные фабрики, комбинированные с accessory. Интерфейс такого класса будет содержать любое количество методов с именами create<name>() и get<name>(), например:

interface MultiFactory
{
	function createArticle(): Article;
	function getDb(): PDO;
}

Так что вместо того, чтобы передавать себе несколько сгенерированных фабрик и accessory, мы передадим одну более комплексную фабрику, которая умеет больше.

Альтернативно, вместо нескольких методов можно использовать 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