Edit
Lang

DI Container Extensions

Configurator does not generate the code itself, that is the task of Nette\DI\Compiler and Nette\DI\ContainerBuilder classes. First configuration files are loaded and passed to Compiler. Add your own extension to Compiler via config.neon:

extensions:
    blog: MyBlogExtension

Each Compiler extension must extend Nette\DI\CompilerExtension and can implement three different methods that are called successively during the Container compilation.

CompilerExtension::loadConfiguration()

This method is called first and loads additional configuration files, creates methods using Nette\DI\ContainerBuilder and most importantly processes the application's configuration.

Config can contain a section bearing the same name as your extension. Using the last example, following lines can appear in your config file:

blog: # same name as your extension
    postsPerPage: 10
    comments: FALSE

Use getConfig() method in the extension to list its configuration.

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

Honoring the convention over configuration principle we can set default values and be able to use the application without explicitly setting anything. First argument of getConfig() accepts an array of default values, into which the config section (as mentioned above) will be merged.

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

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

Now we have the configuration in the $config array and can use it while creating the services. ContainerBuilder class allows you to use the same way of service description as NEON used in the configuration files.

$builder = $this->getContainerBuilder();

Create blog_articles service representing a model for handling and loading articles.

Convention is to prefix services by their name to avoid conflicts. Using prefix() method we can then access the service via $container->getService('blog.articles).

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

You can also create a component factory, pass it blog_articles service and set a number of posts per page option. How to use component factories will be shown soon.

$builder->addDefinition($this->prefix('articlesList'))
    ->setClass('MyBlog\Components\ArticlesList', [$this->prefix('@articles')])
    ->addSetup('setPostsPerPage', [$config['postsPerPage']])
    ->setShared(FALSE)->setAutowired(FALSE); // turns service into factory

We will also need a blog_comment class and pass it connection to database and blog_articles instance. If the comments are disabled we can reflect it using addSetup method:

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

if (!$config['comments']) { // optional disabling of commenting
    $comments->addSetup('disableComments');
}

Of course comments needs to be written and shown, so we add one more component factory – blog_commentsControl, that will allow us to list comments and to post new ones, unless they have been closed.

$builder->addDefinition($this->prefix('commentsControl'))
    ->setClass('MyBlog\Components\CommentsControl', [$this->prefix('@comments')])
    ->setShared(FALSE)->setAutowired(FALSE); // turns service into factory

This division into models and components is purely illustrative.

Loading additional configurations

If you prefer configuration files over extensions, you can move some of the definitions into separate configuration file.

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)

Load the file and set additional services

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

    // load additional config file for this extension
    $this->compiler->parseServices($builder, $this->loadFromFile(__DIR__ . '/blog.neon'), $this->name);

    // set a number of articles per page in the component
    $builder->getDefinition('blog_articlesList')
        ->addSetup('setPostsPerPage', [$config['postsPerPage']]);

    // optional disabling of commenting
    if (!$config['comments']) {
        $builder->getDefinition('blog_comments')
            ->addSetup('disableComments');
    }
}

Thanks to NEON syntax you now have cleaner container extension.

CompilerExtension::beforeCompile()

In beforeCompile phase we should not add any more services, however you can modify already existing ones or add relations between services (for example using tags).

CompilerExtension::afterCompile(Nette\PhpGenerator\ClassType $class)

In this phase the Container instance is already generated and contains all service methods and is ready to be stored into cache. Thanks to Nette\PhpGenerator\ClassType you can add your own code to the container to modify the service generation.

To inspire yourself take a look at Nette Framework's initialize method, which Nette adds to process some of the user settings. This method is always called after instantiation of the container.

Here is a piece of code of initialize where Nette adds a session start and autorun of services marked with run tag.

public function afterCompile(Nette\PhpGenerator\ClassType $class)
{
    $container = $this->getContainerBuilder();
    $config = $this->getConfig($this->defaults);

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

    // automatic session start
    if ($config['session']['autoStart']) {
        $initialize->addBody('$this->session->start();');
    }

    // services with run tag must be run after instantition of the container
    foreach ($container->findByTag('run') as $name => $foo) {
        $initialize->addBody('$this->getService(?);', [$name]);
    }
}

Methods' beforeCompile() and afterCompile() difference is that beforeCompile() has access to container and services while afterCompile() accesses its code (via Nette\PhpGenerator).