Skip to content
5.0 Upwork, 100+ reviews Adobe Certified since 2021 New: become your own Magento developer Learn more →
Magento glossary

What is a Magento plugin?

Short answer A Magento 2 plugin (also called an interceptor) lets you change what a public method of a Magento class does without editing that class. You declare it in a module’s etc/di.xml, write before, around or after methods in a small class, and Magento generates an Interceptor class in generated/code/ that calls your code around the original. A plugin is what I reach for first when core or third-party behaviour needs to change. It does not work on final, static or non-public methods, on constructors, or on virtualType entries.

How it works
  1. Pick the class and method to intercept 01
  2. Create the plugin class with before / around / after methods 02
  3. Declare the plugin in di.xml under the right area 03
  4. Run setup:di:compile to generate interceptor proxies 04
  5. Magento runs the interceptor at runtime 05
Each step is explained below

Most common mistake Using around when before or after would suffice

How it works

Five steps from di.xml declaration to runtime interception

There is no magic in a plugin: a di.xml entry, a class with methods named by convention, and a generated Interceptor that calls them in order. These are the steps I follow every time I write one.

  1. 01

    Pick the class and method to intercept

    The target has to be a public method that is not final or static, on a class that Magento creates through the object manager. Plugins do not apply to final classes, to virtualType entries, or to objects your own code creates with new, because Magento only wraps objects it builds itself. When I am not sure a plugin is being applied, I run setup:di:compile after declaring it and look in generated/code/ for an Interceptor for that class.

  2. 02

    Create the plugin class with before / around / after methods

    I put the class in Vendor\Module\Plugin\, usually named after the class it plugs into. Method names are the original name with a prefix: beforeMethodName($subject, $arg1) runs first and returns an array of new arguments, or null to keep them; aroundMethodName($subject, callable $proceed, $arg1) wraps the call, and you decide whether to call $proceed(...); afterMethodName($subject, $result, $arg1) runs last and must return $result, changed or not. $subject, the original object, always comes first.

  3. 03

    Declare the plugin in di.xml under the right area

    A plugin declared in etc/di.xml applies in every area; etc/frontend/di.xml limits it to the storefront and etc/adminhtml/di.xml to the admin. The declaration looks like <type name="Original\Class"><plugin name="unique_name" type="Vendor\Module\Plugin\OriginalClassPlugin" sortOrder="10" disabled="false"/></type>. Pick a name that is unique for that type, because the same name in another module replaces the earlier declaration. sortOrder sets the order when several plugins target the same method, and redeclaring a plugin by name with disabled="true" is how I switch off a third-party plugin without editing that module.

  4. 04

    Run setup:di:compile to generate interceptor proxies

    In developer mode Magento generates interceptors the first time they are needed. In production mode they have to exist already, so bin/magento setup:di:compile writes an Original\Class\Interceptor that extends the original class into generated/code/ for every class with at least one plugin. Each intercepted method in that file runs the plugin chain and then calls the parent method. After adding a plugin on a production-mode store I always recompile, otherwise the new plugin never runs.

  5. 05

    Magento runs the interceptor at runtime

    When Magento builds the class through DI, you get the Interceptor instead of the plain original. Calling an intercepted method runs the plugins nested by sortOrder: the plugin with the lowest value starts first and finishes last. In practice before methods run from low to high sortOrder, the original method runs in the middle, and after methods run back out from high to low, each one getting the previous $result. An around plugin wraps every plugin with a higher sortOrder. That is why sortOrder matters, and why an after method that forgets to return $result breaks every caller.

When to use

Four scenarios where a plugin is the right extension point

Plugins are not the answer to everything. These are the four cases where I choose a plugin over a preference or an observer.

  • Modifying a Magento or third-party class without touching its source

    This is the main reason plugins exist. If Magento\Catalog\Model\Product::getFinalPrice() needs a different result on one store view, or a third-party controller needs an extra response header, I write a plugin. The original class stays untouched, upgrades keep working, and several modules can plug the same method at once, with Magento chaining them by sortOrder.

  • Adding pre-validation or post-processing to a repository method

    A beforeSave($subject, $entity) plugin on a repository is how I add a validation rule before an entity is saved. If the rule fails, I throw a \Magento\Framework\Exception\LocalizedException and the save stops with a clear message. An afterGetById($subject, $result, $id) plugin does the opposite job: it adds extra data to the entity every time it is loaded, for example a value from an external system.

  • Wrapping an expensive call in cache

    An around plugin is a good fit for caching a slow call: check the cache, call $proceed(...) only on a miss, store the result and return it. I use this for things like a third-party API lookup or a heavy calculation that always gives the same answer for the same input. I register a custom cache type in etc/cache.xml for it, so it shows up in Cache Management and bin/magento cache:clean clears it like any other cache.

  • Intercepting a method that fires no dispatchable event

    Magento dispatches events at known points, such as model save and controller predispatch, and observers hook into those. Many methods dispatch nothing at all; Magento\Catalog\Model\Product::isAvailable() is one example. To change those, a plugin is the clean option. My rule: if the method already dispatches an event that gives me what I need, I use an observer; if it does not, I use a plugin.

Common mistakes

Three plugin mistakes that wreck performance or break silently

