The Unexpected Behavior of Magento Plugin Execution Order

The Unexpected Behavior of Magento Plugin Execution Order

Understand the intricacies of Magento plugin execution and how the surprising behavior of around plugins works.

If you've been working with Magento for a while, you've definitely used plugins — they're essential to Magento's extensibility.

But have you ever thought about how they execute, especially when multiple plugins target the same method?

The question started on Campus

A question recently popped up on Campus, which is our premium community for Magento developers (which is exclusively available by enrolling in the University).

Denis, one of our University students, asked specifically about how about plugin sort order and execution works, which kicked off this little deep-dive:

Question on Campus about plugin execution order

On a side note, I love diving into these questions that send you down a bit of a rabbit hole. This makes your brain really think, and when you document your findings, you can truly learn how something works. It's one of the reasons I love Campus so much.

Magento Plugins: The Basics

Let's go over a quick refresher of what we know about Magento's plugin system and how it works.

Magento provides us three types of plugins:

  1. Before plugins: These run before the target method and can modify the arguments passed to it.
  2. After plugins: These run after the target method and can modify the return value.
  3. Around plugins: These wrap around the target method, allowing you to execute code both before and after the method, and give you complete control over whether the original method is even called.

These plugins can also be prioritized with the sortOrder attribute within the di.xml file that they are declared within. This allows you to control the order in which plugins execute, and are determined by both the naming of the plugin class and the sortOrder attribute.

Now let's determine what happens when you have multiple plugins which target the same method.

According to Adobe's documentation, when you have multiple plugins (let's call them A, B, and C) each with a different sort order, they execute like this:

Plugin A (sort order 10): beforeExecute()
Plugin B (sort order 20): beforeExecute()
Plugin C (sort order 30): beforeExecute()
Original method execution
Plugin C (sort order 30): afterExecute()
Plugin B (sort order 20): afterExecute()
Plugin A (sort order 10): afterExecute()

This makes sense — but what happens when an around plugin is thrown into the mix?

This is where things get interesting.

Three rules that explain Magento plugin execution order

After a lot of testing, I've found that Magento's plugin execution follows these three rules, in order of importance:

  1. Lower sort order = higher priority

    This determines which plugin gets executed first. If two plugins target the same method, the one with the lower sort order runs first. There can even be negative sort orders, which execute before positive sort orders!

  2. Before methods, then around methods, then after methods

    The type of plugin also determines the execution order. Before methods run first, then around methods, then after methods. This is of course why before plugins are called "before" and after plugins are called "after".

  3. Around methods create a nested execution context for all remaining plugins

    This rule is the most surprising! Around plugins throw a wrench in the mix, and most developers don't realize how its sequencing works.

And this third rule is the absolute key to fully understanding plugin execution order.

Around Plugins restart execution loops

When an around plugin executes, it doesn't just wrap around the original method — it actually starts an entirely new execution sequence for all remaining plugins.

Let's look at an example so you can see what I mean. I created a simple module with three plugins.

They are named A, B, and C, each with a related and corresponding sortOrder:

etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Cms\Controller\Index\Index">
        <plugin name="vendor_module_a" type="Vendor\Module\Plugin\A" sortOrder="10"/>
        <plugin name="vendor_module_b" type="Vendor\Module\Plugin\B" sortOrder="20"/>
        <plugin name="vendor_module_c" type="Vendor\Module\Plugin\C" sortOrder="30"/>
    </type>
</config>
Plugin/A.php
<?php
 
declare(strict_types=1);
 
namespace Vendor\Module\Plugin;
 
class A
{
    public function beforeExecute()
    {
        dump(__METHOD__);
    }
 
    public function afterExecute()
    {
        dump(__METHOD__);
    }
}
Plugin/B.php
<?php
 
declare(strict_types=1);
 
namespace Vendor\Module\Plugin;
 
class B
{
    public function beforeExecute()
    {
        dump(__METHOD__);
    }
 
    public function aroundExecute(
        $subject,
        callable $proceed
    ) {
        dump(__METHOD__ . ' before');
        $result = $proceed();
        dump(__METHOD__ . ' proceed');
        dump(__METHOD__ . ' after');
        return $result;
    }
 
    public function afterExecute()
    {
        dd(__METHOD__);
    }
}
Plugin/C.php
<?php
 
declare(strict_types=1);
 
namespace Vendor\Module\Plugin;
 
