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 custom di.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!

Resources

Complete and Continue