Co to jest kontener DI?
Kontener wstrzykiwania zależności (DIC) to klasa, która potrafi tworzyć instancje i konfigurować obiekty.
Może Cię to zaskoczyć, ale w wielu przypadkach nie potrzebujesz kontenera wstrzykiwania zależności, aby móc korzystać z zalet wstrzykiwania zależności (krótko DI). Przecież nawet w rozdziale wstępnym pokazaliśmy DI na konkretnych przykładach i żaden kontener nie był potrzebny.
Jeśli jednak potrzebujesz zarządzać dużą liczbą różnych obiektów z wieloma zależnościami, kontener wstrzykiwania zależności będzie naprawdę przydatny. Co ma miejsce na przykład 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
. A dla tych klas utworzymy teraz kontener.
Oczywiście dla tak prostego przykładu nie ma sensu mieć kontenera. Ale utworzymy go, aby pokazać, jak wygląda i działa.
Oto prosty, hardkodowany kontener dla podanego 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 następująco:
$container = new Container;
$controller = $container->createUserController();
Kontenera pytamy tylko o obiekt i nie musimy już wiedzieć nic o tym, jak go utworzyć i jakie ma zależności; to wszystko wie kontener. Zależności są wstrzykiwane przez kontener automatycznie. W tym tkwi jego siła.
Kontener ma na razie wszystkie dane zapisane na stałe. Zrobimy więc kolejny krok i dodamy parametry, aby kontener był rzeczywiście 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' => '***',
]);
Bystrzy czytelnicy mogli zauważyć pewien problem. Za każdym razem, gdy pobieram obiekt UserController
, tworzona
jest również nowa instancja ArticleFactory
i bazy danych. Tego zdecydowanie nie chcemy.
Dodamy więc metodę getService()
, która będzie zwracać zawsze 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') będzie wywoływać createDatabase()
$method = 'create' . $name;
$this->services[$name] = $this->$method();
}
return $this->services[$name];
}
// ...
}
Przy pierwszym wywołaniu np. $container->getService('Database')
zleci createDatabase()
utworzenie
obiektu bazy danych, który zapisze w tablicy $services
, a przy następnym wywołaniu od razu go zwróci.
Zmodyfikujemy również resztę kontenera, aby używał getService()
:
class Container
{
// ...
public function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->getService('Database'));
}
public function createUserController(): UserController
{
return new UserController($this->getService('ArticleFactory'));
}
}
Nawiasem mówiąc, terminem usługa określa się dowolny obiekt zarządzany przez kontener. Stąd też nazwa metody
getService()
.
Gotowe. Mamy w pełni funkcjonalny kontener DI! I możemy go użyć:
$container = new Container([
'db.dsn' => 'mysql:',
'db.user' => 'root',
'db.password' => '***',
]);
$controller = $container->getService('UserController');
$database = $container->getService('Database');
Jak widzisz, napisanie DIC nie jest niczym skomplikowanym. Warto przypomnieć, że same obiekty nie wiedzą, że tworzy je jakiś kontener. Dzięki temu można w ten sposób tworzyć 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, który potrafi generować się i aktualizować niemal sam.