DI: konfigurace služeb
Dependency injection (DI) kontejner se snadno konfiguruje pomocí souborů NEON. Řekneme si:
- jak používat parametry
- jak přidat a nastavit služby
- jak vložit více konfiguračních souborů
Samotná konfigurace se obykle zapisuje ve formátu NEON.
Parametry
V konfiguraci můžete definovat parametry, které lze pak použít jako součást definic služeb. Čímž můžete zpřehlednit konfiguraci nebo sjednodit a vyčlenit hodnoty, které se budou měnit.
Pro nastavení parametrů použijte sekci parameters
konfiguračního souboru:
parameters:
dsn: 'mysql:host=127.0.0.1;dbname=test'
user: root
password: secret
Na parametr dsn
se odkážeme kdekoliv v konfiguraci zápisem %dsn%
. Lze je používat i uvnitř
řetězců jako '%wwwDir%/images'
.
Parametry nemusí být jen řetězce nebo čísla, mohou také obsahovat pole:
parameters:
mailer:
host: smtp.example.com
secure: ssl
user: franta@gmail.com
languages: [cs, en, de]
Na konkrétní klíč se odkážeme jako %mailer.user%
.
Chcete-li zapsat řetězec začínající znakem @
nebo obsahující %
, musíte znak
escapovat zdvojením na @@
nebo %%
.
Služby
Konfigurační soubor je místem, kam umísťujeme definice vlastních služeb. Slouží k tomu sekce services
.
Například takto může vypadat definice služby pojmenované database
, což bude instance PDO
:
services:
# v jednom řádku
database: PDO(%dsn%, %user%, %password%)
# nebo ve dvou řádcích:
database:
factory: PDO(%dsn%, %user%, %password%)
# s názvy parametrů (na pořadí nezáleží)
database:
factory: PDO(dsn: %dsn%, username: %user%)
# nebo ve třech řádcích :-)
database:
factory: PDO
arguments: [%dsn%, %user%, %password%]
Což vygeneruje tovární metodu v DI kontejneru:
function createServiceDatabase(): PDO
{
$service = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'secret');
return $service;
}
Kromě vytvoření instance třídy lze volat i metodu:
services:
database: Database::create(root, secret)
Vygenerovaný kód:
function createServiceDatabase()
{
$service = Database::create('root', 'secret');
return $service;
}
V tomto případě je nutné, aby metoda Database::create()
měla definovaný návratový typ buď pomocí
anotace @return
nebo type hintů v PHP 7.
Službu získáme z DI kontejneru metodou getService()
:
$database = $container->getService('database');
Setup
Nad vytvořenou službou můžeme volat metody nebo nastavovat hodnoty proměnným a statickým proměnným:
services:
database:
factory: PDO(%dsn%, %user%, %password%)
setup:
- setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)
- $mode = 123
- '$items[]' = 456
myService:
factory: MyService
setup:
- MyService::$foo = 2
Vygeneruje:
public function createServiceDatabase(): PDO
{
$service = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'secret');
$service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$service->mode = 123;
$service->items[] = 456;
return $service;
}
public function createServiceMyService(): MyService
{
$service = new MyService;
MyService::$foo = 2;
return $service;
}
Parametry metod mohou být kromě řetězců a čísel i pole, lze vytvářet i nové objekty nebo volat metody:
services:
analyser: My\Analyser(
FilesystemIterator(%appDir%)
[dryrun: true, verbose: false]
DateTime::createFromFormat('Y-m-d')
)
Vygeneruje:
public function createServiceAnalyser(): My\Analyser
{
return new My\Analyser(
new FilesystemIterator('...'),
['dryrun' => true, 'verbose' => false],
DateTime::createFromFormat('Y-m-d')
);
}
Anonymní služby
Pojmenování služeb je vhodné hlavně ve chvíli, kdy na ně chceme odkazovat z jiných částí konfiguračního souboru. Pokud na službu není odkazováno názvem, není ji potřeba pojmenovávat. Pro anonymní služby použijte následující syntax:
services:
- PDO('sqlite::memory:')
-
factory: Model\ArticleRepository
setup:
- setCacheStorage
Službu získáme z DI kontejneru metodou getByType()
:
$database = $container->getByType(PDO::class);
$repository = $container->getByType(Model\ArticleRepository::class);
Propojení služeb
Na jednotlivé služby se odkazujeme pomocí zavináče a názvu služby, takže například @database
:
services:
database: PDO(%dsn%, %user%, %password%)
articles:
factory: Model\ArticleRepository(@database)
setup:
- setCacheStorage(@cache.storage) # cache.storage je systémová služba
Vygeneruje:
public function createServiceArticles(): Model\ArticleRepository
{
$service = new Model\ArticleRepository($this->getService('database'));
$service->setCacheStorage($this->getService('cache.storage'));
return $service;
}
I na anonymní služby lze odkazovat přes zavináč, jen místo názvu uvedeme jejich typ (třídu nebo rozhraní). Tohle ovšem obvykle není potřeba dělat díky autowiringu.
services:
articles:
factory: Model\ArticleRepository(@Nette\Database\Connection) # nebo @\PDO
Pokročilá syntaxe
Formát NEON nám dává mimořádně silné výrazové prostředky, pomocí kterých můžete zapsat téměř cokoliv.
Nad odkazovanou službou lze volat metody, nicméně pro jednoduchost místo ->
používáme
::
.
services:
routerFactory: App\Router\Factory
router: @routerFactory::create()
Vygeneruje:
public function createServiceRouterFactory(): App\Router\Factory
{
return new App\Router\Factory;
}
public function createServiceRouter(): Router
{
return $this->getService('routerFactory')->create();
}
Volání method lze zřetězit za sebe:
services:
foo: FooFactory::build()::get()
Vygeneruje:
public function createServiceFoo()
{
return FooFactory::build()->get();
}
Volání metod lze použít i v parametrech. Krom metod je lze volat i globální funkce, před jejich názvem dáme čtyřtečku:
services:
routerFactory: App\Router\Factory( Foo::bar() ) # volání statické metody
setup:
- setIp( @http.request::getRemoteAddress() ) # http.request je systémová služba
- setMode( ::getenv(NETTE_MODE) ) # global function getenv
Vygeneruje:
public function createServiceRouterFactory(): App\Router\Factory
{
$service = new App\Router\Factory( Foo::bar() );
$service->setIp( $this->getService('http.request')->getRemoteAddress() );
$service->setMode( getenv('NETTE_MODE') );
return $service;
}
Zápis ve tvaru NázevTřídy([parametry, ...])
, který používáme obvykle v položce factory
a
který znamená vytvoření objektu, vlastně odpovídá zápisu v PHP jen s tím rozdílem, že vynecháváme operátor
new
. Tento zápis můžeme používat i kdekoliv jinde, třeba jako parametr:
services:
articles:
factory: Model\ArticleRepository( PDO(%dsn%, %user%, %password%) )
setup:
- setCacheStorage( Nette\Caching\Storages\DevNullStorage() )
Vygeneruje:
public function createServiceArticles(): Model\ArticleRepository
{
$service = new Model\ArticleRepository( new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'secret') );
$service->setCacheStorage( new Nette\Caching\Storages\DevNullStorage );
return $service;
}
Dokonce nad vytvořeným objektem můžeme rovnou volat metody:
services:
router: App\Router\Factory()::create()
# pozor, neplést s App\Router\Factory::create()
Vygeneruje:
public function createServiceRouter(): Router
{
return (new App\Router\Factory())->create();
}
Autowiring
Autowiring je skvělá vlastnost, která umí automaticky předávat do konstruktoru a dalších metod požadované služby, takže je nemusíme vůbec psát. Ušetří vám spoustu času.
Výše uvedený příklad služby articles
tak můžeme takto zjednodušit:
services:
articles:
factory: Model\ArticleRepository
setup:
- setCacheStorage
Autowiring se řídí podle typehintů, takže aby fungoval, musí být třída ArticleRepository
definována
asi takto:
namespace Model;
class ArticleRepository
{
public function __construct(\PDO $db)
{}
public function setCacheStorage(\Nette\Caching\Storage $storage)
{}
}
Aby bylo možné použit autowiring, musí pro každý typ být v kontejneru právě jedna služba. Pokud by jich bylo víc, autowiring by nevěděl, kterou z nich předat a vyhodil by výjimku:
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb: PDO('sqlite::memory:')
articles: Model\ArticleRepository # VYHODÍ VÝJIMKU, vyhovuje mainDb i tempDb
Řešením by bylo buď autowiring obejít a explicitně uvést název služby (tj
articles: Model\ArticleRepository(@mainDb)
). Nebo jej u jedné ze služeb můžeme vypnout. Autowiring pak bude
fungovat a automaticky bude předávat službu druhou:
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb:
factory: PDO('sqlite::memory:')
autowired: false # služba tempDb je vyřazena z autowiringu
articles: Model\ArticleRepository # tudíž předá do kontruktoru mainDb
Zúžení autowiringu
Jednotlivým službám lze autowiring zúžit jen na určité třídy nebo rozhraní. Služba se pak předá jen do parametrů (např. konstruktoru), jejichž typehint krom toho, že odpovídá typu služby, odpovídá i typům uvedeným v nastavení.
Ukážeme si to na příkladu:
class ParentClass
{}
class ChildClass extends ParentClass
{}
class ParentDependent
{
function __construct(ParentClass $obj)
{}
}
class ChildDependent
{
function __construct(ChildClass $obj)
{}
}
Pokud bychom je všechny zaregistrovali jako služby, tak by autowiring selhal:
services:
parent: ParentClass
child: ChildClass
parentDep: ParentDependent # VYHODÍ VÝJIMKU, vyhovují služby parent i child
childDep: ChildDependent # autowiring předá do konstruktoru službu child
Služba parentDep
vyhodí výjimku Multiple services of type ParentClass found: parent, child
,
protože do jejího kontruktoru pasují obě služby parent
i child
, a autowiring nemůže rozhodnout,
kterou z nich zvolit.
U služby child
můžeme proto zúžit její autowirování na typ ChildClass
:
services:
parent: ParentClass
child:
factory: ChildClass
autowired: ChildClass # lze napsat i 'autowired: self'
parentDep: ParentDependent # autowiring předá do konstruktoru službu parent
childDep: ChildDependent # autowiring předá do konstruktoru službu child
Nyní se do kontruktoru služby parentDep
předá služba parent
, protože teď je to jediný
vyhovující objekt. Službu child
už tam autowiring nepředá. Typehintu, kterým je ParentClass
, sice
služba odpovídá (tj. child
je typu ParentClass
), a už neplatí, že
ParentClass
je typu ChildClass
(vazba is_a).
U služby child
by bylo možné autowired: ChildClass
zapsat také jako
autowired: self
, jelikož self
je zástupné označení pro třídu aktuální služby.
V klíči autowired
je možné uvést i několik tříd nebo interfaců jako pole:
autowired: [BarClass, FooInterface]
Zkusme příklad doplnit ještě o rozhraní:
interface FooInterface
{}
interface BarInterface
{}
class ParentClass implements FooInterface
{}
class ChildClass extends ParentClass implements BarInterface
{}
class FooDependent
{
function __construct(FooInterface $obj)
{}
}
class BarDependent
{
function __construct(BarInterface $obj)
{}
}
class ParentDependent
{
function __construct(ParentClass $obj)
{}
}
class ChildDependent
{
function __construct(ChildClass $obj)
{}
}
Když službu child
nijak neomezíme, bude pasovat do konstruktorů všech tříd FooDependent
,
BarDependent
, ParentDependent
i ChildDependent
a autowiring ji tam předá.
Pokud její autowiring ale zúžíme na ChildClass
pomocí autowired: ChildClass
(nebo
self
), předá ji autowiring pouze do konstruktoru ChildDependent
, protože jen tento typehint je
typu ChildClass
.
Pokud jej omezíme na ParentClass
pomocí autowired: ParentClass
, předá ji autowiring navíc i do
konstruktoru ParentDependent
, protože typehint bude taktéž vyhovovat podmínce.
Pokud jej omezíme na FooInterface
, bude autowirovaná do konstruktoru FooDependent
a stále i do
ParentDependent
(typehint ParentClass
je typu FooInterface
) a
ChildDependent
, ale nikoliv BarDependent
, neboť typehint BarInterface
není typu
FooInterface
.
services:
child:
factory: ChildClass
autowired: FooInterface
fooDep: FooDependent # autowiring předá do konstruktoru child
barDep: BarDependent # VYHODÍ VÝJIMKU, žádná služba nevyhovuje
parentDep: ParentDependent # autowiring předá do konstruktoru child
childDep: ChildDependent # autowiring předá do konstruktoru child
Preference autowiringu
Pokud máme více služeb stejného typu a u jedné z nich uvedeme volbu autowired
, stává se tato služba
preferovanou:
services:
mainDb:
factory: PDO(%dsn%, %user%, %password%)
autowired: PDO # stává se preferovanou
tempDb:
factory: PDO('sqlite::memory:')
articles: Model\ArticleRepository
Proto služba articles
nevyhodí výjimku, že existují dvě vyhovující služby typu PDO
(tj.
mainDb
a tempDb
), které lze do konstruktoru předat, ale použije preferovanou službu, tedy
mainDb
.
Rozšíření Inject
Pomocí příznaku inject: true
se aktivuje předávání závislostí přes veřejné proměnné s anotací inject a metody inject*.
services:
articles:
factory: App\Model\Articles
inject: true
namespace App\Model;
use Nette\Database\Connection;
class Articles
{
/** @var Connection @inject */
public $connection;
}
Nebo v PHP 8 pomocí atributu:
use Nette\Database\Connection;
use Nette\DI\Attributes\Inject;
class Articles
{
#[Inject]
public Connection $connection;
}
V základním nastavení je inject
aktivováno pouze pro presentery.
Více konfiguračních souborů
V případě, že chceme pro nastavení používat více propojených konfiguračních souborů, můžeme je uvést v sekci
includes
.
includes:
- parameters.php
- services.neon
- presenters.neon
Pokud se v konfiguračních souborech objeví prvky se stejnými klíči, budou přepsány, nebo v případě polí
sloučeny. Později vkládaný soubor má vyšší prioritu než předchozí. Soubor se sekcí includes
má vyšší
prioritu, než v něm uvedené soubory.
config1.neon | config2.neon | výsledek |
---|---|---|
|
|
|
U polí lze zabránit slučování uvedením vykřičníku za názvem klíče:
config1.neon | config2.neon | výsledek |
---|---|---|
|
|
|
Související články na blogu
Services don't need names
I love Nette Framework's dependency injection solution. I really do. This post is here to share this passion, explaining why I think it is the best…
Dependency Injection: úvod (1/6)
Pamatujete si na svůj první program? Netuším sice, v jakém jazyce byl napsaný, ale kdyby to bylo PHP 7, nejspíš by vypadal nějak takto: function…
Ako automaticky zaregistrovať triedy do DIC
Veľa z vás to možno nevedelo, no Nette 3 má zabudované rozšírenie na automatické registrovanie tried do DI kontajnera. Takéto rozšírenie dokáže …
Nabušené DI srdce pro vaše aplikace
Jednou z nejzajímavějších částí Nette, kterou vychvalují i uživatelé jiných frameworků, je Dependency Injection Container (dále Nette DI). Podívejte…