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.
This allows us to skip the vast majority of arguments when writing service definitions. Instead of:
services:
articles: Model\ArticleRepository(@database, @cache.storage)
Just write:
services:
articles: Model\ArticleRepository
Autowiring is driven by types, so ArticleRepository
class must be defined as follows:
namespace Model;
class ArticleRepository
{
public function __construct(\PDO $db, \Nette\Caching\Storage $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 either bypass autowiring and explicitly state the service name (i.e.
articles: Model\ArticleRepository(@mainDb)
). However, it is more convenient to disable autowiring of one services, or the first service prefer.
Disabled Autowiring
You can disable service autowiring by using the autowired: no
option:
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb:
create: PDO('sqlite::memory:')
autowired: false # removes tempDb from autowiring
articles: Model\ArticleRepository # therefore passes mainDb to constructor
The articles
service does not throw the exception that there are two matching services of type PDO
(i.e. mainDb
and tempDb
) that can be passed to the constructor, because it only sees the
mainDb
service.
Configuring autowiring in Nette works differently than in Symfony, where the autowire: false
option
says that autowiring should not be used for service constructor arguments. In Nette, autowiring is always used, whether for
arguments of the constructor or any other method. The autowired: false
option says that the service instance should
not be passed anywhere using autowiring.
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:
create: PDO(%dsn%, %user%, %password%)
autowired: PDO # makes it preferred
tempDb:
create: PDO('sqlite::memory:')
articles: Model\ArticleRepository
The articles
service does not throw the exception that there are two matching PDO
services (i.e.
mainDb
and tempDb
), but uses the preferred service, i.e. mainDb
.
Collection of Services
Autowiring can also pass an array of services of a particular type. Since PHP cannot natively notate the type of array items,
in addition to the array
type, a phpDoc comment with the item type like ClassName[]
must be added:
namespace Model;
class ShipManager
{
/**
* @param Shipper[] $shippers
*/
public function __construct(array $shippers)
{}
}
The DI container then automatically passes an array of services matching the given type. It will omit services that have autowiring turned off.
The type in the comment can also be of the form array<int, Class>
or list<Class>
. If you
can't control the form of the phpDoc comment, you can pass an array of services directly in the configuration using typed()
.
Scalar Arguments
Autowiring can only pass objects and arrays of objects. Scalar arguments (e.g. strings, numbers, booleans) write in configuration. An alternative is to create a settings-object that encapsulates a scalar value (or multiple values) as an object, which can then be passed again using autowiring.
class MySettings
{
public function __construct(
// readonly can be used since PHP 8.1
public readonly bool $value,
)
{}
}
You create a service by adding it to the configuration:
services:
- MySettings('any value')
All classes will then request it via autowiring.
Narrowing of Autowiring
For individual services, autowiring can be narrowed to specific classes or interfaces.
Normally, autowiring passes the service to each method parameter whose type the service corresponds to. Narrowing means that we specify conditions that the types specified for the method parameters must satisfy for the service to be passed to them.
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:
create: 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
The parentDep
service is now passed to the parentDep
service constructor, since it is now the only
matching object. The child
service is no longer passed in by autowiring. Yes, the child
service is still
of type ParentClass
, but the narrowing condition given for the parameter type no longer applies, i.e. it is no longer
true that ParentClass
is a supertype of ChildClass
.
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.
However, if we narrow its autowiring to ChildClass
using autowired: ChildClass
(or
self
), autowiring it only passes it to the ChildDependent
constructor, because it requires an argument
of type ChildClass
and ChildClass
is of type ChildClass
. No other type specified
for the other parameters is a superset of ChildClass
, so the service is not passed.
If we restrict it to ParentClass
using autowired: ParentClass
, autowiring will pass it again to the
ChildDependent
constructor (since the required type ChildClass
is a superset of
ParentClass
) and to the ParentDependent
constructor too, since the required type of
ParentClass
is also matching.
If we restrict it to FooInterface
, it will still autowire to ParentDependent
(the required type
ParentClass
is a supertype of FooInterface
) and ChildDependent
, but additionally to the
FooDependent
constructor, but not to BarDependent
, since BarInterface
is not a supertype of
FooInterface
.
services:
child:
create: 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