Every plugin-related Magento bug I’ve been called in to fix came from one of these three mistakes. Read your di.xml and your Plugin/ class with these in mind before shipping.

  • Using around when before or after would suffice

    Every around plugin adds a Closure wrap to the call chain, measurable overhead, and it prevents the framework from optimising the chain. before plugins are essentially free; after plugins are near-free. The discipline: use before when you only need to munge arguments, after when you only need to munge the return value, and reserve around for cases where you genuinely need to wrap the call, conditional skip-original, caching, transaction wrapping, retry-on-failure. Lifting a wrong around back to a before/after is a common quick-win in performance audits.

  • Plugining a final, static, or private method

    The DI compiler silently skips methods it cannot intercept, final, static, private, protected, constructors. Your plugin class is loaded, the di.xml entry is parsed, no error is thrown, and your plugin method simply never fires. The pain hits in production mode where the silent failure looks identical to a working integration. Always test plugins in developer mode first and assert your before/after hook actually runs with a quick log line, if the log stays silent, the target method isn’t pluginable and you need a different extension point.

  • Forgetting to return the right shape from an after-plugin

    afterMethodName($subject, $result, ...) must return $result or a modified version of it. PHP’s lack of return-type enforcement means return; (or simply forgetting the return) compiles fine and the interceptor uses null as the result, clobbering every downstream consumer. Classic symptom: a Magento page works perfectly without your module, breaks with the cryptic Trying to access array offset on value of type null the moment you enable it. Always: return $result; from every after plugin, even if the body did nothing.

FAQ

Magento plugins, frequently asked questions

  • Plugin vs observer vs preference, which one when?
    Three different jobs. A plugin wraps a specific method on an existing class without modifying that class, the surgical option. An observer reacts to a dispatched event (e.g. sales_order_place_after), the loosely-coupled option, but only works where Magento actually dispatches an event. A preference replaces one class entirely with another via the di.xml <preference for="X" type="Y"/> directive, the heavy-hammer option, used when you want to swap out a full implementation. Pick by scope: changing one method without touching the class? Plugin. Reacting to a known event? Observer. Replacing the class wholesale? Preference. Plugins are the most common choice in practice because they balance surgical control with non-invasive upgrades.
  • Why isn’t my plugin firing?
    Four common causes, in rough order of likelihood. First, you edited di.xml but didn’t re-run setup:di:compile (or your environment is in production mode and the interceptor wasn’t regenerated). Second, the target method is final, static, private, or protected, the DI compiler silently skips these and your plugin never wires. Third, the class is instantiated outside DI via raw new in the calling code, the framework only intercepts classes constructed through the DI container. Fourth, you declared the plugin in etc/di.xml but the calling code runs in an area-specific scope where a more specific etc/frontend/di.xml or etc/adminhtml/di.xml takes precedence and your generic one is overridden. Test in developer mode first with a log line in your before-method to confirm wiring before chasing the bug elsewhere.
  • Can I plugin a Magento\Framework core class?
    Yes, but tread carefully. Framework classes are pluginable as long as the method is public, non-final, non-static, and the class is instantiated via DI (which most framework services are). The pitfalls are: many framework classes are heavily used across the whole platform, so any plugin you add fires on every request, a sloppy implementation here is a measurable platform slowdown. Some framework classes have final methods that look like they should be pluginable but silently aren’t. And Magento’s own integration tests assert behaviour on framework classes, your plugin can break tests that have nothing to do with your module. Rule of thumb: only plugin framework classes when there is no module-level extension point that would do the job, and always measure the performance impact in a staging environment with realistic traffic.
  • What is sortOrder for, and when does it matter?
    When two or more modules plugin the same method, sortOrder controls the execution order. The convention is ascending for before and around plugins, descending for after plugins, meaning the lowest sortOrder runs first on the way in and last on the way out, naturally bracketing higher-sortOrder plugins. For most plugins sortOrder doesn’t matter because the plugins don’t conflict. It matters when one plugin’s output feeds another’s input, for example, plugin A adds a 10% discount and plugin B caps the total at $100; the order in which they fire changes the final price. The safe default is to leave sortOrder at the example value 10 unless you know your plugin must run before or after a specific third-party plugin, in which case set it to 5 (run earlier) or 50 (run later) explicitly.
  • Can I disable an inherited plugin from another module?
    Yes. In your own module’s di.xml, declare a plugin with the exact same name as the one you want to disable, against the same type, with disabled="true". For example: <type name="Magento\Catalog\Model\Product"><plugin name="some_third_party_plugin_name" disabled="true"/></type>. The pluginInfo cache resolves this by overlaying your declaration on top of the third-party module’s, and as long as your module is loaded after the one whose plugin you’re disabling (via sequence in module.xml), the disabled flag wins. This is the documented way to neutralise an undesired third-party plugin without forking the module, useful when a marketplace extension’s plugin causes a conflict and you can’t remove the module entirely.
  • Do plugins work in the admin area?
    Yes, declare them in etc/adminhtml/di.xml for admin-only scope, etc/frontend/di.xml for storefront-only, or etc/di.xml for both. Magento loads area-specific di.xml on top of the global one, so a plugin in etc/di.xml fires in both areas, and a plugin in etc/adminhtml/di.xml fires only when the admin area is active. Admin-area plugins are the standard way to add validation to backend save controllers, to inject extra columns into admin grids, or to intercept admin REST API calls. The same class can be plugined by both an adminhtml/di.xml and a frontend/di.xml entry concurrently, the area-specific compiler picks the right combination at runtime based on the request area.
Plugin audit

Want a plugin and DI architecture review on your Magento store?

Send your repo or storefront URL, I will audit your di.xml plugin declarations, flag wrong-type interceptions (around-where-after-suffices, broken after-returns, silent final/static targets), measure the call-chain overhead on hot paths, and reply with a written tuning plan, fixed-price quote, and earliest start date. 24-business-hour turnaround.

Kishan Savaliya Online 10 AM to 7 PM IST Chat on WhatsApp