1.2.2: Dependency Injection
Dependency Injection
Dependency injection is a crucial part of writing classes in your Magento modules, and the strategies that can be used to manipulate it are key to the framework's extensibility.
If you're not yet familiar with dependency injection, it is a design pattern by which a component receives its dependencies from an external source, instead of creating those objects itself. The specific mechanism Magento uses for this pattern is constructor injection.
Let's take a simple example of one class that depends on another:
class Component
{
public function execute(string $value)
{
$formatter = new \MyVendor\MyModule\Model\Formatter();
$value = $formatter->format($value);
...
}
}
Component
itself creates an instance of its dependency, Formatter
. With Magento's dependency injection, an instance is
instead provided to Component
:
class Component
{
private \MyVendor\MyModule\Model\Formatter $formatter;
public function __construct(
\MyVendor\MyModule\Model\Formatter $formatter
) {
$this->formatter = $formatter;
}
public function execute(string $value)
{
$value = $this->formatter->format($value);
...
}
}
This avoids the need for the code that creates an instance of Formatter
to reside directly in Component
. With this simple
design pattern in place, it becomes possible to, say, replace Formatter
with a custom implementation that extends the
original. As long as we somehow have control over the mechanism that is used to give Component
its dependencies, we can
do this without needing to touch Component
at all. This is just the beginning of the benefits dependency injection provides.
The Object Manager
A ubiquitous class called Magento\Framework\App\ObjectManager
is responsible for resolving, instantiating, and injecting
any classes referenced in constructors as seen above. The typehinting is the key. Simply provide a class name as a typehint
for any constructor argument in your classes, and the Object Manager will do the rest.
The Object Manager is how virtually every class in the application is instantiated. When one class is requested by the object
manager, it first evaluates all of that class's constructor arguments and resolves/instantiates objects for each of them
before it can instantiate the original requested class. And if any of those classes itself
has dependencies, those must be resolved and instantiated first, and so on and so on. The direct creation of an object with
new
, except in special scenarios, is not found outside the Object Manager itself.
While knowing a bit about the Object Manager helps us to understand the process of injection, the class itself should generally
be invisible to you in your day-to-day development. Except in some very special use cases, ObjectManager
should never
be referenced directly in your code; simply typehint your dependencies in your constructors and let the manager do its work!
(Make sure to take a mental note on this topic! It's not uncommon to find improper direct use of the Object Manager in
poorly written code tutorials or even in poor quality modules!)
Core Concept
You may notice that the constructor injection pattern always provides a single instance of an object - a "singleton". While there is a way to modify this behavior for specific classes, when a class is intended to be a state-keeping object with potentially multiple instances (e.g., an object representing a database record), the real solution is to inject a factory - a topic we'll cover later.
Depending on Interfaces
Our example class above directly injected a concrete class, and we even alluded to the fact that it could be replaced with a custom implementation as long as it extended the original. But we have a better pattern to follow: depending on interfaces. When interfaces - or "service contracts" as they are often called in Magento's architecture - are injected instead of concrete classes, then any class implementing those interfaces can be substituted by the Object Manager. This avoids entangling classes together through inheritance where it may not be desirable.
We've seen this in action in the example of dependency injection already present in our "Hello World" example! Take a look
at ViewModel\ProductView\Questions
in the SwiftOtter_ProductQuestions
module:
class Questions implements ArgumentInterface
{
private ScopeConfigInterface $scopeConfig;
public function __construct(
ScopeConfigInterface $scopeConfig
) {
$this->scopeConfig = $scopeConfig;
}
...
}
The class injects ScopeConfigInterface
, and this is the way the dependency is expressed virtually everywhere in the application
that needs this functionality. Thus, if a module had a reason to replace this "scope config" object with a class that behaves
differently, it need only provide one that implements the same interface.
While not all classes implement an interface, and so injecting concrete classes is sometimes necessary, it is a heavily emphasized convention throughout Magento's architecture. It's best to create your own service contracts for many types of classes, and you should always depend on interfaces instead of concrete classes when they exist.
Of course, this now begs the question: If a mere interface is injected in a constructor, how does the Object Manager know what class to create? Furthermore, how would this "replacement" we keep talking about be done?
di.xml
You knew XML configuration would come into this eventually! One of the most important and versatile config files in Magento's
architecture is di.xml
; this file can be included in the etc
directory of any module and can be used to effect a host
of clever configurations that tell the Object Manager how to do its job.
Note
The topic of
di.xml
is a more advanced subject, and one we'll only cover at a glance here. While it's likely to become a key tool for you over time, we're not going to tackle writing customdi.xml
configuration in this course. We discuss it here only to give a complete picture of how dependency injection works.
If you search for ScopeConfigInterface
in your project codebase and limit the results to di.xml
files, you'll find this
in app/etc/di.xml
:
<preference for="Magento\Framework\App\Config\ScopeConfigInterface"
type="Magento\Framework\App\Config" />
This is all that's required to tell the Object Manager what class to instantiate when that interface is required. And thanks to our constant friend - XML merging - it's trivial for one module to override the instructions of another in this regard.
That's just the beginning of what DI configuration can accomplish. It can also be used to specify custom implementations only in a specific class context, or even to supply array or scalar values! Here's a modified version of our previous example:
class Component
{
public function __construct(
\MyVendor\MyModule\Model\Formatter $formatter,
array $configValues = []
) {
...
}
}
Any module, even multiple modules, could provide the Object Manager with values to inject into $configValues
when
Component
is instantiated:
<type name="MyVendor\MyModule\Model\Component">
<arguments>
<argument name="configValues" xsi:type="array">
<item name="someKey" xsi:type="string">someValue</item>
</argument>
</arguments>
</type>
This makes for some very powerful extensibility patterns.
di.xml
has some other features too, including the creation of "virtual types" that define a unique object by virtue
of its configured arguments. Crucially, it can also be used to define plugins, which allow for the customization of
public methods without the need for something as heavy-handed as replacing an entire class (thus also allowing multiple
modules to modify the same method). Once you have the Magento basics under your belt, you're highly encouraged to make
plugins one of your next topics of study!