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:
        factory: 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 and static properties:

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

    myService:
        factory: MyService
        setup:
            - MyService::$foo = 2

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;
}

public function createServiceMyService()
{
    $service = new MyService;
    MyService::$foo = 2;
    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')
    );
}

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

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

$database = $container->getByType(PDO::class);
$repository = $container->getByType(Model\ArticleRepository::class);

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;
}

Even anonymous services can be referred to via @, instead of their name we use their type (class or interface). However, this does not usually have to be done thanks to autowiring.

services:
    articles:
        factory: Model\ArticleRepository(@Nette\Database\Connection)  # or @\PDO

Advanced syntax

The NEON format gives us extraordinary powerful syntax expression, with which you can write almost everything.

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

services:
    routerFactory: App\Router\Factory
    router: @routerFactory::create()

Result:

public function createServiceRouterFactory()
{
    return new App\Router\Factory;
}

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

Calling a method can be concatenated:

services:
    foo: FooFactory::build()::get()

Generates:

public function createServiceFoo()
{
    return FooFactory::build()->get();
}

Method calls can also be used in parameters. In addition to the methods, we can also call global functions, before its name we put :::

services:
    routerFactory: App\Router\Factory( Foo::bar() )   # static method calling
    setup:
        - setIp( @http.request::getRemoteAddress() )  # http.request is system service
        - setMode( ::getenv(NETTE_MODE) )             # global function getenv

Generates:

public function createServiceRouterFactory()
{
    $service = new App\Router\Factory( Foo::bar() );
    $service->setIp( $this->getService('http.request')->getRemoteAddress() );
    $service->setMode( getenv('NETTE_MODE') );
    return $service;
}

The form ClassName([parameters, ...]), which we usually use in the factory entry and which means creating an object, actually corresponds to the PHP syntax, except that we omit the new operator. We can also use this syntax anywhere else, for example as a parameter:

services:
    articles:
        factory: Model\ArticleRepository( PDO(%dsn%, %user%, %password%) )
        setup:
            - setCacheStorage( Nette\Caching\Storages\DevNullStorage() )

Generates:

public function createServiceArticles()
{
    $service = new Model\ArticleRepository( new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'secret') );
    $service->setCacheStorage( new Nette\Caching\Storages\DevNullStorage );
    return $service;
}

Even we can call the created object's methods:

services:
    router: App\Router\Factory()::create()
    # don't confuse with App\Router\Factory::create()

Generates:

public function createServiceRouter()
{
    return (new App\Router\Factory())->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)
    {}
}

Limited Autowiring

To use autowiring, there must be just one service for each type in the container. If there were more, autowiring would not know which one to pass and throw away an exception:

services:
    mainDatabase: PDO(%dsn%, %user%, %password%)
    tempDatabase: PDO('sqlite::memory:)
    articles: Model\ArticleRepository  # THROWS EXCEPTION

The solution would be to bypass autowiring and explicitly name the service (ie articles: Model\ArticleRepository(@mainDatabase)). Or we can make one of our services not autowired. Autowiring will then work and will automatically pass the second service:

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

    tempDatabase:
        factory: PDO('sqlite::memory:)
        autowired: no                    # removes tempDatabase from autowiring

    articles: Model\ArticleRepository    # therefore passes mainDatabase to constructor

Autowiring can also be restricted to specific classes or interfaces. We will show you on the example:

interface FooInterface
{}

class BarClass implements FooInterface
{}

class FooDependent
{
    function __construct(FooInterface $obj)
    {}
}

class BarDependent
{
    function __construct(BarClass $obj)
    {}
}

The first example without limitation of autowiring:

services:
    bar: BarClass
    fooDep: FooDependent  # autowiring passes the service bar to the constructor
    barDep: BarDependent  # autowiring passes the service bar to the constructor

In both cases, autowiring passes the bar service to the constructor because it satisfies BarClass and FooInterface. But now we limit it only to the interface:

services:
    bar:
        factory: BarClass
        autowired: FooInterface

    fooDep: FooDependent  # autowiring passes the service bar to the constructor
    barDep: BarDependent  # THROWS EXCEPTION

Autowiring at barDep throws the Nette\DI\ServiceCreationException exception because there is no service that would pass to the constructor. The bar service is resolved under FooInterface instead of BarClass. And vice versa, if we use autowired: BarClass, the service fooDep threw the exception.

The autowired key can include several classes and interfaces as array:

services:
    bar:
        factory: BarClass
        autowired: [BarClass, FooInterface]

Similarly, this will work in the case of inheritance, as shown in another example:

class ParentClass
{}

class ChildClass extends ParentClass
{}

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

class ChildDependent
{
    function __construct(ChildClass $obj)
    {}
}

We'll add the services into the configuration file with the child being limited to autowiring only for ChildClass (and descendants):

services:
    child:
        factory: ChildClass
        autowired: ChildClass   # you can use 'self'

    parentDep: ParentDependent  # THROWS EXCEPTION, the 'child' service can not be used
    childDep: ChildDependent    # autowiring passes the service 'child' to the constructor

In this case, autowired: ChildClass could be written as autowired: self as the self means service type.

Preferred Autowiring

In autowiring, we can also prefer certain services for certain types. We will use the above classes for the demonstration.

If we registered them all as services, autowiring would fail:

services:
    parent: ParentClass
    child: ChildClass
    parentDep: ParentDependent  # autowiring passes the service 'child' to the constructor
    childDep: ChildDependent    # THROWS EXCEPTION

The parentDep service throws the exception Multiple services of type ParentClass found: parent, child, because both parent and child fit into its constructor and autowiring can not make a clear decision on which one to choose.

For service child, we can limit its autowing to ChildClass, so it will not be passed to where ParentClass is expected, which we have already shown above. But now we also add service parent:

services:
    parent: ParentClass
    child:
        factory: ChildClass
        autowired: ChildClass

    parentDep: ParentDependent  # autowiring passes the service 'parent' to the constructor
    childDep: ChildDependent    # autowiring passes the service 'child' to the constructor

In the case of parentDep, the parent is passed to the constructor because it is now the only satisfactory object due to the restrictions defined in child. By limiting it, however, it has also become a preferred service for this type. Therefore, childDep does not throw the exception that there are two satisfactory services (ie child and parent) that can be passed to the constructor but use the preferred service, child.

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