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