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

Dependency Injection контейнер (DIC) — это класс, который умеет инстанцировать и конфигурировать объекты.

Возможно, вас это удивит, но во многих случаях вам не нужен dependency injection контейнер, чтобы использовать преимущества dependency injection (кратко DI). Ведь даже во вводной главе мы на конкретных примерах показали DI, и никакой контейнер не был нужен.

Однако, если вам нужно управлять большим количеством различных объектов с множеством зависимостей, dependency injection container будет действительно полезен. Что, например, имеет место в веб-приложениях, построенных на фреймворке.

В предыдущей главе мы представили классы 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