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%)

    # with parameter names (the order does not matter)
    database:
        factory: PDO(dsn: %dsn%, username: %user%)

    # 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)
    {}
}

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:
    mainDb: PDO(%dsn%, %user%, %password%)
    tempDb: PDO('sqlite::memory:')
    articles: Model\ArticleRepository  # THROWS EXCEPTION, both mainDb and tempDb matches

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

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

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

    articles: Model\ArticleRepository    # therefore passes mainDb to constructor

Limited Autowiring

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

class ParentClass
{}

class ChildClass extends ParentClass
{}

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

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

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

services:
    parent: ParentClass
    child: ChildClass
    parentDep: ParentDependent  # THROWS EXCEPTION, both parent and child matches
    childDep: ChildDependent    # passes the service 'child' to the constructor

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 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:

services:
    parent: ParentClass
    child:
        factory: ChildClass
        autowired: ChildClass   # alternative: autowired: self

    parentDep: ParentDependent  # THROWS EXCEPTION, the 'child' can not be autowired
    childDep: ChildDependent    # 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.

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

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

autowired: [BarClass, FooInterface]

By limiting the autowiring for a certain type, we do not say it will be available purely for this type, but also for all descendants in the hierarchy. I.e. for child the autowired: ParentClass means the same as autowired: [ParentClass, ChildClass] because ChildClass is a descendant of ParentClass.

Example:

services:
    child:
        factory: ChildClass
        autowired: ParentClass

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

Let's try to add interfaces to the example:

interface FooInterface
{}

interface BarInterface
{}

class ParentClass implements FooInterface
{}

class ChildClass extends ParentClass implements BarInterface
{}

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

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

When service child is limited to FooInterface, it will be autowired to all services except barDep:

services:
    child:
        factory: ChildClass
        autowired: FooInterface

    fooDep: FooDependent        # passes the service child to the constructor
    barDep: BarDependent        # THROWS EXCEPTION, no service would pass
    parentDep: ParentDependent  # passes the service child to the constructor
    childDep: ChildDependent    # passes the service child to the constructor

Preferred Autowiring

In autowiring, we can also prefer certain services for certain types. This is done in the same way as when we limit autowiring. By limiting it, however, it has also become a preferred service for this type.

services:
    mainDb:
        factory: PDO(%dsn%, %user%, %password%)
        autowired: PDO    # makes it preferred

    tempDb:
        factory: PDO('sqlite::memory:')

    articles: Model\ArticleRepository

Therefore, articles does not throw the exception that there are two satisfactory services (ie mainDb and tempDb) that can be passed to the constructor but use the preferred service, mainDb.

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