Czym jest kontener DI?
Kontener wstrzykiwania zależności (DIC albo kontener DI) to obiekt odpowiedzialny za tworzenie instancji i konfigurowanie innych obiektów (nazywanych usługami).
Może Cię to zaskoczyć, ale w wielu przypadkach nie potrzebujesz kontenera wstrzykiwania zależności, aby korzystać z zalet wstrzykiwania zależności (w skrócie DI). Przecież nawet w rozdziale wprowadzającym pokazywaliśmy konkretne przykłady DI i żaden kontener nie był potrzebny.
Gdy jednak zarządzasz dużą liczbą obiektów o złożonych zależnościach, kontener DI staje się bardzo przydatny. Tak bywa często w aplikacjach webowych zbudowanych na frameworku.
W poprzednim rozdziale przedstawiliśmy klasy Article i EditController. Obie mają zależności,
mianowicie bazę danych i fabrykę ArticleFactory. I dla tych klas utworzymy teraz kontener. Oczywiście tworzenie
kontenera dla tak prostego przykładu to przesada. Utworzymy go jednak, aby pokazać, jak wygląda i jak działa.
Oto prosty, zapisany na sztywno kontener dla powyższego przykładu:
class Container
{
public function createDatabase(): Nette\Database\Connection
{
return new Nette\Database\Connection('mysql:', 'root', '***');
}
public function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->createDatabase());
}
public function createEditController(): EditController
{
return new EditController($this->createArticleFactory());
}
}
Użycie wyglądałoby tak:
$container = new Container;
$controller = $container->createEditController();
Po prostu prosimy kontener o obiekt, nie musząc wiedzieć, jak go utworzyć ani jakie ma zależności; kontener zajmuje się tym wszystkim. Zależności wstrzykiwane są przez kontener automatycznie. Na tym polega jego siła.
Na razie kontener ma wszystkie informacje zapisane na sztywno. Zróbmy więc kolejny krok i dodajmy parametry, aby kontener stał się naprawdę użyteczny:
class Container
{
public function __construct(
private array $parameters,
) {
}
public function createDatabase(): Nette\Database\Connection
{
return new Nette\Database\Connection(
$this->parameters['db.dsn'],
$this->parameters['db.user'],
$this->parameters['db.password'],
);
}
// ...
}
$container = new Container([
'db.dsn' => 'mysql:',
'db.user' => 'root',
'db.password' => '***',
]);
Spostrzegawczy czytelnicy mogą zauważyć problem. Za każdym razem, gdy pobieramy obiekt EditController,
tworzone są również nowe instancje ArticleFactory i połączenia z bazą danych. Zdecydowanie tego nie
chcemy.
Dodamy więc metodę getService(), która zawsze zwróci te same instancje:
class Container
{
private array $services = [];
public function __construct(
private array $parameters,
) {
}
public function getService(string $name): object
{
if (!isset($this->services[$name])) {
// getService('Database') wywoła createDatabase()
$method = 'create' . $name;
$this->services[$name] = $this->$method();
}
return $this->services[$name];
}
// ...
}
Przy pierwszym wywołaniu, na przykład $container->getService('Database'), wywoła
createDatabase(), aby utworzyć obiekt bazy danych, zapisze go do tablicy $services i zwróci. Przy
kolejnych wywołaniach zwraca bezpośrednio już zapisaną instancję.
Modyfikujemy również resztę kontenera, aby używała getService():
class Container
{
// ...
public function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->getService('Database'));
}
public function createEditController(): EditController
{
return new EditController($this->getService('ArticleFactory'));
}
}
Nawiasem mówiąc, terminem usługa określamy dowolny obiekt zarządzany przez kontener. Stąd nazwa metody
getService().
Gotowe. Mamy w pełni działający kontener DI! I możemy go używać:
$container = new Container([
'db.dsn' => 'mysql:',
'db.user' => 'root',
'db.password' => '***',
]);
$controller = $container->getService('EditController');
$database = $container->getService('Database');
Jak widzisz, napisanie DIC nie jest trudne. Warto zauważyć, że same obiekty nie wiedzą, że tworzy je kontener. W konsekwencji można w ten sposób tworzyć dowolny obiekt PHP bez modyfikowania jego kodu źródłowego.
Ręczne tworzenie i utrzymywanie klasy kontenera może szybko stać się koszmarem. Dlatego w kolejnym rozdziale omówimy Nette DI Container, który potrafi generować się i aktualizować niemal automatycznie.