Co je DI kontejner?
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 úvodní 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 natvrdo. Uděláme tedy další krok a přidáme parametry, aby byl kontejner skutečně užitečný:
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' => '***',
]);
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 array $services = [];
public function __construct(
private array $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.