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:
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:
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:
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:
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:
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.
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.
The setter call is defined in the DI container configuration in the setup key. Here too, automatic dependency provision via autowiring is used:
Property Injection
Dependencies are provided by writing directly to a member property:
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:
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.