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

версия: 3.x