background

May 18, 2019

Extending Magento 2 classes by composition

Yireo Blog Post

With a Magento 2 DI preference, you can rewrite one class to another: However, once you have your new preferred class in place, you'll still want to build on top of the original class to re-use all of its (public and protected) methods. It is easy to do this by extending the original class. However, composition offers a much neater way.

Why not extend the original class?

First of all, why not extend the original class? Well, it depends a bit on the class you are extending. Let's say you are extending a class that has zero dependencies. Then its constructor is pretty much empty (or totally empty or not existing). You can easily create your own constructor and add the dependencies of your choice to it.

However, once you want to extend upon a more complex class - let's say a ProductRepository - then you'll find that the original constructor already has many dependencies. At the time of writing the ProductRepository constructor has 24 arguments. And extending its constructor also means duplicating all of these constructor arguments in your own constructor, simply to pass them through to your parent. This clutters the code.

Disclaimer

Please note that Magento 2 offers more ways to solve this issue. Instead of rewriting the class (as within this example), a more solid approach is to use interceptors. Alternatively, you can maybe solve things using a DI type. Preference rewrites might lead to conflicts, once multiple extensions try to do the same thing.

Composition over inheritance

The basic principle of composition is that we solve our dependencies through composition, instead of extending upon the original class. So let's simply build this step-by-step.

Adding a DI preference rewrite

We'll introduce a new class first:

namespace Yireo\ExampleRewriteComposition;

class ImprovedProductRepository
{
}

Now, we want to make sure that Magento calls upon this class, not the original ProductRepository, which we can accomplish through a XML preference:

<preference for="Magento\Catalog\Api\ProductRepositoryInterface" type="Yireo\ExampleRewriteComposition\ImprovedProductRepository"/>

Of course, this means that our new ImprovedProductRepository also needs to implement this original interface:

namespace Yireo\ExampleRewriteComposition;

use Magento\Catalog\Api\ProductRepositoryInterface;

class ImprovedProductRepository implements ProductRepositoryInterface
{
}

At this moment, our code is broken, because we have not implemented the methods of the interface yet.

Injecting the original class

So how to implement all of those methods? Well, here comes the main point: We know that all methods in the interface are public (because that's how interfaces work). Therefore, we can call upon the original methods of the old class (because they are public). Instead of cluttering our own code by extending from the original class, we inject the original class into our constructor:

namespace Yireo\ExampleRewriteComposition;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\ProductRepository;

class ImprovedProductRepository implements ProductRepositoryInterface
{
	public function __construct(ProductRepository $original) {
		$this->original = $original;
	}
}

Let's implement the original methods

Once, this internal variable $original contains a reference to the original repository. The interface that we implement enforces us to implement the methods of that interface. Instead of writing those methods ourselves, we simply route it back to the $original object:

public function save(ProductInterface $product, $saveOptions = false)
{
    return $this->original->save($product, $saveOptions);
}

Once we do this for every method in the interface, we are done with the new implementation.

Undocumented methods

It could be that the actual ProductRepository model contains more methods than the ones defined in the interface. This is common: Not all methods automatically fall under the backwards compatibility standards of Magento and are marked as API. Some methods are legacy.

To make sure our new composed repository doesn't generate an error, when calling upon those unknown methods, we can simply generate a magic method __call method, that intercepts the unknown method and checks whether the original repository model contains this method:

public function __call(string $name, array $arguments)
{
    if (method_exists($this->original, $name)) {
        return $this->original->$name($arguments);
    }

    trigger_error('Call to undefined method ' . __CLASS__ . '::' . $name . '()', E_USER_ERROR);
}

This works of course only for the protected and the public methods, not for the private methods. But you can not use the original private methods in your own code either.

Adding new features

From this point onwards, we can extend our new class. We can add new methods to it. We can add new dependencies. And the code doesn't clutter.

Fun fact: You can even use a method name that corresponds with a method of the original class. As long as the new method doesn't collide with the interface, all is fine.

Adding strict typing? No.

One experiment I tried to run was to make my own methods more strict. However, you can't. Your own class needs to comply with the signature of the methods in the interface. You can loosen up things, but you can't tighten things.

This would only work if you would also replace the original interface with a better one. But that's not possible. That's the reason why we use a DI preference.

Recap: This leads to better code

You might say that this requires more work. But I would disagree. Duplicating a complex constructor leads to errors. The more because the original ProductRepository class does not fall under the API standards of Magento - any version increment might bring in breaking changes. Instead, we have duplicated methods that were documented in the API interface.

Full example at GitHub

The full source code of this example can be found on GitHub: yireo-training/magento2-example-rewrite-composition

Posted on May 18, 2019

About the author

Author Jisse Reitsma

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.

Sponsor Yireo

Looking for a training in-house?

Let's get to it!

We schrijven niet te commerciële dingen, we richten ons op de technologie (waar we dol op zijn) en we komen regelmatig met innovatieve oplossingen. Via onze nieuwsbrief kun je op de hoogte blijven van al deze coolness. Inschrijven kost maar een paar seconden.

Do not miss out on what we say

This will be the most interesting spam you have ever read

We schrijven niet te commerciële dingen, we richten ons op de technologie (waar we dol op zijn) en we komen regelmatig met innovatieve oplossingen. Via onze nieuwsbrief kun je op de hoogte blijven van al deze coolness. Inschrijven kost maar een paar seconden.