Passing Dependencies
Arguments, or „dependencies“ in DI terminology, can be passed to classes in the following main ways:
- passing by constructor
- passing by method (called a setter)
- by setting a property
- by method, annotation or attribute inject
We will now illustrate the different variants with concrete examples.
Constructor Injection
Dependencies are passed as arguments to the constructor when the object is created:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
This form is useful for mandatory dependencies that the class absolutely needs to function, as without them the instance cannot be created.
Since PHP 8.0, we can use a shorter form of notation (constructor property promotion) that is functionally equivalent:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
As of PHP 8.1, a property can be marked with a flag readonly
that declares that the contents of the property will
not change:
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
DI container passes dependencies to the constructor automatically using autowiring. Arguments that cannot be passed in this way (e.g. strings, numbers, booleans) write in configuration.
Constructor Hell
The term constructor hell refers to a situation where a child inherits from a parent class whose constructor requires dependencies, and the child requires dependencies too. It must also take over and pass on the parent's dependencies:
abstract class BaseClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass extends BaseClass
{
private Database $db;
// ⛔ CONSTRUCTOR HELL
public function __construct(Cache $cache, Database $db)
{
parent::__construct($cache);
$this->db = $db;
}
}
The problem occurs when we want to change the constructor of the BaseClass
class, for example when a new
dependency is added. Then we have to modify all the constructors of the children as well. Which makes such a
modification hell.
How to prevent this? The solution is to prioritize composition over inheritance.
So let's design the code differently. We will avoid abstract Base*
classes. Instead of MyClass
getting some functionality by inheriting from BaseClass
, it will have that
functionality passed as a dependency:
final class SomeFunctionality
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass
{
private SomeFunctionality $sf;
private Database $db;
public function __construct(SomeFunctionality $sf, Database $db) // ✅
{
$this->sf = $sf;
$this->db = $db;
}
}
Setter Injection
Dependencies are passed by calling a method that stores them in a private properties. The usual naming convention for these
methods is of the form set*()
, which is why they are called setters, but of course they can be called
anything else.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
This method is useful for optional dependencies that are not necessary for the class function, since it is not guaranteed that the object will actually receive them (i.e., that the user will call the method).
At the same time, this method allows the setter to be called repeatedly to change the dependency. If this is not desirable, add
a check to the method, or as of PHP 8.1, mark the property $cache
with the readonly
flag.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if ($this->cache) {
throw new RuntimeException('The dependency has already been set');
}
$this->cache = $cache;
}
}
The setter call is defined in the DI container configuration in section setup. Also here the automatic passing of dependencies is used by autowiring:
services:
- create: MyClass
setup:
- setCache
Property Injection
Dependencies are passed directly to the property:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
This method is considered inappropriate because the property must be declared as public
. Hence, we have no control
over whether the passed dependency will actually be of the specified type (this was true before PHP 7.4) and we lose the ability
to react to the newly assigned dependency with our own code, for example to prevent subsequent changes. At the same time, the
property becomes part of the public interface of the class, which may not be desirable.
The setting of the variable is defined in the DI container configuration in section setup:
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
While the previous three methods are generally valid in all object-oriented languages, injecting by method, annotation or inject attribute is specific to Nette presenters. They are discussed in a separate chapter.
Which Way to Choose?
- constructor is suitable for mandatory dependencies that the class needs to function
- the setter, on the other hand, is suitable for optional dependencies, or dependencies that can be changed
- public variables are not recommended