Version 2.4

DI: Services Configuration

Dependency Injection (DI) container is easily configured using NEON files. We'll talk about:

  • how to use parameters
  • how to add and setup services
  • how to include multiple configuration files

Configuration is usually written in NEON format. Have fun trying out the syntax at https://ne-on.org.

Parameters

You can define parameters which can then be used as part of service definitions. This can help to separate out values that you will want to change more regularly.

Use the parameters section of a config file to set parameters:

parameters:
    dsn: 'mysql:host=127.0.0.1;dbname=test'
    user: root
    password: secret

You can refer to foo parameter via %foo% elsewhere in any config file. They can also be used inside strings like '%wwwDir%/images'.

Parameters do not need to be flat strings, they can also contain array values:

parameters:
    mailer:
        host: smtp.example.com
        secure: ssl
        user: franta@gmail.com
    languages: [cs, en, de]

You can refer to single key as %mailer.user%.

If you use a string that starts with @ or has % anywhere in it, you need to escape it by adding another @ or %.

Services

The configuration file is place where we add definitions of our own services in section services. For example, this is definition of service named database which is PDO instance:

services:
    # in single line
    database: PDO(%dsn%, %user%, %password%)

    # or multi-lines
    database:
        factory: PDO(%dsn%, %user%, %password%)

    # or more multi-lines :-)
    database:
        class: PDO
        arguments: [%dsn%, %user%, %password%]

It generates factory method in DI container:

function createServiceDatabase()
{
    $service = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'secret');
    return $service;
}

In addition to creating a class instance, you can also call the method:

services:
    database: Database::create(root, password)

Result:

function createServiceDatabase()
{
    $service = Database::create('root', 'secret');
    return $service;
}

In this case, the Database::create() method must have a defined return type either by using an annotation @return or a PHP 7 type hint.

We obtain the service from the DI container using the getService():

$database = $container->getService('database');

Setup

We can call service methods or set properties:

services:
    database:
        factory: PDO(%dsn%, %user%, %password%)
        setup:
            - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)
            - $mode = 123

Result:

public function createServiceDatabase()
{
    $service = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'secret');
    $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $service->mode = 123;
    return $service;
}

In addition to strings and numbers, the method parameters can also be arrays, created objects or method calls:

services:
    analyser: My\Analyser(
        FilesystemIterator(%appDir%)
        [dryrun: yes, verbose: no]
        DateTime::createFromFormat('Y-m-d')
    )

Result:

public function createServiceAnalyser()
{
    return new My\Analyser(
        new FilesystemIterator('...'),
        ['dryrun' => true, 'verbose' => false],
        DateTime::createFromFormat('Y-m-d')
    );
}

Service Referencing

We refer to the service using at-sign and the name of the service, ie. @database:

services:
    database: PDO(%dsn%, %user%, %password%)
    articles:
        factory: Model\ArticleRepository(@database)
        setup:
            - setCacheStorage(@cache.storage)   # cache.storage is a system service

Result:

public function createServiceArticles()
{
    $service = new Model\ArticleRepository($this->getService('database'));
    $service->setCacheStorage($this->getService('cache.storage'));
    return $service;
}

We can call referenced service's methods, but for simplicity we write :: instead of :::

services:
    routerFactory: App\Router\Factory( @http.request::getMethod() )  # http.request je systémová služba
    router: @routerFactory::create()

Result:

public function createServiceRouterFactory()
{
    return new App\Router\Factory($this->getService('http.request')->getMethod());
}

public function createServiceRouter()
{
    return $this->getService('routerFactory')->create();
}

Autowiring

Autowiring is a great feature that can automatically pass services to the constructor and other methods, so we do not need to write them at all. It saves you a lot of time.

The example of articles can be simplified as follows:

services:
    articles:
        factory: Model\ArticleRepository
        setup:
            - setCacheStorage

Autowiring is driven by typehints, so ArticleRepository class must be defined as follows:

namespace Model;

class ArticleRepository
{
    public function __construct(\PDO $db)
    {}

    public function setCacheStorage(\Nette\Caching\IStorage $storage)
    {}
}

A service of the type such as PDO or Nette\Caching\IStorage must be exactly one in the container. If there were more, autowiring would not know which one to pass and throws an exception.

It is possible to place two services of the same type in the container and exclude one of them from the autowire. Autowiring will then work and will automatically pass the second service:

services:
    mainDatabase: PDO(%dsn%, %user%, %password%)

    tempDatabase:
        factory: PDO('sqlite::memory:)
        autowired: no

    articles: Model\ArticleRepository    # passes @mainDatabase to constructor

Autowiring can also be restricted to specific classes or interfaces:

class Parent
{}

class Child extends Parent implements Int
{}

class ParentDependent
{
    function __construct(Parent $obj)
    {}
}

class ChildDependent
{
    function __construct(Child $obj)
    {}
}
services:
    child:
        factory: Child
        autowired: [Parent, Int]

    parentDep: ParentDependent  # autowiring automatically passes the service @child

    childDep: ChildDependent  # throws exception!

Anonymous Services

Named services are particularly useful when we want to refer to them from other parts of the configuration file. If the service is no longer referenced by name, it does not need to be named. For anonymous services, use the following syntax:

services:
    - PDO('sqlite::memory:)

    -
        factory: Model\ArticleRepository
        setup:
            - setCacheStorage

However, even anonymous services can be referred to, instead of their name we use their type (class or interface):

services:
    router: @App\Router\Factory::create

We get the service from the DI container using the getByType():

$database = $container->getByType(PDO::class);

Multiple Configuration Files

Use includes section to add more configuration files.

includes:
    - parameters.php
    - services.neon
    - presenters.neon

If items with the same keys appear in configuration files, they will be overwritten or merged in the case of arrays. The later included file has higher priority. The file with the includes section has higher priority than included files.

config1.neon config2.neon result
items:
    - 1
    - 2
items:
    - 3
items:
    - 1
    - 2
    - 3

To prevent merging of a certain array use exclamation mark right after the name of the array:

config1.neon config2.neon result
items:
    - 1
    - 2
items!:
    - 3
items:
    - 3