Какво е контейнер 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, който може да се генерира и актуализира почти автоматично.