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.

wersja: 3.x