Generowane fabryki

Nette DI potrafi automatycznie wygenerować kod fabryki na podstawie interfejsów, oszczędzając Ci pisania kodu.

Fabryka to klasa odpowiedzialna za tworzenie obiektów i przekazywanie ich zależności. Nie myl tego ze wzorcem projektowym factory method, który opisuje konkretny sposób używania fabryk i nie ma z tym tematem związku.

Jak taka fabryka wygląda, pokazaliśmy w rozdziale wprowadzającym:

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

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

Nette DI potrafi automatycznie wygenerować kod fabryki. Wystarczy, że utworzysz 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;
}

Fabryka ArticleFactory ma więc metodę create, która tworzy obiekty Article. Klasa Article może wyglądać na przykład tak:

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

Dodaj fabrykę do pliku konfiguracyjnego:

services:
	- ArticleFactory

Nette DI wygeneruje odpowiadającą jej implementację fabryki.

W kodzie używającym fabryki poproś o obiekt przez jego interfejs, a Nette DI dostarczy wygenerowaną implementację:

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

	public function foo()
	{
		// pozwalamy fabryce utworzyć obiekt
		$article = $this->articleFactory->create();
	}
}

Fabryka z parametrami

Metoda fabryczna create może przyjmować parametry, które następnie przekazuje do konstruktora. Dodajmy na przykład do klasy Article 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;
}

Ponieważ nazwa parametru w konstruktorze ($authorId) zgadza się z nazwą parametru w metodzie fabrycznej, Nette DI przekazuje go automatycznie.

Definicja zaawansowana

Definicję można zapisać również w postaci wielowierszowej, używając klucza implement:

services:
	articleFactory:
		implement: ArticleFactory

Użycie tej dłuższej postaci pozwala podać dodatkowe argumenty konstruktora kluczem arguments i dalszą konfigurację przez setup, podobnie jak przy zwykłych definicjach usług.

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

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

Odwrotnie, gdyby create() przyjmowało $authorId, ale nie byłby on częścią konstruktora, tylko przekazywany metodą w rodzaju Article::setAuthorId(), odwołalibyśmy się do parametru w sekcji setup:

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

Akcesor

Poza fabrykami Nette potrafi generować również tak zwane akcesory. To obiekty z metodą get(), która zwraca konkretną usługę z kontenera DI. Powtarzane wywołania get() zawsze zwracają tę samą instancję.

Akcesory zapewniają leniwe ładowanie zależności. Rozważ klasę, która loguje błędy do dedykowanej bazy danych. Gdyby klasa ta otrzymywała połączenie z bazą danych przez wstrzykiwanie w konstruktorze, połączenie nawiązywałoby się zawsze, nawet jeśli błędy występują rzadko, a połączenie przez większość czasu pozostaje niewykorzystane. Zamiast tego klasa może otrzymać akcesor. Obiekt bazy danych (połączenie) tworzony jest dopiero wtedy, gdy metoda get() akcesora zostanie wywołana po raz pierwszy.

Jak utworzyć akcesor? Wystarczy napisać interfejs, a Nette DI wygeneruje implementację. Interfejs musi mieć dokładnie jedną metodę o nazwie get, która nie przyjmuje parametrów i deklaruje typ zwracany:

interface PDOAccessor
{
	function get(): PDO;
}

Dodaj akcesor do pliku konfiguracyjnego wraz z definicją usługi, którą ma zwracać:

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

Ponieważ akcesor zwraca usługę PDO, a w konfiguracji zdefiniowana jest tylko jedna taka usługa, akcesor zwróci właśnie ją. Jeśli usług tego typu jest więcej, podaj po nazwie, którą akcesor ma zwracać, np. - PDOAccessor(@db1).

Multifabryka/akcesor

Jak dotąd nasze fabryki i akcesory potrafiły tworzyć albo zwracać tylko jeden typ obiektu. Możesz jednak łatwo utworzyć multifabryki, które łączą cechy fabryk i akcesorów. Interfejs takiego komponentu może zawierać wiele metod o nazwach create<Nazwa>() i get<Nazwa>(), na przykład:

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

Zamiast wstrzykiwać wiele osobnych fabryk i akcesorów, możesz więc wstrzyknąć jeden, bardziej kompleksowy komponent.

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

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

MultiFactory::getDb() robi wtedy to samo co MultiFactoryAlt::get('db'). Ten alternatywny zapis ma jednak tę wadę, że obsługiwane wartości $name nie wynikają jawnie z sygnatury interfejsu. Ponadto nie da się zdefiniować w interfejsie różnych typów zwracanych dla różnych wartości $name.

Zamiast get($name) interfejs może deklarować create($name), który przy każdym wywołaniu zwraca nową instancję (podczas gdy get() zwraca współdzieloną). Interfejs może zawierać tylko jedną taką metodę z parametrem. Jeśli typ zwracany metody jest nullable (np. ?PDO), dla nieznanego $name zwraca null zamiast zgłaszać wyjątek.

Definicja z listą

Multifabrykę możesz zdefiniować w konfiguracji za pomocą listy, z usługami zapisanymi w linii:

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

Alternatywnie możesz odwołać się w definicji multifabryki 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 z tagami

Innym sposobem zdefiniowania multifabryki jest użycie tagów. Wartość tagu określa nazwę odpowiadającej metody:

services:
	article:
		create: Article
		tags: {multi: article}     # definiuje createArticle()
	db:
		create: PDO(%dsn%, %user%, %password%)
		tags: {multi: db}          # definiuje getDb()

	- MultiFactory(tagged: multi)
wersja: 3.x