Edit
Lang

Rozšíření pro DI 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 zavoláním metody getConfig() v rozšíření.

class MyBlogExtension extends Nette\DI\CompilerExtension
{
    public function loadConfiguration()
    {
        $config = $this->getConfig();
        // [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. V prvním argumentu metody getConfig() můžeme poskytnout výchozí hodnoty, do kterých bude sloučena příslušná sekce konfiguračního souboru.

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

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

Nastavení máme v poli $config, takže je aplikujeme na 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 službu blog.articles, která bude reprezentovat model pro práci s články a jejich načítání.

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'))
    ->setClass('MyBlog\ArticlesModel', ['@connection']);

Dále si vytvoříme továrničku na komponentu, které předáme blog.articles a nastavíme počet zobrazených příspěvků na stránku. Jak používat továrničky na komponenty si ukážeme později.

$builder->addDefinition($this->prefix('articlesList'))
    ->setClass('MyBlog\Components\ArticlesList', [$this->prefix('@articles')])
    ->addSetup('setPostsPerPage', [$config['postsPerPage']])
    ->setShared(FALSE)->setAutowired(FALSE); // ze služby se stane továrnička

Budeme potřebovat i modelovou třídu pro komentáře blog.comments, které předáme připojení k databázi a blog.articles a pokud jsou vyplé komentáře, tak je zakážeme.

$comments = $builder->addDefinition($this->prefix('comments'))
    ->setClass('MyBlog\CommentsModel', ['@connection', $this->prefix('@articles')]);

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

Komentáře se musí také někde zobrazovat a psát, takže přidáme ještě jednu továrničku na komponentu blog.commentsControl, která nám bude komentáře zobrazovat a dovolí nám psát nové, pokud nebudou vyplé.

$builder->addDefinition($this->prefix('commentsControl'))
    ->setClass('MyBlog\Components\CommentsControl', [$this->prefix('@comments')])
    ->setShared(FALSE)->setAutowired(FALSE); // ze služby se stane továrnička

Toto rozdělení na modely a komponenty je pouze ilustrativní.

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:
        class: MyBlog\ArticlesModel(@connection)

    comments:
        class: MyBlog\CommentsModel(@connection, @blog.articles)

    articlesList:
        class: MyBlog\Components\ArticlesList(@blog.articles)

    commentsControl:
        class: MyBlog\Components\CommentsControl(@blog.comments)

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

public function loadConfiguration()
{
    $config = $this->getConfig($this->defaults);
    $builder = $this->getContainerBuilder();

    // načtení konfiguračního souboru pro rozšíření
    $this->compiler->parseServices($builder, $this->loadFromFile(__DIR__ . '/blog.neon'), $this->name);

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

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

Rozšíření se nám krásně vyčistilo a díky syntaxi Neon, jsou definice služeb mnohem lépe čitelné.

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(Nette\PhpGenerator\ClassType $class)

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();
    $config = $this->getConfig($this->defaults);

    // metoda initialize
    $initialize = $class->methods['initialize'];

    // automatické startování session
    if ($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]);
    }
}

Metody beforeCompile() a afterCompile() se liší pouze v tom, že jedna má přístup ke kódu výsledného Containeru a druhá ne.