Що таке DI-контейнер?

Dependency injection контейнер (DIC) — це клас, який вміє інстанціювати та конфігурувати об'єкти.

Можливо, вас це здивує, але в багатьох випадках вам не потрібен dependency injection контейнер, щоб скористатися перевагами dependency injection (коротко DI). Адже навіть у вступному розділі ми показали DI на конкретних прикладах, і жоден контейнер не був потрібний.

Однак, якщо вам потрібно керувати великою кількістю різних об'єктів з багатьма залежностями, dependency injection контейнер буде дійсно корисним. Що, наприклад, стосується веб-додатків, побудованих на фреймворку.

У попередньому розділі ми представили класи Article та UserController. Обидва мають певні залежності, а саме базу даних та фабрику ArticleFactory. І для цих класів ми тепер створимо контейнер. Звичайно, для такого простого прикладу немає сенсу мати контейнер. Але ми створимо його, щоб показати, як він виглядає і працює.

Ось простий жорстко закодований контейнер для наведеного прикладу:

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());
	}
}

Використання виглядало б так:

$container = new Container;
$controller = $container->createUserController();

Ми лише запитуємо у контейнера об'єкт і вже не повинні нічого знати про те, як його створити та які у нього залежності; все це знає контейнер. Залежності контейнером вводяться автоматично. У цьому його сила.

Контейнер поки що має всі дані записані жорстко. Зробимо наступний крок і додамо параметри, щоб контейнер став дійсно корисним:

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' => '***',
]);

Уважні читачі, можливо, помітили певну проблему. Кожного разу, коли я отримую об'єкт UserController, також створюється новий екземпляр ArticleFactory та бази даних. Цього ми точно не хочемо.

Тому додамо метод getService(), який буде повертати завжди ті самі екземпляри:

class Container
{
	private array $services = [];

	public function __construct(
		private array $parameters,
	) {
	}

	public function getService(string $name): object
	{
		if (!isset($this->services[$name])) {
			// getService('Database') викличе createDatabase()
			$method = 'create' . $name;
			$this->services[$name] = $this->$method();
		}
		return $this->services[$name];
	}

	// ...
}

При першому виклику, наприклад, $container->getService('Database'), він попросить createDatabase() створити об'єкт бази даних, який збереже в масиві $services, а при наступному виклику просто поверне його.

Змінимо і решту контейнера, щоб він використовував getService():

class Container
{
	// ...

	public function createArticleFactory(): ArticleFactory
	{
		return new ArticleFactory($this->getService('Database'));
	}

	public function createUserController(): UserController
	{
		return new UserController($this->getService('ArticleFactory'));
	}
}

До речі, терміном “сервіс” позначається будь-який об'єкт, керований контейнером. Тому й назва методу getService().

Готово. У нас є повністю функціональний DI-контейнер! І ми можемо його використовувати:

$container = new Container([
	'db.dsn' => 'mysql:',
	'db.user' => 'root',
	'db.password' => '***',
]);

$controller = $container->getService('UserController');
$database = $container->getService('Database');

Як бачите, написати DIC не так вже й складно. Варто нагадати, що самі об'єкти не знають, що їх створює якийсь контейнер. Таким чином, можна створювати будь-який об'єкт у PHP без втручання в його вихідний код.

Ручне створення та підтримка класу контейнера може досить швидко стати кошмаром. Тому в наступному розділі ми поговоримо про Nette DI Container, який вміє генеруватися та оновлюватися майже самостійно.

версія: 3.x