Men programming on a laptop

Code customization techniques in Magento 2

Compared to previous Magento versions, Magento 2 uses a technique called Dependency Injection which allows an object to receive the dependencies it relies on (injection) instead of instantiating them directly. This generally introduces another layer of abstraction between objects that minimises the risk of conflicts by assuring that no client code would have to be changed in case an object it depends on is changed to a different one.

In Magento this process is controlled by a di.xml file. It keeps all the dependencies that need to be injected by Object Manager. Each module can have its own di.xml files: a global <moduleDir>/etc/di.xml and/or an area-specific <moduleDir>/etc/<area>/di.xml. There is also one initial file app/etc/di.xml which is loaded by Magento first. This means there are some new ways for native code customization in Magento 2 which I will explain below in more detail.

Preferences

This way of code customization is the most similar to the class override from Magento 1.x. In Magento 2 all classes are defined by their corresponding interfaces in the di.xml files. Therefore by defining your preference, the current class could be replaced with your own.

Example: you want to override the Magento\Cms\Model\Page class with your own.
This is how you do it:

1) Set file dependency for Magento_Cms module in our module's etc/module.xml :

&lt;config&gt;
    &lt;module name="Kiwee_MyModule" setup_version="0.1.0"&gt;
        &lt;sequence&gt;
            &lt;module name="Magento_Cms" /&gt;
        &lt;/sequence&gt;
    &lt;/module&gt;
&lt;/config&gt;

2) Now check how the Magento\Cms\Model\Page class is defined in Magento_Cms module's di.xml file:

<config>
	<preference for="Magento\Cms\Api\Data\PageInterface" type="Magento\Cms\Model\Page" />
</config>

3) Define your own preference in the module's di.xml file:

&lt;config&gt;
	&lt;preference for="Magento\Cms\Api\Data\PageInterface" type="Kiwee\Cms\Model\Page" /&gt;
&lt;/config&gt;

4) Create a class that extends the original class:

namespace Kiwee\Cms\Model;

class Page extends \Magento\Cms\Model\Page
{
	// place your code here
}

This technique is prone to conflicts, thus I recommend avoiding it whenever another one could be used.

Argument replacement

As mentioned before, you can control which dependencies should be injected into a class using the di.xml file. Your can use it to your advantage and replace some of them with your own.

Example: The class Magento\User\Model\User has in its constructor an argument \Magento\User\Helper\Data $userData.

1) Replace it with your own class using di.xml:

&lt;config&gt;
	&lt;type name="Magento\User\Model\User"&gt;
		&lt;arguments&gt;
			&lt;argument name="userData" xsi:type="object"&gt;Kiwee\User\Helper\Data&lt;/argument&gt;
		&lt;/arguments&gt;
	&lt;/type&gt;
&lt;config&gt;

2) You could also make changes to the simple type arguments. Please check this definition for the Magento\Framework\Url type in the global di.xml file:

&lt;config&gt;
	&lt;type name="Magento\Framework\Url"&gt;
		&lt;arguments&gt;
			&lt;argument name="session" xsi:type="object"&gt;Magento\Framework\Session\Generic\Proxy&lt;/argument&gt;
			&lt;argument name="scopeType" xsi:type="const"&gt;Magento\Store\Model\ScopeInterface::SCOPE_STORE&lt;/argument&gt;
		&lt;/arguments&gt;
	&lt;/type&gt;
&lt;config&gt;

3) You see a constant parameter scopeType. You could change its value using your own module's di.xml file to something completely different:

&lt;config&gt;
	&lt;type name="Magento\Framework\Url"&gt;
		&lt;arguments&gt;
			&lt;argument name="scopeType" xsi:type="const"&gt;Magento\Store\Model\ScopeInterface::SCOPE_WEBSITE&lt;/argument&gt;
		&lt;/arguments&gt;
	&lt;/type&gt;
&lt;config&gt;

Plugins

Plugins are a new way of customizing code introduced in Magento 2. Its main advantage is that it doesn’t change the class itself. Instead it intercepts the method calls and runs the code before, after or around the original method. This is the best way of changing method's behaviour as it doesn't cause conflicts when many modules try to extend the same method. On the other hand, plugins have limitations. They cannot be used with any of the following:

  • Objects that are instantiated before Magento\Framework\Interception is bootstrapped
  • Final methods
  • Final classes
  • Any class that contains at least one final public method
  • Non-public methods
  • Static methods
  • __construct
  • Virtual types

Plugins are declared in module's di.xml file.

&lt;config&gt;
	&lt;type name="{ObservedType}"&gt;
		&lt;plugin name="{PluginName}"
			type="{PluginClassName}"
			sortOrder="1"
			disabled="false"/&gt;
	&lt;/type&gt;
&lt;/config&gt;

You need to specify the following elements when declaring a plugin:

  • Type name: a class that the plugin observes.
  • Plugin name: an arbitrary name that identifies the plugin; used to merge the configurations for the plugin.
  • Plugin type: the name of a plugin class or its virtual type; it uses the naming convention <ModelName>\Plugin.

Optional arguments:

  • Plugin sortOrder: the order in which plugins that call the same method are run.
  • Plugin disabled: set to TRUE to disable a plugin.

Plugins can have three different types of intercepting methods: before, after and around:

“Before” listener

If you want to modify argument values of the original method you could use the "before" listener. Such methods are created using the "before" prefix added to the original method name. For instance, a listener for a method getData() will be beforeGetData() .
Example: Here's the plugin that changes the $name parameter before sending it to the original setName() method using the "before" listener:

namespace Kiwee\MyModule\Plugin;

