DI: Rozšíření pro kontejner

Configurator sám však žádný kód negeneruje, o to se stará Nette\DI\Compiler a Nette\DI\ContainerBuilder. Nejdříve se načtou konfigurační soubory a předají Compileru. Do Compileru si můžeme připojit vlastní rozšíření v config.neon:

extensions:
    blog: MyBlogExtension

Každé rozšíření Compileru musí dědit od Nette\DI\CompilerExtension a může implementovat tři různé metody, které jsou postupně volány, během sestavování Containeru.

CompilerExtension::loadConfiguration()

Metoda je nad všemi rozšířeními volána jako první a je určena k načtení dodatečných konfiguračních souborů, vytvoření dalších služeb pomocí Nette\DI\ContainerBuilder a hlavně zpracování konfigurace aplikace.

V configu je možné uvést sekci, která se jmenuje stejně jako naše rozšíření. Pokud tedy budeme uvažovat předchozí příklad, mohlo by v konfigu přibýt například toto

blog: # stejné jméno jako má extension
    postsPerPage: 10
    comments: false

Nastavení si lze přečíst v proměnné $this->config v rozšíření.

class MyBlogExtension extends Nette\DI\CompilerExtension
{
    public function loadConfiguration()
    {
        dump($this->config);
        // [2] [ 'postsPerPage' => 10, 'comments' => false ]

Protože uznáváme princip konvence před konfigurací, nastavíme si výchozí hodnoty, aby aplikace fungovala i bez toho, že budeme něco nastavovat, a sloučíme je hodnotami z konfiguračního souboru.

class MyBlogExtension extends Nette\DI\CompilerExtension
{
    public $defaults = [
        'postsPerPage' => 5,
        'comments' => true
    ];

    public function loadConfiguration()
    {
        $this->config += $this->defaults;

Kompletní nastavení máme v poli $this->config, takže je použijeme pro služby, které vytvoříme. Použijeme k tomu ContainerBuilder, který umožňuje to stejné, jako zápis služby v konfiguračním souboru.

$builder = $this->getContainerBuilder();

Vytvoříme si jako příklad službu articles, což bude objekt třídy MyBlog\ArticlesModel, nad kterým ještě zavoláme metodu setLogger.

Konvence je, prefixovat služby v rozšíření jeho názvem, aby nevznikaly konflikty. Pomůže nám s tím metoda prefix().

$builder->addDefinition($this->prefix('articles'))
    ->setFactory('MyBlog\ArticlesModel', ['@connection'])
    ->addSetup('setLogger', ['@logger']);

Načítání dodatečné konfigurace

Pokud se nám nelíbí vytvářet všechny služby v rozšíření, můžeme jeho část přesunout do samostatného konfiguračního souboru.

services:
    articles:
        factory: MyBlog\ArticlesModel(@connection)

    comments:
        factory: MyBlog\CommentsModel(@connection, @extension.articles)

    articlesList:
        factory: MyBlog\Components\ArticlesList(@extension.articles)

Který načteme a dodatečně služby nastavíme

public function loadConfiguration()
{
    $this->config += $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);

    // počet článků na stránku v komponentě
    $builder->getDefinition($this->prefix('articlesList'))
        ->addSetup('setPostsPerPage', [$this->config['postsPerPage']]);

    // volitelné vypnutí komentářů
    if (!$this->config['comments']) {
        $builder->getDefinition($this->prefix('comments'))
            ->addSetup('disableComments');
    }
}

CompilerExtension::beforeCompile()

V této fázi sestavování už by neměly přibývat další služby. Můžeme ovšem upravovat existující a doplnit některé potřebné vazby mezi službami, například pomocí tagů.

CompilerExtension::afterCompile()

V této fázi už je třída Containeru vygenerována, obsahuje všechny metody, které vytváří služby a je připravena na zápis do cache. Díky api třídy Nette\PhpGenerator\ClassType můžeme přidávat vlastní kód do kontejneru a upravovat tak výsledný kód, který se stará o vytvoření služeb.

Samotný Nette Framework například přidává metodu initialize. Tuto metodu pak sám vždy volá po instanciová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.

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->session->start();');
    }

    // služby s tagem run musejí být spouštěny po vytvoření kontejneru
    foreach ($container->findByTag('run') as $name => $foo) {
        $initialize->addBody('$this->getService(?);', [$name]);
    }
}