Nette DI Container
******************
.[perex]
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|neon:format]. Kontejner, který jsme ručně vytvořili v [předchozí kapitole|container], by se zapsal takto:
```neon
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|autowiring], 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 |services#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|factory] na základě dodaného rozhraní. Proto místo třídy `ArticleFactory` nám bude stačit vytvořit v aplikaci jen interface:
```php
interface ArticleFactory
{
function create(): Article;
}
```
Celý příklad najdete v kurzu [Dependency Injection by Example |https://github.com/nette-examples/di-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é):
```shell
composer require nette/di
```
Následující kód pomocí [Compileru |api:Nette\DI\Compiler] vytvoří instanci DI kontejneru podle konfigurace uložené v souboru `config.neon`:
```php
$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`:
```php
$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 |application:bootstrapping] 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:
```php
$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`:
```php
$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 |api:Nette\DI\ContainerLoader] uvést jako druhý argument `true`.
```php
$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í. .{data-version:3.2.7}
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ří. .{data-version:3.2.7}
- `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:
```php
$container = new $class(['host' => 'localhost']);
```
PSR-11 Container .{data-version:3.2.7}
--------------------------------------
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á:
```php
$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:
```shell
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 |application:bootstrapping#Konfigurace DI kontejneru].
{{maintitle: Nette DI Container: rychlý kompilovaný kontejner pro PHP}}
{{description: Nette DI generuje a automaticky aktualizuje kompilované kontejnery, které jsou extrémně rychlé a snadno se konfigurují v NEON. Podívejte se, jak ho použít.}}
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.