Magento 2: Plugin before/around/after Interaction
-
29-09-2020 - |
Question
In Magento 2, when you create an "around" plugin
public function aroundRenderResult(
\Magento\Framework\Controller\ResultInterface $subject,
\Closure $proceed,
ResponseHttp $response
) {
//...
$proceed($response);
//...
}
you can proceed to the next around plugin, culminating with call the actual original method, by calling/invoking the passed in $proceed
method. This is a common design pattern, often seen in PHP Frameworks middleware implementations.
However -- it does present some confusion w/r/t to implementation details. Specifically
If, in addition to an
aroundPlugin
, an object/class has abefore
orafter
plugin defined, when do they fire in relation to the chain of around plugins?
i.e. will all the before methods fire before any around plugin methods fire? Or will before plugins only fire before the final, actual real method fires?
The specific problem I'm trying to track down is, I can't seem to get a plugin attached to the dispatch method the a Magento 2 front controller when Magento's in full page caching mode. The full page cache operates by an around plugin that does not call $proceed($response)
. I've tried digging into some of the code around these plugins and have found the system difficult to reason about without knowing how its intended that plugins work.
i.e. -- the description on the dev docs page appears, in this one specific instance, to be inaccurate. It's unclear if the documentation is wrong, or if this is a recently introduced bug, if it's an edge case, or if my plugin configuration is wrong.
Does anyone know, by direct observation, or by cultural knowledge, how this prioritization is supposed to work?
Solution
Plugins are sorted by sort order first, and then by method prefix.
Example: for method with 3 plugins (PluginA, PluginB, PluginC) with following methods and sortOrder:
- PluginA (sortOrder = 10)
- beforeDispatch()
- afterDispatch()
- PluginB (sortOrder = 20)
- beforeDispatch()
- aroundDispatch()
- afterDispatch()
- PluginC (sortOrder = 30):
- beforeDispatch()
- aroundDispatch()
- afterDispatch()
Execution flow should be following:
- PluginA::beforeDispatch()
- PluginB::beforeDispatch()
- PluginB::aroundDispatch()
- PluginC::beforeDispatch()
- PluginC::aroundDispatch()
- Action::dispatch()
- PluginC::afterDispatch()
- PluginB::afterDispatch()
- PluginA::afterDispatch()
OTHER TIPS
From the Magento 2 cookbook:
If there are multiple plugins that extend the same original function, they are executed in the following sequence:
- the before plugin with the lowest
sortOrder
- the around plugin with the lowest
sortOrder
- other before plugins (from the lowest to highest
sortOrder
)- other around plugins (from the lowest to highest
sortOrder
)- the after plugin with the highest
sortOrder
- other after plugins (from the highest to the lowest
sortOrder
)
For me it should work as:
- if sort order is not defined its equivalent of zero (and this mean that real order is undefined)
- plugins should be sort by order
If you review code of \Magento\Framework\Interception\Interceptor::___callPlugins()
you can see that plugins called in order of stored in $pluginInfo
variable.
This information passed form auto generated method in interceptors like
public function {method}()
{
$pluginInfo = $this->pluginList->getNext($this->subjectType, '{method}');
if (!$pluginInfo) {
return parent::{method}();
} else {
return $this->___callPlugins('{method}', func_get_args(), $pluginInfo);
}
}
As you see \Magento\Framework\Interception\PluginListInterface
interface and \Magento\Framework\Interception\PluginList\PluginList
default implementation responsible for plugin sorting. See _inheritPlugins:152 method
/**
* Sort items
*
* @param array $itemA
* @param array $itemB
* @return int
*/
protected function _sort($itemA, $itemB)
{
if (isset($itemA['sortOrder'])) {
if (isset($itemB['sortOrder'])) {
return $itemA['sortOrder'] - $itemB['sortOrder'];
}
return $itemA['sortOrder'];
} elseif (isset($itemB['sortOrder'])) {
return $itemB['sortOrder'];
} else {
return 1;
}
}
For me this function have two logical errors:
return $itemB['sortOrder'];
should bereturn - $itemB['sortOrder']
;return 1;
should bereturn 0;
Hope it will help you.