Що таке DI-контейнер?
Dependency injection контейнер (DIC) — це клас, який вміє інстанціювати та конфігурувати об'єкти.
Можливо, вас це здивує, але в багатьох випадках вам не потрібен dependency injection контейнер, щоб скористатися перевагами dependency injection (коротко DI). Адже навіть у вступному розділі ми показали DI на конкретних прикладах, і жоден контейнер не був потрібний.
Однак, якщо вам потрібно керувати великою кількістю різних об'єктів з багатьма залежностями, dependency injection контейнер буде дійсно корисним. Що, наприклад, стосується веб-додатків, побудованих на фреймворку.
У попередньому розділі ми представили класи 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, який вміє генеруватися та оновлюватися майже самостійно.