Tvorba rozšíření pro Nette DI
Generování DI kontejneru kromě konfiguračních souborů ovlivňují ještě tzv rozšíření. Aktivujeme je
v konfiguračním souboru v sekci extensions
. Takto přidáme rozšíření reprezentované třídou
BlogExtension
pod názvem blog
:
extensions:
blog: BlogExtension
Každé rozšíření kompileru dědí od Nette\DI\CompilerExtension a může implementovat tři metody, které jsou postupně volány během sestavování DI kontejneru:
- loadConfiguration()
- beforeCompile()
- afterCompile()
loadConfiguration()
Tato metoda se volá jako první. Používá se k validaci konfiguračních parametrů a přidání služeb do kontejneru. K tomu slouží Nette\DI\ContainerBuilder:
class BlogExtension extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
$builder = $this->getContainerBuilder();
$builder->addDefinition($this->prefix('articles'))
->setFactory(App\Model\HomepageArticles::class, ['@connection'])
->addSetup('setLogger', ['@logger']);
}
}
Konvence je prefixovat služby přidané rozšířením jeho názvem, aby nevznikaly jmenné konflikty. To dělá
metoda prefix()
, takže pokud se rozšíření jmenuje blog
, služba ponese název
blog.articles
.
Konfigurace rozšíření
Rozšíření můžeme konfigurovat v sekci, jejíž název je stejný jako ten, pod kterým bylo rozšíření přidáno,
tedy blog
.
# stejné jméno jako má extension
blog:
postsPerPage: 10
comments: false
K definovaným hodnotám se v rozšíření dostaneme přes proměnnou $this->config
.
class BlogExtension extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
$config = $this->config;
// ['postsPerPage' => 10, 'comments' => false]
}
}
Výchozí hodnoty konfigurace
Pokud to lze, je užitečné podle principu konvence před konfigurací jednotlivým prvkům konfigurace poskytnout výchozí hodnoty, aby uživatel nemusel konfigurovat úplně vše.
Pro sloučení uživatelské konfigurace a výchozích hodnot můžeme použít metodu validateConfig()
. Jako
parametr jí předáme pole všech povolených klíčů a jejich výchozích hodnot, o které rozšíří
$this->config
. Zároveň pokud se v konfiguraci objeví klíče, které nejsou mezi povolenými, bude vyhozena
výjimka. Tak se předejde tomu, že někdo omylem splete název klíče v konfiguračním souboru.
blog:
postsPerPage: 10
class BlogExtension extends Nette\DI\CompilerExtension
{
private $defaults = [
'postsPerPage' => 5,
'comments' => true
];
public function loadConfiguration()
{
$this->validateConfig($this->defaults);
// ['postsPerPage' => 10, 'comments' => true]
$builder->addDefinition($this->prefix('articles'))
->setFactory(App\Model\HomepageArticles::class, ['@connection'])
->addSetup('setPostsPerPage', [$this->config['postsPerPage']]);
if (!$this->config['comments']) {
$builder->getDefinition($this->prefix('articles'))
->addSetup('disableComments');
}
}
}
Pokud potřebujeme přejmenovat službu, můžeme kvůli zachování zpětné kompatibility vytvořit alias s původním
názvem. Podobně to dělá Nette např. u služby routing.router
, která je dostupná i pod dřívějším názvem
router
.
$builder->addAlias('router', 'routing.router');
Načtení služeb ze souboru
Služby nemusíme vytvářet jen pomocí API třídy ContainerBuilder, ale i známým zápisem používaným v konfiguračním souboru NEON v sekci services.
services:
articles:
factory: MyBlog\ArticlesModel(@connection)
comments:
factory: MyBlog\CommentsModel(@connection, @extension.articles)
articlesList:
factory: MyBlog\Components\ArticlesList(@extension.articles)
Dodatečně služby načteme.
class BlogExtension extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
// načtení konfigurační parametrů
$config = $this->validateConfig($this->defaults);
$builder = $this->getContainerBuilder();
// načtení konfiguračního souboru pro rozšíření
$this->compiler->loadDefinitions(
$builder,
$this->loadFromFile(__DIR__ . '/blog.neon')['services'],
$this->name
);
}
}
beforeCompile()
Metoda se volá ve chvíli, kdy kontejner obsahuje všechny služby přidané jednotlivými rozšířeními v metodách
loadConfiguration
a taktéž uživatelskými konfiguračními soubory. V této fázi sestavování tedy můžeme
definice služeb upravovat nebo doplnit vazby mezi nimi. Pro vyhledávání služeb v kontejneru podle tagů lze využít metodu
findByTag()
, podle třídy či rozhraní zase metodu findByType()
.
class BlogExtension extends Nette\DI\CompilerExtension
{
public function beforeCompile()
{
$builder = $this->getContainerBuilder();
foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) {
$builder->getDefinition($serviceName)->addSetup('setLogger');
}
}
}
afterCompile()
V této fázi už je třída kontejneru vygenerována v podobě objektu ClassType, obsahuje všechny metody, které vytváří služby, a je připravena na zápis do cache. Výsledný kód třídy můžeme v této chvíli ještě upravit.
Samotný Nette Framework například přidává metodu initialize
. Tuto metodu pak sám vždy volá po
instancování kontejneru.
Ukážeme si kousek kódu, kde Nette Framework doplňuje do metody initialize
startování session a automatické
spouštění služeb, které mají tag run
.
class BlogExtension extends Nette\DI\CompilerExtension
{
public function afterCompile(Nette\PhpGenerator\ClassType $class)
{
$container = $this->getContainerBuilder();
// metoda initialize
$initialize = $class->getMethod('initialize');
// automatické startování session
if ($this->config['session']['autoStart']) {
$initialize->addBody('$this->getService("session")->start()');
}
// služby s tagem run musejí být vytvořeny po instancování kontejneru
foreach ($container->findByTag('run') as $name => $foo) {
$initialize->addBody('$this->getService(?);', [$name]);
}
}
}