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(): PDO
{
    $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, secret)

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(): PDO
{
    $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(): MyService
{
    $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: true, verbose: false]
        DateTime::createFromFormat('Y-m-d')
    )

Result:

public function createServiceAnalyser(): My\Analyser
{
    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(): Model\ArticleRepository
{
    $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(): App\Router\Factory
{
    return new App\Router\Factory;
}

public function createServiceRouter(): IRouter
{
    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(): App\Router\Factory
{
    $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(): Model\ArticleRepository
{
    $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(): IRouter
{
    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: false                 # removes tempDb from autowiring

    articles: Model\ArticleRepository    # therefore passes mainDb to constructor

Narrowing of Autowiring

For individual services, autowiring can be narrowed down to specific classes or interfaces. The service is then passed to parameters (e.g. in constructor) whose typehint, in addition that matches service type, also matches the types specified in the settings.

Let's take an 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 therefore narrow down its autowiring to ChildClass:

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

Now parent is passed to parentDep constructor, as it is the only satisfactory object now. The child service is not autowired here anymore. Typehint, which is ParentClass, matches the service (ie child is a ParentClass), but it is not true that ParentClass is a ChildClass (see is_a).

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

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

autowired: [BarClass, FooInterface]

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

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

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

When we do not limit the child service, it will fit into the constructors of all FooDependent, BarDependent, ParentDependent and ChildDependent classes and autowiring will pass it there.

If we narrow child service's autowiring down to ChildClass with autowired: ChildClass (or self), autowiring will pass it only to the ChildDependent constructor, because only its typehint is a ChildClass.

If we limit it to ParentClass with autowired: ParentClass, autowiring will also pass it to the ParentDependent constructor, because its typehint will match too.

If we limit it to FooInterface, it will be autowired into the FooDependent constructor and still in ParentDependent (typehint ParentClass is a FooInterface) and ChildDependent, but not in BarDependent, because the BarInterface typehint is not a FooInterface.

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

If we have more services of the same type and one of them has the autowired option, this service becomes the preferred one:

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 of type PDO (ie mainDb and tempDb) that can be passed to the constructor, but it uses 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