First, take a look at this method Magento\Framework\Interception\Interceptor::___callPlugins
. this is where the magic happens. (I'll explain why you need to look at this later).
Notice that Magento\Framework\Interception\Interceptor
is a trait and not a class. So $this
inside the trait references the object that has this trait.
This method accepts 3 parameters.
- the method being pluginized,
- the arguments of the original method
- some information about the plugin.
This method defines a variable (that is a function) that does this.
if (isset($currentPluginInfo[DefinitionInterface::LISTENER_BEFORE])) {
checks if the method has before
plugins.
if it does, it loops through all declared plugins. (foreach ($currentPluginInfo[DefinitionInterface::LISTENER_BEFORE] as $code) {
)
.
and calls the beforeOriginalMethod
(the plugin itself) that receives as parameter the class being pluginized ($this) and the arguments of the original method
Then it checks for around
plugins (if (isset($currentPluginInfo[DefinitionInterface::LISTENER_AROUND])) {
) similar as for before
plugins.
The difference is that the plugin method receives an extra parameter $next
which is a callable
the variable (function) defined above. this is used to be able to call the orignial method inside the around
plugin since the around
plugin replaces the original method.
Then it checks for after
plugins (if (isset($currentPluginInfo[DefinitionInterface::LISTENER_AFTER])) {
)
this is very similar to the before
plugins. The difference is that this receives as parameters the result of the original method also.
What the plugins should return.
before
This plugin is useful if you want to change the arguments of the original method. So it must return an array with the new values of the arguments. For example if you pluginize the method setName
from the product model, this one receives as parameter a string called $name
. Your plugin should return an array with a string [$name]
. This type of plugin can return null. This means that the original method will receive the parameters unchanged.
around
should return the same thing as the original method returns. THere is also a catch. If you don't call inside your plugin the original method (second parameter described above) all other plugins that come after yours will be ignored.
- after. Must return the same thing as the original method.
How the magic happens.
When instantiating a class through the ObjectManager magento checks if that class has methods that are pluginizable
(say that 3 times fast :D).
See here what methods / classes support plugins.
long story short, it reads the di.xml
files, checks for declared plugins to the class it tries to instantiate, checks if the classes that are declared as plugins have methods that start with before
, after
, around
and that match the public method names of the class it tries to instantiate.
If it finds something it generates a class that extends the original class and it is called Original\ClassName\Interceptor
(adds interceptor
at the end.) and instantiates this class instead of the original one.
Just look in the generated
folder for a file named Interceptor.php
.
All these classes look the same.
They have the trait \Magento\Framework\Interception\Interceptor;
, the one I described above.
The constructor is the same as the original class constructor with an extra ___init()
call, which is located in the \Magento\Framework\Interception\Interceptor
trait.
Then all the pluginized methods are listed and they all look the same
public function someMethodHere(arguments here)
{
$pluginInfo = $this->pluginList->getNext($this->subjectType, 'someMethodHere');
if (!$pluginInfo) {
return parent::someMethodHere(arguments here);
} else {
return $this->___callPlugins('someMethodHere', func_get_args(), $pluginInfo);
}
}
This is how the link is made from the plugins to the actual classes. Through a child class of the original class that either calls the original class method or the plugins declared for that class.