Что такое «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-контейнере, который может генерировать и обновлять себя практически автоматически.