Dependency Injection Container

Dependency injection kontejner (DIC) je třída, která umí instancovat a konfigurovat objekty.

Možná vás to překvapí, ale v mnoha případech nepotřebujete dependency injection kontejner, abyste mohli využívat výhod dependency injection (krátce DI). Vždyť i v předchozí kapitole jsme si na konkrétních příkladech DI ukázali a žádný kontejner nebyl potřeba.

Pokud však potřebujete spravovat velké množství různých objektů s mnoha závislostmi, bude dependency injection container opravdu užitečný. Což je třeba případ webových aplikací postavených na frameworku.

V předchozí kapitole jsme si představili třídy Article a UserController. Obě mají nějaké závislosti, a to databázi a továrnu ArticleFactory. A pro tyto třídy si nyní vytvoříme kontejner. Samozřejmě pro tak jednoduchý příklad nemá smysl mít kontejner. Ale vytvoříme ho, abychom si ukázali, jak vypadá a funguje.

Zde je jednoduchý hardcoded kontejner pro uvedený příklad:

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());
	}
}

Použití by vypadalo následovně:

$container = new Container;
$controller = $container->createUserController();

Kontejneru se pouze zeptáme na objekt a již nemusíme vědět nic o tom, jak jej vytvořit a jaké má závislosti; to všechno ví kontejner. Závislosti jsou kontejnerem injektovány automaticky. V tom je jeho síla.

Kontejner má zatím zapsané všechny údaje navrdo. Uděláme tedy další krok a přidáme parametry, aby byl kontejner skutečně užitečný:

class Container
{
	private $parameters;

	public function __construct(array $parameters)
	{
		$this->parameters = $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' => '***',
]);

Bystří čtenáři si možná všimli jistého problému. Pokaždé, když získám objekt UserController, vytvoří se také nová instance ArticleFactory a databáze. To rozhodně nechceme.

Přidáme proto metodu getService(), která bude vracet stále stejné instance:

class Container
{
	private $parameters;
	private $services = [];

	public function __construct(array $parameters)
	{
		$this->parameters = $parameters;
	}

	public function getService(string $name): object
	{
		if (!isset($this->services[$name])) {
			// getService('Database') bude volat createDatabase()
			$method = 'create' . $name;
			$this->services[$name] = $this->$method();
		}
		return $this->services[$name];
	}

	// ...
}

Při prvním volání např. $container->getService('Database') si nechá od createDatabase() vytvořit objekt databáze, který uloží do pole $services a při příštím volání jej rovnou vrátí.

Upravíme i zbytek kontejneru, aby používal getService():

class Container
{
	// ...

	public function createArticleFactory(): ArticleFactory
	{
		return new ArticleFactory($this->getService('Database'));
	}

	public function createUserController(): UserController
	{
		return new UserController($this->getService('ArticleFactory'));
	}
}

Mimochodem, termínem služba se označuje jakýkoliv objekt spravovaný kontejnerem. Proto i ten název metody getService().

Hotovo. Máme plně funkční DI kontejner! A můžeme ho použít:

$container = new Container([
	'db.dsn' => 'mysql:',
	'db.user' => 'root',
	'db.password' => '***',
]);

$controller = $container->getService('UserController');
$database = $container->getService('Database');

Jak vidíte, napsat DIC není nic složitého. Za připomenutí stojí, že samotné objekty neví, že je vytváří nějaký kontejner. Tím pádem je možné takto vytvářet jakýkoliv objekt v PHP bez zásahu do jeho zdrojového kódu.

Ruční vytváření a údržba třídy kontejneru se může poměrně rychle stát noční můrou. V další kapitole si proto povíme o Nette DI Containeru, který se umí generovat a aktualizovat téměř sám.