DI: Tvorba rozšíření

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:

  1. loadConfiguration()
  2. beforeCompile()
  3. 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', ['@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', ['@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 $name) {
            $builder->getDefinition($name)->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]);
        }
    }
}