class C
{
    public function beforeExecute()
    {
        dump(__METHOD__);
    }
 
    public function afterExecute()
    {
        dump(__METHOD__);
    }
}

There's nothing magical going on here -- Plugin A runs before the original method, Plugin B runs around the original method, and Plugin C runs after the original method.

But you'll also notice that I added some extra calls to dump() in the around method to help us see what's really happening within this method.

The output of this code looks like this:

Vendor\Module\Plugin\A::beforeExecute
Vendor\Module\Plugin\B::beforeExecute
Vendor\Module\Plugin\B::aroundExecute before
Vendor\Module\Plugin\C::beforeExecute
Vendor\Module\Plugin\C::afterExecute
Vendor\Module\Plugin\B::aroundExecute proceed
Vendor\Module\Plugin\B::aroundExecute after
Vendor\Module\Plugin\A::afterExecute
Vendor\Module\Plugin\B::afterExecute

Did you catch what's actually happening here? 😅

Plugin B's around method starts to execute, but before it finishes, Plugin C's before and after methods execute. Then only after that execution completes is when Plugin B's around method finishes it's execution.

This happens because when the around plugin calls $proceed(), it triggers all remaining plugins that target the same method to execute. The around plugin doesn't just wrap the original method — it wraps everything that comes after it within a plugin chain.

Break it down a bit more

This can be a big hard to visualize, so let's break this down step by step:

  1. Plugin A executes first with its beforeExecute() method (sort order 10)
  2. Plugin B's beforeExecute() method then executes as next in the queue (sort order 20)
  3. Then Plugin B's aroundExecute() begins it's execution as around plugins are called after before plugins
  4. But When B's around method hits the $proceed() call, it doesn't just go to the original method - it triggers all remaining plugins that wrap the original method, starting with Plugin C
  5. Plugin C's beforeExecute() runs
  6. Since there are no more plugins left after C, the original method of the class being plugged into then executes
  7. Then the natural flow calls for Plugin C's afterExecute() to execute
  8. Then... we go back to Plugin B's around method, which continues executing whatever code comes after the call to $proceed()
  9. And finally, the afterExecute() methods for A and B run

This means that when the around method executes, it's sort of like a set of nesting dolls. Things aren't executed in a linear sequence as you'd expect, as they keep wrapping around other method calls.

Taking another look at Plugin B's around method:

Plugin/B.php
// ...
 
public function aroundExecute(
    $subject,
    callable $proceed
) {
    dump(__METHOD__ . ' before');
    $result = $proceed();  // This triggers Plugin C
    dump(__METHOD__ . ' proceed');
    dump(__METHOD__ . ' after');
    return $result;
}
 
// ...

The call to $proceed() actually starts an entirely new plugin execution chain.

Some practical advice

Based on what we've learned here, I have some recommendations:

  • Avoid around plugins whenever possible: They create additional complexity that makes it harder for you and others to debug and reason about your code. Just use before and after plugins instead, unless you have a really intricate use-case.

  • Keep plugins focused: Each plugin should only handle one specific task. This makes things easier to understand. Too much complexity isn't good for anyone.

  • Specify a sortOrder (but only if needed): I typically don't like to specify a sort order for my custom plugins unless I have a reason because it allows others to more easily override my plugins with their plugins. But if your use-case requires a specific sort order in order to function properly, be explicit and define it.

  • Document around plugin usage: Around plugins are an edge-case, so you should have an extremely solid reason using one. And be sure to document it within a corresponding docblock, and always leave a comment within an around plugin if you choose not to execute it's $proceed() function, otherwise it will look you introduced a bug for leaving it out.

  • Test things thoroughly: Plugin interactions can get really complicated, so be sure to test all possible scenarios, which gets way more difficult with around plugins. You want to ensure that the code you write is predictable and reliable.

Takeaway

The biggest thing to take away here is that around plugins create nested execution contexts. They don't just wrap the original method -- they wrap around all subsequent plugins too. This is why they increase the size of the call stack, and why you should always prefer before and after plugins instead.

Magento's plugin system is incredibly powerful, but the execution order — especially with around plugins — isn't always intuitive.

  1. Read more about plugins, interceptors, and event observers (Free article)
  2. Grow your Magento expertise with all courses & lessons (700+ students)
  3. Get weekly Magento deep-dives and learn something new every week (9,000+ subscribers)