Passing Dependencies
Arguments, or ‘dependencies’ in DI terminology, can be passed to classes in the following main ways:
- Constructor injection
- Method injection (so-called setter injection)
- Property injection
- Using the
inject
method, annotation, or attribute
Let's demonstrate each variant with specific examples.
Constructor Injection
Dependencies are provided as constructor arguments at the time the object is instantiated:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
This approach is suitable for mandatory dependencies that the class absolutely requires for its operation, because without them, the instance cannot be created.
Since PHP 8.0, we can use a shorter notation (constructor property promotion), which is functionally equivalent:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
Since PHP 8.1, a property can be marked with the readonly
flag, which declares that the property's value will not
change after initialization:
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
The DI container passes dependencies to the constructor automatically using autowiring. Arguments that cannot be provided this way (e.g., strings, numbers, booleans) are specified in the configuration.
Constructor Hell
The term constructor hell describes a situation where a child class inherits from a parent class whose constructor requires dependencies, and the child class also requires dependencies. It must then accept and pass on the parent's dependencies as well:
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 arises when we want to change the constructor of the BaseClass
, for example, when a new dependency is
added. Then, it becomes necessary to modify all the constructors of the child classes as well. Which turns such a modification
into hell.
How can this be prevented? The solution is to prefer composition over inheritance.
So, we design the code differently. We will avoid abstract Base*
classes. Instead of MyClass
acquiring certain functionality by inheriting from BaseClass
, it will have
this 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 provided by calling a method that stores them in a private property. The common naming convention for these
methods is the set*()
pattern, hence they are called setters, but they can, of course, be named differently.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
This approach is suitable for optional dependencies that are not essential for the class's operation, as it's not guaranteed that the object will actually receive the dependency (i.e., that the caller will invoke the method).
At the same time, this method allows the setter to be called repeatedly to change the dependency. If this is undesirable, add a
check within the method, or since PHP 8.1, mark the $cache
property 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 the setup key. Here too, automatic dependency provision via autowiring is used:
services:
- create: MyClass
setup:
- setCache
Property Injection
Dependencies are provided by writing directly to a member property:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
This method is considered inappropriate because the member property must be declared as public
. Consequently, we
lose control over ensuring the passed dependency is actually of the required type (this was particularly true before PHP 7.4 type
hinting for properties), and we lose the ability to react to a newly assigned dependency with custom logic, for example, to
prevent subsequent modification. At the same time, the property becomes part of the class's public API, which might not be
intended.
Property assignment is defined in the DI container configuration in the setup section:
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
While the previous three approaches apply generally in all object-oriented languages, injection via method, annotation, or the
inject
attribute is specific to Nette presenters. They are discussed in a separate chapter.
Which Method to Choose?
- The constructor is suitable for mandatory dependencies that the class absolutely requires for its operation.
- The setter, conversely, is suitable for optional dependencies, or dependencies that might need to be changed later.
- Public properties are generally not recommended.