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

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