class ProductPlugin
{
	public function beforeSetName(\Magento\Catalog\Model\Product $subject, $name) {
		return '(' . $name . ')';
	}
}

"Before" listeners will receive $subject parameter following by any parameters the original method has. $subject parameter gives you access to public methods of original object.

“After” listener

If you want to change the return value of the original method you could use "after" listener. To create such a listener you need to add the prefix "after" to the original method name, so that the "after" listener for the method getData() is afterGetData() .

Example: such a listener modifies the result of the original getName() method using the "after" listener:

namespace Kiwee\MyModule\Plugin;

class ProductPlugin
{
	public function afterGetName(\Magento\Catalog\Model\Product $subject, $result) {
		return '|' . $result . '|';
	}
}

"After" listeners will receive two parameters. $subject will give you access to public methods of the original objects. $result is original method's return value.

“Around “listener

If you want to change both the arguments and return value of original method you can use "around" listener. To create such method you need to add the "around" prefix to the original method name, aroundGetData() listener for getData() method. "Around" listeners are executed in such a way that it runs before and after the original method allowing you to completely override the method's functionality.

namespace Kiwee\MyModule\Plugin;

class ProductPlugin
{
	public function aroundSave(\Magento\Catalog\Model\Product $subject, \Closure $proceed) {
		$this-&gt;doSmthBeforeProductIsSaved();
		$returnValue = $proceed();
		if ($returnValue) {
			$this-&gt;postProductToFacebook();
		}
		return $returnValue;
	}
}

The around listener method will receive two parameters ($subject and $proceed) followed by arguments that belong to the original method:

  • The $subject parameter will provide access to all public methods of the original class.
  • The $proceed parameter is a lambda that will call the next plugin or method.

Any further method arguments will be passed to the around plugin methods after the $subject and the $proceed arguments. They have to be passed on to the next plugin method when calling $proceed().

If the around method does not call the $proceed, it will prevent execution of all the following plugins in the row and the original method call.

It is important that your listener has exactly the same parameters as the original method including the type hints and default values.

Execution order of the plugin methods

If several plugins are applied to the same method they are executed in the following sequence:

  • a before method with the highest priority (the smallest sortOrder value)
  • an around method with the highest priority (the smallest sortOrder value) *
  • before methods with the decreasing priority (from the smallest to the greatest sortOrder values)
  • around methods with the decreasing priority (from the smallest to the greatest sortOrder values) *
  • an after method with the lowest priority (the greatest sortOrder value)
  • other after methods with the increasing priority (from the greatest to the smallest sortOrder value)

* If there are any other around or before listeners with lower priority, only the first part of around listener is called (before $proceed()). The remaining part of the method will be called after the execution of the listeners is finished.

Events & observers

This has nothing to do with the dependency injection. Rather, it’s another way of customising Magento functionality. Events and observers are something every Magento developer should already be familiar with. It's an easy way of customising existing functionalities but it's limited to specific places in the code only, so it doesn't give you the complete freedom. There is little difference in how they work in Magento 2 compared to the previous versions.

Event observers

To execute custom code by an event trigger you need to create an observer and attach it to the event needed. To define an observer you create a class in the <moduleDir>/Observer directory. The class needs to implement Magento\Framework\Event\ObserverInterface and define its execute() method:

namespace Kiwee\MyModule\Observer;

use Magento\Framework\Event\ObserverInterface;

class MyObserver implements ObserverInterface
{
	public function execute(\Magento\Framework\Event\Observer $observer) {
		$eventData = $observer-&gt;getData('myEventData');
		//Observer execution code...
	}
}

Once this is done, you attach this observer to an event. This is done in the <moduleDir>/etc/events.xml or <moduleDir>/etc/<area>/events.xml files.

Example: your could attach the observer to the "checkout_cart_save_after" event:

&lt;config&gt;
	&lt;event name="checkout_cart_save_after"&gt;
		&lt;observer name="myObserverName" instance="Kiwee\MyModule\Observer\MyObserver" /&gt;
	&lt;/event&gt;
&lt;/config&gt;

The <observer> xml element has the following properties:

  • Name (required) - the name of the observer for the event definition. It must be unique per event definition.
  • Instance (required) - the fully qualified class name of the observer.
  • Disabled - determines whether this observer is active or not. Default value is false.
  • Shared - determines if the object is a singleton. Default is false.

Custom events

You can also define custom events in your code. To be able to do this you need to inject an event manager into your class. In the following example you can see how to trigger an event with or without the parameters:

namespace Kiwee\MyModule;

class MyClass
{
	private $eventManager;

	public function __construct(\Magento\Framework\Event\ManagerInterface $eventManager){
		$this-&gt;eventManager = $eventManager;
	}

	public function something() {
		$eventData = null;
		// Code...
		$this-&gt;eventManager-&gt;dispatch('my_module_event_before');
		// More code that sets $eventData...
		$this-&gt;eventManager-&gt;dispatch('my_module_event_after', ['myEventData' =&gt; $eventData]);
	}
}

Choosing code customization method

As you can see, there are many ways one can customise the existing code in Magento 2. If used properly, these methods can minimize the conflicts even if different modules are installed. There are many methods to choose from and each of the methods described above has its advantages and disadvantages. So I would recommend to try various ones before you know what's best for you in each situation.

FacebookTwitterPinterest

Piotr Kaczkowski

Senior Full Stack Developer

My adventure with computers started when my parents bought our first PC, I was about 10 at the time. At first I was just playing games but after a while I became interested in programming. Starting with Turbo Pascal and Assembler then moved to C++ and PHP.