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:
class Index extends MagentoFrameworkAppActionAction { /** * @var MagentoFrameworkUrl */ protected $urlHelper; /** * @param MagentoFrameworkAppActionContext $context * @param MagentoFrameworkUrl $url */ public function __construct( MagentoFrameworkAppActionContext $context, MagentoFrameworkUrl $url ) { $this->urlHelper = $url; parent::__construct($context); } /** * @return void */ public function execute() { echo $this->urlHelper->getBaseUrl(); } }
You could create a class that extends MagentoFrameworkUrl
, 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 MagentoFrameworkUrl
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.
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 DholdenDiExampleControllerExampleOneIndex
from the GitHub project:
namespace DholdenDiExampleControllerExampleOne; class Index extends MagentoFrameworkAppActionAction { /** * @var MagentoFrameworkUrl */ protected $original; /** * @var MagentoFrameworkUrl */ protected $modified; /** * @param MagentoFrameworkAppActionContext $context * @param MagentoFrameworkUrl $url1 * @param MagentoFrameworkUrl $url2 */ public function __construct( MagentoFrameworkAppActionContext $context, MagentoFrameworkUrl $url1, MagentoFrameworkUrl $url2 ) { $this->original = $url1; $this->modified = $url2; parent::__construct($context); } /** * @return void */ public function execute() { echo '
Original Base URL: ‘ . $this->original->getBaseUrl() . ‘
‘; echo ‘
Modified Base URL: ‘ . $this->modified->getBaseUrl() . ‘
‘; } }
This simple controller calls the getBaseUrl()
method from MagentoFrameworkUrl
. Notice that $url1 and $url2 both instantiate MagentoFrameworkUrl
, 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 DholdenDiExampleModelModifiedUrl
:
namespace DholdenDiExampleModel; /** * Returns modified URL */ class ModifiedUrl extends MagentoFrameworkUrl { /** * {@inheritdoc} */ public function getBaseUrl($params = []) { return 'http://newurl.dev/'; } }
This simple class extends MagentoFrameworkUrl
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:
DholdenDiExampleModelModifiedUrl ...
We’re replacing the third constructor argument of our controller (i.e. $url2) with the object DholdenDiExampleModelModifiedUrl
, and since this class extends MagentoFrameworkUrl
, 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 DholdenDiExampleControllerExampleTwoIndex
from our Github project:
namespace DholdenDiExampleControllerExampleTwo; class Index extends MagentoFrameworkAppActionAction { /** * @var MagentoCatalogModelCategory */ protected $categoryOriginal; /** * @var MagentoCatalogModelCategory */ protected $categoryModified; /** * @param MagentoFrameworkAppActionContext $context * @param MagentoCatalogModelCategoryFactory $categoryFactory1 * @param MagentoCatalogModelCategoryFactory $categoryFactory2 */ public function __construct( MagentoFrameworkAppActionContext $context, MagentoCatalogModelCategoryFactory $categoryFactory1, MagentoCatalogModelCategoryFactory $categoryFactory2 ) { $this->categoryFactory1 = $categoryFactory1; $this->categoryFactory2 = $categoryFactory2; $this->categoryOriginal = $categoryFactory1->create(); $this->categoryModified = $categoryFactory2->create(); parent::__construct($context); } /** * @return void */ public function execute() { $this->categoryOriginal->load(1); $this->categoryModified->load(1); echo '
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 MagentoCatalogModelCategory
using the factory’s create()
method. Our desire is to override the getName()
method of MagentoCatalogModelCategory
, but only in our specific example class and nowhere else.
Like in our first example, we have created a new class (DholdenDiExampleModelModifiedCategory
) that extends MagentoCatalogModelCategory
and overrides the getName()
method, but now our etc/di.xml looks a bit different:
... DholdenDiExampleModelModifiedCategoryFactory
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 (DholdenDiExampleModelModifiedCategory
) 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 MagentoCatalogModelCategoryFactory
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 DholdenDiExampleControllerExampleTwoIndexInterceptor::__construct() must be an instance of MagentoCatalogModelCategoryFactory, instance of DholdenDiExampleModelModifiedCategoryFactory 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 MagentoCatalogModelCategoryFactory
. 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:
namespace DholdenDiExampleModel; class ModifiedCategoryFactory extends MagentoCatalogModelCategoryFactory { /** * {@inheritdoc} */ public function __construct(MagentoFrameworkObjectManagerInterface $objectManager, $instanceName = 'DholdenDiExampleModelModifiedCategory') { parent::__construct($objectManager, $instanceName); } /** * {@inheritdoc} */ public function create(array $data = []) { return $this->_objectManager->create($this->_instanceName, $data); } }
Here we create a factory that extends MagentoCatalogModelCategoryFactory
. 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.