Co to jest kontener DI?
Dependency injection container (DIC) to klasa, która może instancjonować i konfigurować obiekty.
Może cię to zaskoczyć, ale w wielu przypadkach nie potrzebujesz kontenera wtrysku zależności, aby skorzystać z wtrysku zależności (w skrócie DI). Przecież nawet w poprzednim rozdziale pokazywaliśmy konkretne przykłady DI i żaden kontener nie był potrzebny.
Jeśli jednak musisz zarządzać dużą liczbą różnych obiektów z wieloma zależnościami, kontener wtrysku zależności będzie naprawdę przydatny. Co ma miejsce np. w przypadku aplikacji internetowych zbudowanych na frameworku.
W poprzednim rozdziale przedstawiliśmy klasy Article
i UserController
. Obie mają pewne
zależności, a mianowicie bazę danych i fabrykę ArticleFactory
. I dla tych klas stworzymy teraz kontener.
Oczywiście dla tak prostego przykładu posiadanie kontenera nie ma sensu. Ale stworzymy jeden, aby pokazać jak to wygląda
i działa.
Oto prosty kontener hardcoded dla tego 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 createUserController(): UserController
{
return new UserController($this->createArticleFactory());
}
}
Użycie wyglądałoby tak:
$container = new Container;
$controller = $container->createUserController();
Po prostu pytamy kontener o obiekt i nie musimy nic wiedzieć o tym, jak go stworzyć i jakie są jego zależności; kontener wie to wszystko. Zależności są wstrzykiwane automatycznie przez kontener. To jest jego moc.
Do tej pory kontener zapisywał wszystkie dane na górze. Robimy więc kolejny krok i dodajemy parametry, aby kontener był 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' => '***',
]);
Uważni czytelnicy mogli zauważyć pewien problem. Za każdym razem, gdy otrzymuję obiekt UserController
,
tworzona jest również nowa instancja ArticleFactory
i baza danych. Z pewnością tego nie chcemy.
Dodamy więc metodę getService()
, która będzie zwracała w kółko 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') bude volat createDatabase()
$method = 'create' . $name;
$this->services[$name] = $this->$method();
}
return $this->services[$name];
}
// ...
}
Przy pierwszym wywołaniu np. $container->getService('Database')
zleci createDatabase()
stworzenie
obiektu bazy danych, który zapisze w tablicy $services
i zwróci go bezpośrednio przy kolejnym wywołaniu.
Modyfikujemy też resztę kontenera, aby korzystał z getService()
:
class Container
{
// ...
public function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->getService('Database'));
}
public function createUserController(): UserController
{
return new UserController($this->getService('ArticleFactory'));
}
}
Przy okazji, termin usługa odnosi się do dowolnego obiektu zarządzanego przez kontener. Stąd nazwa metody
getService()
.
Zrobione. Mamy w pełni funkcjonalny kontener DI! I możemy z niego korzystać:
$container = new Container([
'db.dsn' => 'mysql:',
'db.user' => 'root',
'db.password' => '***',
]);
$controller = $container->getService('UserController');
$database = $container->getService('Database');
Jak widać, napisanie DIC nie jest trudne. Warto pamiętać, że same obiekty nie wiedzą, że tworzy je kontener. Można więc w ten sposób stworzyć dowolny obiekt w PHP bez ingerencji w jego kod źródłowy.
Ręczne tworzenie i utrzymywanie klasy kontenera może dość szybko stać się koszmarem. Dlatego w następnym rozdziale opowiemy o kontenerze Nette DI Container, który może generować i aktualizować się niemal samodzielnie.