I found myself at a difficult position: There is this Magento class X that I would like to extend. But the constructor was huge: 20+ arguments. Therefore, creating an extend of the parent constructor would create a mess. I found a more innovative way to extend the class instead.
The conandrum of extending the parent constructor
First off, a class that has 20+ constructor arguments is not cleanly written. But sometimes you need to deal with legacy code (or simply badly written extensions, which was my use case). I needed to inject a new dependency. Normally, you can do this by extending upon the class, overriding the original class with your own class by adding a DI preference and then overriding the constructor.
For a very simple usecase (with only one constructor argument), this could like the following:
class LegacyExample
{
private $productRepository;
public function __construct(
ProductRepositoryInterface $productRepository
) {
$this->productRepository = $productRepository;
}
}
My new class NewExample
could look like the following:
class NewExample extends LegacyExample
{
private $productFactory;
public function __construct(
ProductRepositoryInterface $productRepository,
ProductFactory $productFactory
) {
parent::__construct($productRepository);
$this->productFactory = $productFactory;
}
}
By duplicating the original constructor argument $productRepository
I'm able to pass that on to the parent constructor to satisfy its needs. And then on top of it, I'm injecting my own dependency $productFactory
. If the constructor is this small, this approach is totally fine. But if the constructor arguments add up to be 20+ dependencies, the overriding constructor would be huge. But worse, the chances for bugs increase dramatically: What if the original class changes its constructor?
I didn't want that to happen.
Extending it anyway
My approach is actually quiet simple. First of all, make sure to extend the class. But next, don't duplicate the constructor at all. Only create a new property $productFactory
and add a declaration of the dependency $productFactory
. And make it public. If you're clever enough, you can see where this is going.
class NewExample extends LegacyExample
{
public $productFactory;
}
The hack: A DI plugin to inject the dependency
Now, let's create a DI plugin for this class by adding a new di.xml
to Magento (I'm keeping things simple here, no module, no namespaces, but you should get it):
<type name="NewExample">
<plugin name="inject_data" type="NewExamplePlugin" />
</type>
The new DI plugin class NewExamplePlugin
is where the magic takes place. It injects the $productFactory
that we were trying to inject earlier. And it also injects the NewExample
class (which is a dumb extend of LegacyExample
which we were trying to inject stuff into). In the plugin class, we combine the two things of our earlier example: The injectable and the place where it needs to be injected into.
class NewExamplePlugin
{
private $newExample;
private $productFactory;
public function __construct(
NewExample $newExample,
ProductFactory $productFactory
) {
$this->newExample = $newExample;
$this->productFactory = $productFactory;
}
}
Actually injecting it
Now this is the point where the scenario is a bit flawed. For a Magento 2 DI plugin to work, we need to zoom into a specific public method - for example getSomething()
. And the plugin then allows you to create a beforeGetSomething()
, afterGetSomething()
and aroundGetSomething()
method, depending on your scenario.
In our case, we only had one public method so far: The constructor. And a DI plugin is unable to hack that. So in my example, I will need to assume that the original class LegacyExample
actually has a public method. And we need to be pretty sure that this public method is going to be call, because otherwise our trick does not work. (in bold)
In my case, I'm just going to add a simple init()
method in the original code. It could be _construct()
as well (happy Magento).
class LegacyExample
{
private $productRepository;
public function __construct(
ProductRepositoryInterface $productRepository
) {
$this->productRepository = $productRepository;
}
public function init()
{
...
}
}
And then afterwards, we are able to modify the DI plugin again to modify this init()
method:
class NewExamplePlugin
{
private $newExample;
private $productFactory;
public function __construct(
NewExample $newExample,
ProductFactory $productFactory
) {
$this->newExample = $newExample;
$this->productFactory = $productFactory;
}
public function beforeInit()
{
$this->newExample->productFactory = $this->productFactory;
}
}
Done. We have used a DI preference to extend the original class only with a new public property. And we have used a DI plugin to inject a new object into the public property.
Caveats of this approach
The setup so far requires a DI plugin to hook into a public method - in my case init()
was hooked into by using beforeInit()
. This means that the init()
method definitely needs to be called - in runtime. What if you are not sure about this? Or what if the original class has 10 public methods (yikes) where you don't know which method will be called and which not? Of course, we can plugin into all 10 methods.
Now theoretically (and practically) you can overcome this by assigning the dependency via the constructor:
class NewExamplePlugin
{
public function __construct(
NewExample $newExample,
ProductFactory $productFactory
) {
$newExample->productFactory = $productFactory;
}
}
This works, but to my feeling it is a bit less correct: Instead of injecting the dependency in runtime (with a plugin method), we inserted it at compilation time (the constructor). Still it works. And it is kind of what a preference rewrite would be anyway.
Hope you like this hack as much as I do! ;o
Learn everything there is to learn about Magento 2 development with our courses, starting for backenders with our Magento 2 Backend Development I on-demand training
About the author
Jisse Reitsma is the founder of Yireo, extension developer, developer trainer and 3x Magento Master. His passion is for technology and open source. And he loves talking as well.