Generowane fabryki

Nette DI potrafi automatycznie generować kod fabryk na podstawie interfejsów, co oszczędza Ci pisania kodu.

Fabryka to klasa, która tworzy i konfiguruje obiekty. Przekazuje im więc również ich zależności. Proszę nie mylić z wzorcem projektowym factory method, który opisuje specyficzny sposób wykorzystania fabryk i nie jest związany z tym tematem.

Jak wygląda taka fabryka, pokazaliśmy w rozdziale wstępnym:

class ArticleFactory
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function create(): Article
	{
		return new Article($this->db);
	}
}

Nette DI potrafi automatycznie generować kod fabryk. Wszystko, co musisz zrobić, to utworzyć interfejs, a Nette DI wygeneruje implementację. Interfejs musi mieć dokładnie jedną metodę o nazwie create i deklarować typ zwracany:

interface ArticleFactory
{
	function create(): Article;
}

Czyli fabryka ArticleFactory ma metodę create, która tworzy obiekty Article. Klasa Article może wyglądać na przykład następująco:

class Article
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}
}

Fabrykę dodajemy do pliku konfiguracyjnego:

services:
	- ArticleFactory

Nette DI wygeneruje odpowiednią implementację fabryki.

W kodzie, który używa fabryki, żądamy obiektu według interfejsu, a Nette DI użyje wygenerowanej implementacji:

class UserController
{
	public function __construct(
		private ArticleFactory $articleFactory,
	) {
	}

	public function foo()
	{
		// zlecamy fabryce utworzenie obiektu
		$article = $this->articleFactory->create();
	}
}

Fabryka sparametryzowana

Metoda fabryczna create może przyjmować parametry, które następnie przekaże do konstruktora. Uzupełnijmy na przykład klasę Article o ID autora artykułu:

class Article
{
	public function __construct(
		private Nette\Database\Connection $db,
		private int $authorId,
	) {
	}
}

Parametr dodamy również do fabryki:

interface ArticleFactory
{
	function create(int $authorId): Article;
}

Dzięki temu, że parametr w konstruktorze i parametr w fabryce nazywają się tak samo, Nette DI przekaże je całkowicie automatycznie.

Definicja zaawansowana

Definicję można zapisać również w formie wieloliniowej za pomocą klucza implement:

services:
	articleFactory:
		implement: ArticleFactory

Przy zapisie tym dłuższym sposobem można podać dodatkowe argumenty dla konstruktora w kluczu arguments oraz dodatkową konfigurację za pomocą setup, tak samo jak w przypadku zwykłych usług.

Przykład: gdyby metoda create() nie przyjmowała parametru $authorId, moglibyśmy podać stałą wartość w konfiguracji, która byłaby przekazywana do konstruktora Article:

services:
	articleFactory:
		implement: ArticleFactory
		arguments:
			authorId: 123

Lub odwrotnie, gdyby create() przyjmowała parametr $authorId, ale nie byłby on częścią konstruktora i przekazywany byłby metodą Article::setAuthorId(), odwołalibyśmy się do niego w sekcji setup:

services:
	articleFactory:
		implement: ArticleFactory
		setup:
			- setAuthorId($authorId)

Accessor

Nette potrafi oprócz fabryk generować również tzw. akcesory. Są to obiekty z metodą get(), która zwraca określoną usługę z kontenera DI. Powtarzane wywołanie get() zwraca zawsze tę samą instancję.

Akcesory zapewniają lazy-loading zależności. Miejmy klasę, która zapisuje błędy do specjalnej bazy danych. Gdyby ta klasa otrzymywała połączenie z bazą danych jako zależność przez konstruktor, połączenie musiałoby być zawsze tworzone, chociaż w praktyce błąd pojawia się tylko wyjątkowo, a więc w większości przypadków połączenie pozostałoby niewykorzystane. Zamiast tego klasa przekaże sobie akcesor i dopiero gdy zostanie wywołana jego metoda get(), dojdzie do utworzenia obiektu bazy danych:

Jak utworzyć akcesor? Wystarczy napisać interfejs, a Nette DI wygeneruje implementację. Interfejs musi mieć dokładnie jedną metodę o nazwie get i deklarować typ zwracany:

interface PDOAccessor
{
	function get(): PDO;
}

Akcesor dodajemy do pliku konfiguracyjnego, gdzie znajduje się również definicja usługi, którą będzie zwracał:

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

Ponieważ akcesor zwraca usługę typu PDO, a w konfiguracji jest jedyna taka usługa, będzie zwracał właśnie ją. Gdyby usług danego typu było więcej, określimy zwracaną usługę za pomocą nazwy, np. - PDOAccessor(@db1).

Wielokrotna fabryka/akcesor

Nasze fabryki i akcesory potrafiły dotychczas zawsze tworzyć lub zwracać tylko jeden obiekt. Można jednak bardzo łatwo utworzyć również wielokrotne fabryki połączone z akcesorami. Interfejs takiej klasy będzie zawierał dowolną liczbę metod o nazwach create<name>() i get<name>(), np.:

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

Więc zamiast przekazywać sobie kilka generowanych fabryk i akcesorów, przekażemy jedną bardziej złożoną fabrykę, która potrafi więcej.

Alternatywnie można zamiast kilku metod użyć get() z parametrem:

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

Wtedy obowiązuje, że MultiFactory::getArticle() robi to samo co MultiFactoryAlt::get('article'). Jednak alternatywny zapis ma tę wadę, że nie jest oczywiste, jakie wartości $name są obsługiwane i logicznie rzecz biorąc, nie można również w interfejsie rozróżnić różnych wartości zwracanych dla różnych $name.

Definicja listą

W ten sposób można zdefiniować wielokrotną fabrykę w konfiguracji:

services:
	- MultiFactory(
		article: Article                      # definiuje createArticle()
		db: PDO(%dsn%, %user%, %password%)    # definiuje getDb()
	)

Lub możemy w definicji fabryki odwołać się do istniejących usług za pomocą referencji:

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

Definicja za pomocą tagów

Drugą możliwością jest wykorzystanie do definicji tagów:

services:
	- App\Core\RouterFactory::createRouter
	- App\Model\DatabaseAccessor(
		db1: @database.db1.explorer
	)
wersja: 3.x