Overriding classes in Magento 2

Compared to its previous version, Magento 2 came out with a new concept of dependency injection where classes inject dependencies (different objects) for an object instead of that object manually creating them internally. That way overriding and manipulating with classes is much easier and allows us more ways of extending the native functionalities.

Which dependencies have to be injected in classes are controlled by the di.xml file. Each module can have a global and area-specific di.xml file that can be used depending on scope. Paths for module di.xml files:

<moduleDir>/etc/di.xml
<moduleDir>/etc/<area>/di.xml

It’s important to note that there are no more differences between overriding block, model, helper, controller or something else. They are all classes that can be overridden. We’ll go through three different ways of extending native Magento classes and methods.

Class preference

Let’s call this the old-fashioned way of overriding classes that we got used to, but slightly different. All the classes are defined by their interfaces and configured by di.xml files. There’s an abstraction-implementation mapping implemented when the constructor signature of a class requests an object by its interface. That means that interfaces should be used, where available, and the mapping will tell which class should be initiated.

Let’s take a look at how catalog product class is defined in the di.xml file of the Catalog module:

<config>
    <preference for="MagentoCatalogApiDataProductInterface" type="MagentoCatalogModelProduct" />
</config>

To override MagentoCatalogModelProduct class all we need is to define our preference and to create a file that will extend the original class:

<config>
    <preference for="MagentoCatalogApiDataProductInterface" type="InchooCatalogModelProduct" />
</config>
<?php
namespace InchooCatalogModel;
 
class Product extends MagentoCatalogModelProduct
{
// code
}

To make sure we have the right order of module dependencies, etc/module.xml should have defined module sequence for Magento_Catalog in our example.

 Plugins

Rewriting by class preference can cause conflicts if multiple classes extend the same original class. To help solve this problem a new concept of plugins is introduced. Plugins extend methods and do not change the class itself as rewriting by class preference does, but intercept a method call before, after or around its call.

Plugins are configured in the di.xml files and they are called before, after or around methods that are being overridden. The first argument is always an object of the observed method’s name followed by arguments of the original method.

As an example we’re going to extend a few methods from the catalog product module. This is how the di.xml file would look like:

<config>
    <type name="MagentoCatalogApiDataProductInterface">
        <plugin name="inchoo_catalog_product" type="InchooCatalogPluginModelProduct" />
    </type>
</config>
Before method

Before plugin is run prior to an observed method and has to return the same number of arguments  in array that the method accepts or null – if the method should not be modified. Method that’s being extended has to have the same name with prefix “before”.

<?php
namespace InchooCatalogPluginModel;
 
class Product
{
    public function beforeSetPrice(MagentoCatalogModelProduct $subject, $price)
    {
        $price += 10;
        return [$price];
    }
}
After method

After methods are executed after the original method is called. Next to a class object the method accepts one more argument and that’s the result that also must return. Method that’s being extended has to have the same name with prefix “after”.

<?php
namespace InchooCatalogPluginModel;
 
class Product
{
    public function afterGetName(MagentoCatalogModelProduct $subject, $result)
    {
        $result .= ' (Inchoo)';
        return $result;
    }
}
Around method

Around methods wrap the original method and allow code execution before and after the original method call. Next to a class object the method accepts another argument receives is callable that allows other plugins call in the chain. Method that’s being extended has to have the same name with prefix “around”.

<?php
namespace InchooCatalogPluginModel;
 
class Product
{
    public function aroundSave(MagentoCatalogModelProduct $subject, callable $proceed)
    {
        // before save
        $result = $proceed();
        // after save
 
        return $result;
    }
}

Using plugins looks like an ideal solution for overriding methods, but it comes with limitations. Plugins cannot be used for all types of methods and other solutions will have to be looked for when trying to extends the following methods:

  • Objects that are instantiated before MagentoFrameworkInterception is bootstrapped
  • Final methods
  • Final classes
  • Any class that contains at least one final public method
  • Non-public methods
  • Class methods (such as static methods)
  • __construct
  • Virtual types

Constructor arguments

di.xml configures which dependencies will be injected into a class what means that they can be controlled and changed to something that will be useful for us. If a change does not need to be global, but for a specific class, instead of overriding the whole class or creating plugins for different methods we’re able to configure arguments that the class receives.

Type configuration

One of the arguments that catalog product module receives is a helper MagentoCatalogHelperProduct $catalogProduct. With di.xml we can configure to use our helper instead:

<config>
    <type name="MagentoCatalogApiDataProductInterface">
        <arguments>
            <argument name="catalogProduct" xsi_type="object">InchooCatalogHelperProduct</argument>
        </arguments>
    </type>
</config>

Different argument types are allowed, depending of what is being changed. Allowed types are object, string, boolean, number, const, null, array and init_parameter.

Virtual type configuration

In the documentation virtual type is defined as a type that allows you to change the arguments of a specific injectable dependency and change the behavior of a particular class.

If we go back to a helper example, instead of injecting a new helper, there are occasions when changing just one argument in the original helper will do the job and creating a new file is redundant. In our example that would mean creating a virtual type from MagentoCatalogHelperProduct, changing its arguments and using that virtual helper as an argument of a catalog product class.

<config>
    <virtualType name="virtualHelper" type="MagentoCatalogHelperProduct">
        <arguments>
            <argument name="catalogSession" xsi_type="object">InchooCatalogModelSessionProxy</argument>
        </arguments>
    </type>
    <type name="MagentoCatalogApiDataProductInterface">
        <arguments>
            <argument name="catalogProduct" xsi_type="object">virtualHelper</argument>
        </arguments>
    </type>
</config>

Virtual types come in handy when only construct arguments of dependencies have to be changed without creating any additional files and by just configuring it in xml files.

There’s more than one way of overriding classes and methods and choosing which one to use will depend on a situation you run into. While using a class preference as a way of overriding may look like the easiest way which will work in most situations, it’s the cause of many conflicts when different modules try to override the same classes and the same methods, which is why all the ways should be taken into consideration.