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.
- Pick the class and method to intercept 01
- Create the plugin class with before / around / after methods 02
- Declare the plugin in di.xml under the right area 03
- Run setup:di:compile to generate interceptor proxies 04
- Magento runs the interceptor at runtime 05
Most common mistake Using around when before or after would suffice
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.
-
01
Pick the class and method to intercept
The target has to be a
publicmethod that is notfinalorstatic, on a class that Magento creates through the object manager. Plugins do not apply tofinalclasses, tovirtualTypeentries, or to objects your own code creates withnew, because Magento only wraps objects it builds itself. When I am not sure a plugin is being applied, I runsetup:di:compileafter declaring it and look ingenerated/code/for anInterceptorfor that class. -
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, ornullto 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. -
03
Declare the plugin in di.xml under the right area
A plugin declared in
etc/di.xmlapplies in every area;etc/frontend/di.xmllimits it to the storefront andetc/adminhtml/di.xmlto 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.sortOrdersets the order when several plugins target the same method, and redeclaring a plugin by name withdisabled="true"is how I switch off a third-party plugin without editing that module. -
04
Run setup:di:compile to generate interceptor proxies
In
developermode Magento generates interceptors the first time they are needed. Inproductionmode they have to exist already, sobin/magento setup:di:compilewrites anOriginal\Class\Interceptorthat extends the original class intogenerated/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. -
05
Magento runs the interceptor at runtime
When Magento builds the class through DI, you get the
Interceptorinstead of the plain original. Calling an intercepted method runs the plugins nested bysortOrder: the plugin with the lowest value starts first and finishes last. In practicebeforemethods run from low to highsortOrder, the original method runs in the middle, andaftermethods run back out from high to low, each one getting the previous$result. Anaroundplugin wraps every plugin with a highersortOrder. That is whysortOrdermatters, and why anaftermethod that forgets to return$resultbreaks every caller.
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 bysortOrder. -
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\LocalizedExceptionand the save stops with a clear message. AnafterGetById($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
aroundplugin 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 inetc/cache.xmlfor it, so it shows up in Cache Management andbin/magento cache:cleanclears 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.
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
aroundplugin adds aClosurewrap to the call chain, measurable overhead, and it prevents the framework from optimising the chain.beforeplugins are essentially free;afterplugins are near-free. The discipline: usebeforewhen you only need to munge arguments,afterwhen you only need to munge the return value, and reservearoundfor cases where you genuinely need to wrap the call, conditional skip-original, caching, transaction wrapping, retry-on-failure. Lifting a wrongaroundback to abefore/afteris 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 inproductionmode where the silent failure looks identical to a working integration. Always test plugins indevelopermode first and assert yourbefore/afterhook 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$resultor a modified version of it. PHP’s lack of return-type enforcement meansreturn;(or simply forgetting thereturn) compiles fine and the interceptor usesnullas the result, clobbering every downstream consumer. Classic symptom: a Magento page works perfectly without your module, breaks with the crypticTrying to access array offset on value of type nullthe moment you enable it. Always:return $result;from everyafterplugin, even if the body did nothing.
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.
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.