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.

wersja: 3.x