Что такое DI-контейнер?
Контейнер внедрения зависимостей (DIC или DI-контейнер) – объект, отвечающий за создание и настройку других объектов (называемых сервисами).
Возможно, вас это удивит, но во многих случаях, чтобы пользоваться преимуществами внедрения зависимостей (сокращённо DI), контейнер не нужен. Ведь даже во вводной главе мы показали конкретные примеры DI, и никакой контейнер там не понадобился.
Однако когда приходится управлять большим количеством объектов со сложными зависимостями, DI-контейнер становится очень полезен. Обычно так и бывает у веб-приложений, построенных на фреймворке.
В предыдущей главе мы познакомились с классами Article и
EditController. У обоих есть зависимости, а именно база данных и
фабрика 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 createEditController(): EditController
{
return new EditController($this->createArticleFactory());
}
}
Использование выглядело бы так:
$container = new Container;
$controller = $container->createEditController();
Мы просто запрашиваем объект у контейнера, и нам не нужно знать, как его создать и какие у него зависимости; всем этим занимается контейнер. Зависимости контейнер внедряет автоматически. В этом его сила.
Пока в контейнере все сведения прописаны жёстко. Поэтому сделаем следующий шаг и добавим параметры, чтобы контейнер стал по-настоящему полезным:
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' => '***',
]);
Внимательные читатели могли заметить проблему. Каждый раз, когда мы
получаем объект EditController, создаются и новые экземпляры
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 createEditController(): EditController
{
return new EditController($this->getService('ArticleFactory'));
}
}
Кстати, термином “сервис” называют любой объект, управляемый
контейнером. Отсюда и имя метода getService().
Готово. У нас полностью работающий DI-контейнер! И мы можем им пользоваться:
$container = new Container([
'db.dsn' => 'mysql:',
'db.user' => 'root',
'db.password' => '***',
]);
$controller = $container->getService('EditController');
$database = $container->getService('Database');
Как видите, написать DIC несложно. Стоит отметить, что сами объекты не знают о том, что их создаёт контейнер. Благодаря этому таким способом можно создавать любой объект PHP, не меняя его исходный код.
Создавать и поддерживать класс контейнера вручную может быстро превратиться в кошмар. Поэтому в следующей главе мы поговорим о Nette DI Container, который умеет порождать и обновлять себя практически автоматически.