Targeted Dependency Injection
Targeted Dependency Injection

Targeted Dependency Injection

Published April 27, 2016 in Development
Magento Imagine 2016 Recap
April 19, 2016
Stealing Louie
Llamas hit the Black Market
May 20, 2016

Global Overrides

When setting up class overrides, you generally want them to apply across Magento as a whole. There are times, however, where you might want to override a class only in one specific location. We at Classy Llama were recently developing a module for Magento 2 that required a Magento controller to return a different URL from what was normally provided by that controller’s service dependency. We wanted this modification to apply to this one controller class and nowhere else.

 

In the following example controller, let’s say you wanted to change the way this controller’s URL helper functions by having the getBaseUrl() method return a different URL, and it is not possible to directly edit the controller or its dependency:

You could create a class that extends \Magento\Framework\Url, override the getBaseURL() function, and then set up a preference. You could even set up a plugin to modify the method’s behavior. Unfortunately for us, both of these options are essentially global overrides. Since the getBaseURL() method is used in many other places, overriding it globally would potentially break other core classes that rely on \Magento\Framework\Url as a dependency.

There is, however, a way to selectively modify functionality using constructor argument injection. As we will see in the following examples, constructor argument injection can replace a specific class’ dependencies with new or updated dependencies, and as long as the new class you create extends the required class specified in the constructor, Magento 2 will not complain about incorrect types. To illustrate the concept, I have written a small module that you can grab from GitHub here.

Dependency Injection

This article assumes familiarity with the concepts of dependency injection as implemented in Magento 2. For further reading, check out the Magento 2 developer documentation. Alan Kent has also written an excellent article about dependency injection that touches on concepts such as injectables, non-injectables, and factories.

Overriding a Dependency’s Method in a Single Location

Extending on our previous example, let’s take a look at how we can use constructor argument injection to selectively modify a class’ behavior. Here is a simple example using the class \Dholden\DiExample\Controller\ExampleOne\Index from the GitHub project:

Original Base URL: ‘ . $this->original->getBaseUrl() . ‘

‘; echo ‘

Modified Base URL: ‘ . $this->modified->getBaseUrl() . ‘

‘; } }

This simple controller calls the getBaseUrl() method from \Magento\Framework\Url. Notice that $url1 and $url2 both instantiate \Magento\Framework\Url, yet the output of the “ExampleOne” controller is as follows:

Original Base URL: http://example.dev/

Modified Base URL: http://newurl.dev/

While the exact same method is being called, the output is completely different, and this difference in functionality applies only to this controller class. To understand what is happening, let’s first look at \Dholden\DiExample\Model\ModifiedUrl:

This simple class extends \Magento\Framework\Url and overrides the parent function getBaseUrl(). Nothing too special here. We could at this point, setup a configuration preference for the class, but this would override this method across all of Magento 2. We shall instead use dependency injection to selectively override one constructor argument in one class. Let’s take a look at our etc/di.xml:

We’re replacing the third constructor argument of our controller (i.e. $url2) with the object \Dholden\DiExample\Model\ModifiedUrl, and since this class extends \Magento\Framework\Url, it fulfills the requirement as set in the constructor.

Taking it a Step Further

Great, but let’s say you wanted to do this with a factory. The same principle applies, but with one small caveat, to which we will get shortly. Let’s take a look at \Dholden\DiExample\Controller\ExampleTwo\Index from our Github project:

Name of Category 1: ‘ . $this->categoryOriginal->getName() . ‘

‘; echo ‘

Name of Category 2: ‘ . $this->categoryModified->getName() . ‘

‘; } }

This controller outputs the name of the category with an id of ‘1’, but like in our first example, the result of calling getName is different even though the same method is called. The primary difference between this example and the previous one is that we are calling factories in the controller’s constructor. We are then instantiating two new instances of \Magento\Catalog\Model\Category using the factory’s create() method. Our desire is to override the getName() method of \Magento\Catalog\Model\Category, but only in our specific example class and nowhere else.

Like in our first example, we have created a new class (Dholden\DiExample\Model\ModifiedCategory) that extends \Magento\Catalog\Model\Category and overrides the getName() method, but now our etc/di.xml looks a bit different:

Since we are using factories in the class’ constructor to create new instances of the category class, we need to override these factories in some manner. But we have created a new category class (\Dholden\DiExample\Model\ModifiedCategory) that modifies the getName() method. As you may have noticed, factories are not the classes themselves. They only create new instances of the classes whose methods we wish to selectively override.

We obviously need to pass a factory to the controller, so the logical approach is to append ‘Factory’ to the end of our constructor argument, i.e. our new class, in di.xml. Unfortunately, this will cause an error because the factory that will the auto-generated by Magento 2 will not fulfill the requirements of the constructor argument because it does not extend \Magento\Catalog\Model\CategoryFactory as the constructor has specified. If we try to view the controller as-is, the page will return the following error in our browser:

Recoverable Error: Argument 3 passed to Dholden\DiExample\Controller\ExampleTwo\Index\Interceptor::__construct() must be an instance of Magento\Catalog\Model\CategoryFactory, instance of Dholden\DiExample\Model\ModifiedCategoryFactory given...

From the error message, we learn that the factory created by Magento 2 does not extend the factory that is specified in the controller’s constructor. The solution is to have that factory inherit from \Magento\Catalog\Model\CategoryFactory. But how do we do this, since the factory was auto-generated? Fortunately for us, we can manually create our own factories and have Magento 2 use these over its own auto-generated classes. Take a look at the following factory located in the same folder as our new ModifiedCategory class:

Here we create a factory that extends \Magento\Catalog\Model\CategoryFactory. The CategoryFactory class will still be auto-generated by Magento 2, but since our factory now extends it, it will now fulfill the requirement of the controller’s constructor argument.

Conclusion

Constructor argument injection allows you to modify a class’ behavior, specifically methods given to you by class dependencies, without directly modifying the classes themselves and without changing their behavior system- or area-wide. I hope this concept will be useful to you in your future Magento 2 projects.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related postsView all
March 27, 2018

How to Create a Payment Method in Magento 2

Sometimes you might need more than the standard out-of-the-box payment method. Perhaps you have an agreement with a credit card processor and a solution for their […]
March 6, 2018

How to Build a Theme in Magento 2

When building a custom theme for Magento, it’s important to follow best practices for how the system is designed to be extended. Magento 2’s frontend is […]
February 12, 2018

Display Configurable Product Price Ranges in Magento 2

Today we’re going to look at the advantages of using configurable products and how we can update how their prices display on Magento 2 product listing […]
Most Recent PostsView all
November 29, 2018

Llama in the Sunset: David Alger

His stature has him towering well above the other llamas that surround him at Classy Llama. And his worn, size 13 cowboy boots show he’s more […]
October 17, 2018

Llamas and the Mobile eCommerce Optimization Initiative

The Mobile eCommerce Optimization Initiative has kicked off and Classy Llama is participating! The Magento Community has come together to create and test multiple conversion optimization […]
June 7, 2018

From Brick-and-Mortar To Online: How to Connect with a Niche Audience

As an eCommerce business, every penny counts. If you want to be successful, you have to carefully balance spending money to generate business and the profits […]