Какво е DI контейнер?
Dependency injection контейнерът (DIC) е клас, който може да инстанцира и конфигурира обекти.
Може да ви изненада, но в много случаи не се нуждаете от dependency injection контейнер, за да се възползвате от предимствата на dependency injection (накратко DI). В края на краищата, дори в уводната глава показахме DI с конкретни примери и не беше необходим контейнер.
Въпреки това, ако трябва да управлявате голям брой различни обекти с много зависимости, dependency injection контейнерът ще бъде наистина полезен. Такъв е случаят например с уеб приложения, изградени върху framework.
В предишната глава представихме класовете Article
и
UserController
. И двата имат някои зависимости, а именно база данни и
фабриката ArticleFactory
. И сега ще създадем контейнер за тези класове.
Разбира се, за толкова прост пример няма смисъл да имаме контейнер. Но
ще го създадем, за да покажем как изглежда и работи.
Ето един прост hardcoded контейнер за дадения пример:
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();
Просто питаме контейнера за обект и вече не е нужно да знаем нищо за това как да го създадем или какви са неговите зависимости; контейнерът знае всичко това. Зависимостите се инжектират автоматично от контейнера. В това е неговата сила.
Засега контейнерът има всички данни, записани hardcoded. Така че ще направим следващата стъпка и ще добавим параметри, за да направим контейнера наистина полезен:
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, който може да се генерира и актуализира почти сам.