A part of making Magento 2 have less issues with extension conflicts is to use better technologies to replace parts of a Magento site. By defining standard APIs to talk to different subsystems (see my previous post on the new Magento 2 Service Layer and Modularization) it becomes much easier to swap in and out different implementations. Dependency injection is the approach used by Magento 2 to make this possible. This article introduces how Magento 2 leverages dependency injection in the context of Magento’s modular architecture and why it is important. More detailed documentation can be found on the Magento 2 documentation wiki.
Disclaimer: I work with the Magento team, but this post contains personal opinions and perspectives and does not necessarily reflect those of eBay Inc.
Acknowledgements: This post borrows from Anton Kril’s (@AntonKril) presentation at the Magento Imagine 2014 conference.
What is Dependency Injection?
Dependency injection has become popular in recent years, particularly with the increase in prominence of unit testing and continuous integration. Dependency injection makes it easier to provide mock (pretend) implementations of classes to the class being tested. This makes it much easier to test a class in isolation, which is the idea of unit testing. One quote from Anton’s talk I liked was “Unit tests make creation of highly coupled code hard”.
However dependency injection is a lot more useful that just writing unit tests in the context of Magento. It is how a module can supply a new implementation of a defined API. This is not limited to the service layer – this is anywhere the dependency injection framework is used to wire application logic together.
Dependency injection to those who have not heard the name before may bring up some unsavory connotations, but is really a pretty simple concept. Instead of calling ‘new’ on a class name hard coded in your code, you pass objects or factories into the constructor of your class and use them instead. That is, the objects you depend on are provided to you (they are “injected” into your class) – you don’t create them yourself. That way, you can easily swap the implementation (class) of a referenced object. This is really useful in the context of Magento as it is common to want to swap in different implementations of logic. For example, a different shipping or tax calculator.
The choice of whether to pass in an object or a factory (a class with a method to create an object) boils down to whether your application needs to supply data (like a product ID) to the object when it is created. An injectable is an object that can be passed directly to the constructor (like a database connection instance) and a non-injectable is a class where a new instance is created based on data inside the code being executed (like a product ID). So non-injectables implies the usage of a factory which typically has a create() function to create a new object instance, taking the data as parameters.
Magento 2 uses constructor injection, where all injected objects or factories are provided when the object is constructed. Some dependency injection systems allow objects to be provided later using setter functions. A side effect of Constructor injection is you tend to see a particular pattern in classes:
- Each dependency has a constructor parameter declared (with PHP Doc of course!).
- Each dependency has a private field declared.
- Each dependency inside the constructor copies the constructor parameter to the private field.
If you have a large number of dependencies, this boilerplate code can get a bit tedious. But having too many dependencies is frequently a sign that the code may need refactoring, so this may actually be a good thing. It is not good to have too many dependencies.
(Side note: This is one thing I think is done better in the Scalar programming language – the declared constructor arguments are automatically turned into read-only fields. There is no need to declare them as fields and copy the constructor parameter list to fields. This is kinda cool – it certainly saves a lot of boilerplate text.)
Why Yet Another Dependency Injection Framework?
The Magento dependency injection framework has been designed to meet the specific needs of Magento. Existing dependency injection frameworks do not, for example, understand the Magento module architecture. Magento is not recommending its dependency injection framework be considered as a generic framework on other projects. There are other frameworks that do that already. The Magento 2 dependency injection framework is custom designed to support the Magento 2 needs. Setting up Magento dependency injection is pretty simple, so learning how to use it is not a big deal.
Whether Magento should have built “yet another dependency injection framework” will probably be a talking point in some circles until Magento 3! To me its a non-issue – dependency injection frameworks are not that complex (once built, they don’t have much maintenance overhead – its not like an image manipulation library). There is nothing stopping developers using another dependency injection framework for their own code. What it boils down to is the Magento dependency injection framework was custom designed to wire Magento modules together and do a good job of solving the issues specific to Magento.
Configuration Files (di.xml)
To connect all the different objects together, a di.xml file is used. In Magento 2, each module has a di.xml file in the etc directory. Each module can also have a di.xml file in an ‘area’ directory under etc. This allows configuration specific say to the admin interface or web API. It is common for each the di.xml to be fairly small for a module, although some more complex modules do have larger di.xml files.
The order in which modules are loaded is important when it comes to di.xml files. This allows an extension to override a dependency declared by a previous module. The final result is merged from all the di.xml files of all loaded modules.
What Goes in a di.xml File
The di.xml file declares which classes to instantiate and provide to other objects. The following features are supported:
- Constants for configuration of objects.
- Ability to specify what class to instantiate to pass to the constructor of other classes, including if a singleton should be used or not (a single globally shared instance for all references).
- Ability to defer creation of dependent objects until they are accessed by using automatically created proxies, just by adding a suffix of \Proxy to the class name in the di.xml file.
- Automatic creation of factories. A new class with single create() method is created which accepts the missing arguments required to create an instance of the class.
- Type preferences, which allows dependencies to be declared based on interface types, then other modules to provide the implementation to use for the interface type. This is particularly useful to swap in different implementations of an interface.
- Plugins (described later).
The following is a sample fragment of a di.xml file. The first <type> element defines that two objects should be passed as arguments to the constructor of Magento\Theme\Model\Config. The second <type> element passes a PHP array of constants (configuration settings) to the constructor of Magento\Theme\Model\Uploader\Service. The last <type> element is an example of passing a proxy to the constructor of Magento\Theme\ModelWysiwyg\Storage. No other configuration or code is required to cause the proxy to be created.
<config xmlns:xsi="..." xsi:noNamespaceSchemaLocation="..."> <type name="Magento\Theme\Model\Config"> <arguments> <argument name="configCache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> <argument name="layoutCache" xsi:type="object">Magento\Framework\App\Cache\Type\Layout</argument> </arguments> </type> <type name="Magento\Theme\Model\Uploader\Service"> <arguments> <argument name="uploadLimits" xsi:type="array"> <item name="css" xsi:type="string">2M</item> <item name="js" xsi:type="string">2M</item> </argument> </arguments> </type> .... <type name="Magento\Theme\Model\Wysiwyg\Storage"> <arguments> <argument name="helper" xsi:type="object">Magento\Theme\Helper\Storage\Proxy</argument> </arguments> </type> </config>
In the case of the proxy object, the proxy dependency is used to defer the creation of the specified type. If the object is not used (e.g. none of the public methods are called), then the object will not be created. This can be useful if the object is expensive to create. It can also be used to help ensure objects are instantiated in the right order.
The following is an example of using type preferences. This example provides the default implementation class for the specified interface. Any dependency using the interface will have the class plugged in.
<config> <preference for="Magento\Framework\Event\InvokerInterface" type="Magento\Framework\Event\Invoker"/> </config>
A complete set of examples can be found in the Magento 2 documentation wiki. The important aspect is that each module can declare its own fragment of the dependency graph. By writing the code of modules to have interfaces for internal APIs, it is easy to provide a default implementation of an interface while allowing other modules to be loaded and override that default implementation. Extension developers are recommended to get a good understanding of the different constructs available and when is best to use each one.
Method Interception with Plugins
The di.xml file is also how plugins are declared. This is a powerful feature that allows interception of calls to public methods of classes. This can be used to replace the Magento 1 practice of class overriding, which was a common source of extension conflicts as if a class was overridden more than once only one class rewrite survived.
The Magento 2 dependency injection framework allows an existing public method of a class to be extended with:
- An additional function called before the public method (the input parameters to the method are available for access)
- An additional function called after the public method returns (the input parameters and return value are available)
- An additional function called around the public method. In this case, the plugin method is called with the arguments plus a closure allowing the original method to be called. This allows the plugin to change arguments, return values, or use condition logic to control whether the public method is called at all.
Note that plugins on the same public method nest, and so different modules can define a plugin on a method and all plugins will be called.
It is worth noting that a plugin declared on an interface takes effect for all classes that implement that interface. This allows an API to be defined as an interface, other modules can declare plugins for methods of that interface, and a completely different module can provide the implementation of the interface. This is an example of where the dependency injection framework in Magento 2 has been designed especially for the Magento module architecture.
<config> <type name="Magento\Framework\Url"> <plugin name="my_plugin" type="My\Module\Url\Plugin"/> </type> </config>
Dependency injection in Magento 2 is one of the key technologies that will be interesting to extension developers. It provides a way to cleanly provide replacement implementations of internal APIs, or extend existing implementations with plugins. Avoiding use of class rewrites is a further benefit, reducing the number of extension conflicts.