Basics of dependency injection and its usage in Magento 2

One of the biggest changes in Magento 2 is the usage of dependency injection design pattern. With this pattern, a lot has been changed inside codebase, and many new things have been introduced. In this article, I will try to explain the very basics of this design pattern, and its implementation in Magento 2 to help those who are beginners in the field of dependency injection.

Let me first start by explaining the basic idea behind this design pattern. By following dependency injection, you should ask for required resources when your object is being created, instead of creating resources when they are required. This will allow class isolation and independent development, as well as unit testing due to ease of mocking required objects. Before continuing, let me show you an example:

class A
{
...
public function read()
{
$dbh = new DatabaseConnection(); // avoid this
$dbh->query('SELECT ...');
...
}
...
}

Previous example demonstrates simple class A with method read(). In this method, we require database connection object which is created in the first line of method. This is something most of you are used to, and it is something similar to what is used in Magento 1.x (with usage of factory pattern). The next example will implement the same logic, but following dependency injection pattern:

class A
{
protected $_dbh;
...
public function __construct(DatabaseConnection $connection)
{
$this->_dbh = $connection;
}
...
public function read()
{
$this->_dbh->query('SELECT ...');
...
}
...
}

In this example, database connection object has been passed to class A through constructor, instead of being created inside read() method. Even though this is not the only way of passing dependencies onto object, this implementation is used in Magento 2. So, what exactly have we achieved by this?

For starters, class A can be developed independently of class DatabaseConnection as we can always fake connection for purpose of development. It is independent of DatabaseConnection implementation, and it is much easier to test our code this way.

One of the downsides you have probably noticed is that the second snippet of the code is larger than the previous one. And that is true, for an example MagentoCatalogModelProduct has constructor that spreads across 52 lines of code. Also, if you try to debug this code, you will find it is a really difficult task. Luckily, this is where testing comes in hand.

For initialization of such large objects, dependency injection introduces dependency injection container, and in Magento 2 it is called ObjectManager. This container is responsible for resolving and creating dependencies upon object creation. Let’s examine how this works on a simple example:

class A
{
public function __construct(B $b)
{
$this->_b = $b;
}
}

When creating object from class A, the following would happen:

  1. ObjectManager->create(‘A’) is called
  2. Constructor of A is examined
  3. Class B is being created, and used for creating A

In those few steps, ObjectManager has created an object from class. But for the moment, let’s not keep it that simple, and imagine we have multiple implementations of class B. This raises the question, which implementation will be used? And the answer to that question can be found in one of the di.xml files, which are located in the following locations:

  • {moduleDir}/etc/{areaCode}/di.xml
  • {moduleDir}/etc/di.xml
  • app/etc/di/*.xml

Within those files, you can find/specify these settings:

Class definitions: Type and number of dependencies. Fortunately, Magento 2 uses constructor signature to compile this automatically.
Instance configuration: Definition on how to instantiate objects. This involves setting of types and virtualTypes which are out of scope of this article.
Abstraction-implementation mappings: These mappings allow you to choose preferred interface implementation. This is something similar to rewrite in Magento 1.x.

One more thing that is left here to explain is type of objects that are being injected. In Magento 2, they are separated into two groups: injectable, and non-injectable. Let me first start by explaining what non-injectable objects are. After that, injectables will be self-explanatory.

For an example, let’s examine the product page, where you wish to display a current product, and some related products. If you pass product model to your controller, first you will have to call load on that product to get the current product. But once that object is used, where will you get info about other products? So you are guessing that product model is one of the non-injectable objects. Actually, all objects that have some kind of identity, would be non-injectables: products, orders, cart items, users, …

In order to use non-injectable objects in your code, you have to request their factory. For example, if you are trying to load multiple products, your code will depend on product factory, and through that object you would call create() method with identity of the products you are trying to load. Proxies are primarily used for lazy loading of optional or expensive dependencies.

In order to have things a bit more complex, sometimes you really wish to depend upon non-injectable object itself, as they behave like singletons. Good example for this would be a CMS page.

To sum up, we have covered the very basics of dependency injection design pattern. There is a lot more to cover, but I hope that I managed to speed up this process for you. There are many materials on this subject out there, but if you are interested in the Magento specifics, a good starting point would be the official documentation.

In case you feel you need some extra help, we can offer you a detailed custom report based on our technical audit – feel free to get in touch and see what we can do for you!