Code customization techniques in Magento 2
Piotrek KaczkowskiReading Time: 5 minutes
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
:
<config> <module name="Kiwee_MyModule" setup_version="0.1.0"> <sequence> <module name="Magento_Cms" /> </sequence> </module> </config>
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:
<config> <preference for="Magento\Cms\Api\Data\PageInterface" type="Kiwee\Cms\Model\Page" /> </config>
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
:
<config> <type name="Magento\User\Model\User"> <arguments> <argument name="userData" xsi:type="object">Kiwee\User\Helper\Data</argument> </arguments> </type> <config>
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:
<config> <type name="Magento\Framework\Url"> <arguments> <argument name="session" xsi:type="object">Magento\Framework\Session\Generic\Proxy</argument> <argument name="scopeType" xsi:type="const">Magento\Store\Model\ScopeInterface::SCOPE_STORE</argument> </arguments> </type> <config>
3) You see a constant parameter scopeType. You could change its value using your own module's di.xml
file to something completely different:
<config> <type name="Magento\Framework\Url"> <arguments> <argument name="scopeType" xsi:type="const">Magento\Store\Model\ScopeInterface::SCOPE_WEBSITE</argument> </arguments> </type> <config>
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.
<config> <type name="{ObservedType}"> <plugin name="{PluginName}" type="{PluginClassName}" sortOrder="1" disabled="false"/> </type> </config>
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->doSmthBeforeProductIsSaved(); $returnValue = $proceed(); if ($returnValue) { $this->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->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:
<config> <event name="checkout_cart_save_after"> <observer name="myObserverName" instance="Kiwee\MyModule\Observer\MyObserver" /> </event> </config>
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->eventManager = $eventManager; } public function something() { $eventData = null; // Code... $this->eventManager->dispatch('my_module_event_before'); // More code that sets $eventData... $this->eventManager->dispatch('my_module_event_after', ['myEventData' => $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.