The Beauty of Dependency Injection in Magento 2

Magento 2 still has some flaws, but its Dependency Injection system isn’t one of them. It was designed as a simple, elegant, and powerful system. In this article I will show you all about it and why I think it’s beautiful.

Klik hier voor de Nederlandse vertaling

An example

If you have no idea what Dependency Injection (DI for short) is, it’s best I start with an example from Magento. Pay special attention to the constructor.

As you can see in the constructor, it is not possible to create an EmailSender object without first instantiating a few other pre-existing objects: a message manager (for notices to the user), a logger (for messages to the system log) and an order sender (the actor in this class). These are all the classes that EmailSender needs to perform its function: its dependencies. The EmailSender class does not create any of its dependencies, nor does it call any static functions from another class. All classes it uses are injected (inserted) via the constructor. Now, when a new EmailSender is needed, the application code does not create these dependencies itself, it is part of the framework to create them dynamically. This, in short, is Magento’s implementation of dependency injection.

How does it work?

All dependencies of a class are listed as arguments to the constructor. When Magento needs an instance of a class or interface, it asks ObjectManager to create it from the fully qualified name. ObjectManager is what’s called the Dependency Injection Container. It instantiates the dependencies for a new object, and ensures that each dependency object is created only once. Note that the ObjectManager has no fixed list of dependencies for each class that needs to be updated manually whenever a class changes. The dependencies are read from the class dynamically, through reflection, and then cached.

ObjectManager checks if a preference is available for this requested type, and if so, replaces the requested type by the preferred type. It does this recursively. After it has determined a specific type for the requested type, it does the same for all the arguments available in the constructor of the class. When all arguments have been instantiated, a new object is created.

Note that preferences are optional. If there’s no preference available for a class name, the requested class itself will be instantiated. If you try to instantiate an interface when there’s no preference available this will lead to a fatal error. ObjectManager also detects circular preference relationships.

Hundreds of preferences are predefined by Magento. You can add your own preferences via di.xml files. Each module can have a global di.xml file (in etc) or an area specific file (in etc/frontend or etc/adminhtml). Let me show you a short example:

The original class or interface is shown as name in the type element. The preferred type is shown as type in the plugin element.

“Do I have to use it?”

DI is a Magento 2 idiom. While it is true that creating objects via DI is more work than creating them directly, get yourself in the habit of always using DI.

It is possible to create objects via PHP’s new operator, of course. And it is possible to call ObjectManager’s create() method directly. Keep this practice at a minimum. Here’s a list of arguments by Magento’s architect, Alan Kent, for you to consider. You will notice that Magento itself sometimes invokes ObjectManager without necessity. That does not mean that Magento approves of it.

Further more, using dependency injection is more beautiful than explicit instantiation. More on this later in the article.

Could Magento have done it differently?

Magento 1 made extensive use of a “God object” called Mage. It was the port to a large part of Magento’s functionality.

Magento could have kept this pattern. It is a form of ambient context, a fixed central place that serves all cross-cutting concerns of a system: the functions needed in almost every part of the system. In fact, the choice to let go of this pattern was likely one of the major reasons Magento 2 was built as an entirely new system.

Magento could have also have used a service locator object, a central registry implemented as a single object, that allowed dependencies to be added and removed at runtime. This locator object would then be passed around through the application. In Magento 2, all objects are built and linked before the application starts.

In place of constructor injection, Magento could have used setter injection or interface injection. Constructor injection is more robust then these two, because the dependencies are certain to exist before the application starts.

One of the restrictions of constructor injection is that is makes circular dependencies impossible. While this helps to decouple code, it is possible that it causes some challenges within the core of the framework itself. This may be one of  the reasons why DI is not used consistently throughout the framework, and that ObjectManager is sometimes called explicitly.

They could have chosen to force developers to create a di.xml entry for each new class they created. And they could have used class-level docblock information for its dependency information.

Finally, Magento’s developers could have chosen a very strict version of DI, that allows only interfaces for constructor argument types. They haven’t. Magento allows also abstract classes and even concrete classes. This is a pragmatic approach.

The Dependency Inversion Principle

Dependency Injection is an implementation of the Dependency Inversion Principle that says:

A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.

By injecting the (low-level) dependencies, and using interfaces or abstract classes as types, you open up each class to a wider range of possibilities, since each type can be replaced by different implementations. For Magento, an open framework, this is a natural fit.

DI in templates

While it is reasonably clear how to use DI in classes, it is less so in other places. Templates (.phtml files), for example. To use a foreign object in a template, you need to go through the block associated with the template. If the template belongs to a Magento core block, create a subclass of that block, then reference the block, and change its class. Yes, it’s a lot of work, there’s no denying. 🙂 Here’s an example, that provides a method getCustomerSession.

In the template you can then use

“What if I really want a new object?”

In some cases it is undesirable to share an object with other parts of the code. For example, in order to query products, you can use a product collection. It is implemented in \Magento\Catalog\Model\ResourceModel\Product\Collection. However, you know that you don’t want to share this collection object with some other parts of the request. Each part wants to add its own filters. You want a new Collection object. This is the reason factories were introduced. A \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory creates Collections. So, in stead of injecting a Collection, you inject a CollectionFactory. Then ask the factory to create a new collection for you.

Contexts

Some classes, through their central position in the framework, attract a lot of dependencies. When a group of dependencies is often used together, it is bundled into a context. Such a context can then be passed as a single entity via the constructor. Here’s an example of use of the product context:

To distinguish this context from the ambient context, we could call it a local context. It is not a service locator.

“Where do I find the dependencies I need?”

When you are developing a new class and you need Magento functionality, you have to search for it. Magento does not provide how-to’s for specific use cases. However, when you need a function from a module, check its api directory. This directory contains classes that form the service contracts of the module. These classes have been designed specifically to ease application programming, and are more stable than other module classes.

Service contracts, however, do not provide complete coverage of all of Magento’s functionality, yet. This will improve, but in the meanwhile Magento Stack Exchange is your best bet.

What makes it beautiful?

I use beauty in the sense of Beautiful Code, a book with stories on programming edited by Andy Oram & Greg Wilson (2007)

“I saw that programs could be more than just instructions for computers. They could be as elegant as well-made kitchen cabinets, as graceful as a suspension bridge, or as eloquent as one of George Orwell’s essays”

“For some, it lives in the small details of elegantly crafted software. Others find beauty in the big picture – in how a program’s structure allows it to evolve gracefully over time, or in the techniques used to build it.”

I think Magento 2’s Dependency Injection framework is beautiful because it allows you to treat any class of the framework as an independent entity, that can be created individually, without the presence of the entire framework. It creates extensibility for each class and facilitates object reuse within each request. It facilitates parallel development, enhances maintainability, and makes it much easier to test classes. But most of all, it was implemented consistently throughout the framework, as an architectural pattern. There’s beauty in that.

Want to know more?

Read Mark Seemann’s book on the subject: Dependency Injection in .Net. It gives you a thorough insight into the subject. Most of it applies to PHP as well.

You may check out the Magento 2 developer documentation on the subject. And, as an advanced topic, Alan Storm on virtual types.

Reacties:

  1. Sajid at 06:58

    Thanks for the amazing article, I have the situation where I have to pass DI on objects on multiple templates so instead of with multiple XML how we may use a generic to all blocks?

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.