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
)