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
The configuration is usually written in NEON format.
Parameters
You can define parameters that 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 a place where we add definitions of our own services in section services
. For example,
this is the definition of service named database
which is PDO
instance:
services:
# in a 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
- '$items[]' = 456
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;
$service->items[] = 456;
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(): Router
{
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(): Router
{
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
.
Inject Extension
The option inject: true
enables passing dependencies via variables with inject annotation and inject* methods.
services:
articles:
factory: App\Model\Articles
inject: true
namespace App\Model;
use Nette\Database\Connection;
class Articles
{
/** @var Connection @inject */
public $connection;
}
Or in PHP 8 using the attribute: (requires nette/di 3.0.6)
use Nette\Database\Connection;
use Nette\DI\Attributes\Inject;
class Articles
{
#[Inject]
public Connection $connection;
}
By default is inject
attribute enabled only for presenters.
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 a higher priority. The file with the includes
section has a higher priority than
included files.
config1.neon | config2.neon | result |
---|---|---|
|
|
|
To prevent merging of a certain array use exclamation mark right after the name of the array:
config1.neon | config2.neon | result |
---|---|---|
|
|
|