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

версия: 3.x