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.

wersja: 3.x