Nette DI Container

Nette DI je jednou z nejzajímavějších knihoven Nette. Umí generovat a automaticky aktualizovat kompilované DI kontejnery, které jsou extrémně rychlé a úžasně snadno konfigurovatelné.

Podobu služeb, které má vytvářet DI kontejner, definujeme obvykle pomocí konfiguračních souborů ve formátu NEON. Kontejner, který jsme ručně vytvořili v předchozí kapitole, by se zapsal takto:

parameters:
	db:
		dsn: 'mysql:'
		user: root
		password: '***'

services:
	- Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%)
	- ArticleFactory
	- EditController

Zápis je opravdu stručný.

Všechny závislosti deklarované v konstruktorech tříd ArticleFactory a EditController si Nette DI samo zjistí a předá díky tzv. autowiringu, v konfiguračním souboru proto není potřeba nic uvádět. Takže i když dojde ke změně parametrů, nemusíte v konfiguraci nic měnit. Během vývoje Nette kontejner automaticky přegeneruje. Vy se můžete soustředit čistě na vývoj aplikace.

Pokud chceme závislosti předávat pomocí setterů, použijeme k tomu sekci setup.

Nette DI vygeneruje přímo PHP kód kontejneru. Výsledkem je tedy soubor .php, který si můžete otevřít a studovat. Díky tomu přesně vidíte, jak kontejner funguje. Můžete jej také debuggovat v IDE a krokovat. A hlavně: vygenerované PHP je extrémně rychlé.

Nette DI umí také generovat kód továren na základě dodaného rozhraní. Proto místo třídy ArticleFactory nám bude stačit vytvořit v aplikaci jen interface:

interface ArticleFactory
{
	function create(): Article;
}

Celý příklad najdete v kurzu Dependency Injection by Example, kde si kontejner nejprve napíšete ručně a teprve pak ho necháte vygenerovat.

Samostatné použití

Nasazení knihovny Nette DI do aplikace je velmi snadné. Nejprve ji nainstalujeme Composerem (protože stahování zipů je tááák zastaralé):

composer require nette/di

Následující kód pomocí Compileru vytvoří instanci DI kontejneru podle konfigurace uložené v souboru config.neon:

$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp');
$class = $loader->load(function ($compiler) {
	$compiler->loadConfig(__DIR__ . '/config.neon');
});
$container = new $class;

Kontejner se vygeneruje jen jednou, jeho kód se zapíše do cache (adresář __DIR__ . '/temp') a při dalších požadavcích se už jen odsud načítá.

Compiler sám o sobě zpřístupní v konfiguraci jen sekce services a parameters. Chcete-li používat další – třeba search, decorator, di nebo inject – nejdřív zaregistrujte jejich rozšíření. A abyste mohli registrovat rozšíření ze sekce extensions v konfiguraci, přidejte ExtensionsExtension:

$compiler->addExtension('search', new Nette\DI\Extensions\SearchExtension($tempDir));
$compiler->addExtension('extensions', new Nette\DI\Extensions\ExtensionsExtension);

Všechna tato rozšíření za vás automaticky registruje Configurator používaný v plných Nette aplikacích.

Pokud v jednom cache adresáři udržujete několik různých kontejnerů, odlište je klíčem předaným jako druhý argument metody load(); ten se stane součástí názvu vygenerované třídy:

$class = $loader->load(
	fn($compiler) => $compiler->loadConfig(__DIR__ . '/config.neon'),
	'my-key',
);

Pro vytvoření a získání služeb slouží metody getService() nebo getByType(). Takto vytvoříme objekt EditController:

$controller = $container->getByType(EditController::class);
$controller->someMethod();

Během vývoje je užitečné aktivovat režim auto-refresh, kdy se kontejner automaticky přegeneruje, pokud dojde ke změně jakékoliv třídy nebo konfiguračního souboru. Stačí v konstruktoru ContainerLoader uvést jako druhý argument true.

$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true);

Auto-refresh funguje i v rámci jednoho procesu, což je důležité pro dlouho běžící workery, vývojové servery a CLI nástroje. Protože PHP nemůže znovu deklarovat už načtenou třídu, deklaruje každá přestavba kontejner pod novým názvem třídy a load() vrací název odpovídající aktuální konfiguraci. Volejte tedy load() znovu před každou jednotkou práce a kdykoli se vrácený název změní, vytvořte nový kontejner; pozná se i přestavba, kterou nad stejnou cache provedl jiný proces. Název třídy si nikdy neukládejte, mezi přestavbami není stabilní.

Práce s kontejnerem

Kromě getService() a getByType() nabízí objekt kontejneru několik dalších užitečných metod:

  • getByType(string $type, bool $throw = true): ?object vrátí službu daného typu. Pokud jako druhý argument předáte false, vrátí místo vyhození výjimky null, když žádná taková služba neexistuje.
  • hasService(string $name): bool a isCreated(string $name): bool zjistí, zda je služba definovaná a zda už byla vytvořena.
  • getServiceDescriptors(): array<string, ServiceDescriptor> popíše všechny registrované služby pod jejich názvy: typ, autowiring, tagy, aliasy a instanci, pokud už existuje. Nic přitom nevytvoří.
  • addService(string $name, object $service): static vloží službu do kontejneru; closure s deklarovaným návratovým typem zaregistruje továrnu místo hotové instance. removeService(string $name): void ji odebere.
  • getParameters(): array vrátí všechny parametry kontejneru, getParameter($key) vrátí jeden.
  • createInstance(string $class, array $args = []): object vytvoří novou instanci dané třídy a předá jí závislosti konstruktoru pomocí autowiringu.
  • callMethod(callable $function, array $args = []): mixed zavolá daný callable a předá mu argumenty pomocí autowiringu.
  • callInjects(object $service): void zavolá na daném objektu všechny metody inject*() a předá jim závislosti.

Konstruktor kontejneru také přijímá pole parametrů, které doplní ty definované v konfiguraci:

$container = new $class(['host' => 'localhost']);

PSR-11 Container

Třída Nette\Bridges\DIPsr\PsrContainer obalí kontejner a zpřístupní jej přes PSR-11 ContainerInterface, takže jej můžete předat jakékoliv knihovně nebo frameworku, který jej očekává:

$psr = new Nette\Bridges\DIPsr\PsrContainer($container);

$psr->get(PDO::class);   // typ, dohledaný autowiringem
$psr->get('database');   // název služby

Identifikátor se nejprve chápe jako typ a dohledá se autowiringem, teprve potom jako název služby. Pokud typu odpovídá více autowirovaných služeb, get() místo výběru jedné z nich vyhodí NotFoundException, zatímco has() vrátí false. Výjimky mostu implementují rozhraní z PSR-11, takže catch (Psr\Container\NotFoundExceptionInterface) funguje podle očekávání.

Balíček psr/container není závislostí nette/di, takže si jej do aplikace přidejte sami:

composer require psr/container

Použití s frameworkem Nette

Jak jsme si ukázali, použití Nette DI není omezené na aplikace psané v Nette Frameworku, můžete jej pomocí pouhých 3 řádků kódu nasadit kdekoliv. Pokud však vyvíjíte aplikace v Nette Framework, konfiguraci a vytvoření kontejneru má na starosti Bootstrap.

verze: 3.x 2.x