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

Контейнер впровадження залежностей (DIC) – це клас, який може інстанціювати та конфігурувати об'єкти.

Це може вас здивувати, але в багатьох випадках вам не потрібен контейнер для впровадження залежностей, щоб скористатися перевагами впровадження залежностей (скорочено DI). Зрештою, навіть у попередньому розділі ми показували конкретні приклади DI, і жоден контейнер не був потрібен.

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

У попередньому розділі ми познайомилися з класами 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') створюватиметься об'єкт бази даних, який він зберігатиме в масиві $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-контейнер, який може генерувати і оновлювати себе практично автоматично.

версію: 3.x