32

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 a before or after 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?

  • Alan, do you have a rule of thumb when to use \closure $proceed vs. \callable $proceed in a plugin? The official doc only mentions \callable and never touches on \closure. – thdoan Dec 1 '16 at 2:55
38

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()
16

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)
1

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 be return - $itemB['sortOrder'];
  • return 1; should be return 0;

Hope it will help you.

  • but is $pluginInfo fully loaded with plugins? Or is there some lazy loading going on that might impact the behavior? What does sort order mean for multiple plugins? i.e. is it "before plugin 1, around plugin 1, after plugin 1, before plugin 2, around plugin 2, after plugin 2" or "before plugin 1", "before plugin 2, around plugin1, around plugin 2", etc. The code looks like the later, but "getNext" populating plugin information in a (maybe?) lazy loading way, and how Magento avoid recursion with around make this all unclear, and hard to spot what's a bug, what's a feature. – Alan Storm May 5 '16 at 16:32
  • Magento sort plugin class not plugin method. – KAndy May 5 '16 at 16:55
  • And list of plugins can be changed, for example, if new aria we be loaded. – KAndy May 5 '16 at 16:56
  • There's some implicit knowledge you have that's not obvious, because "sort the plugin class and not plugin method" doesn't make it clear what the rules for plugin interaction are or should be. – Alan Storm May 5 '16 at 17:49
  • maybe this link will be useful magehero.com/posts/472/magento-2-interception – KAndy May 5 '16 at 19:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.