Що таке “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-контейнер, який може генерувати і оновлювати себе практично автоматично.