What is a Magento plugin ?
A Magento 2 plugin (formally: interceptor) is the framework’s preferred mechanism for modifying the behaviour of any public method on a Magento class — without modifying the original class. Declared in a module’s etc/di.xml, implemented with before, around, and after methods, and wired by setup:di:compile which generates an Interceptor proxy. Cannot intercept final, static, private, or protected methods, constructors, or virtualType entries.
Five steps from di.xml declaration to runtime interception
A plugin is not a magic decorator — it is a declared di.xml entry, a class with conventional method names, and a generated Interceptor proxy that wires the call chain. Here is the wiring, end to end.
-
01
Pick the class and method to intercept
The target must be a
public, non-final, non-staticmethod of an instantiable class — Magento generates anInterceptorproxy only for classes that are constructed through DI. Methods onvirtualTypeentries, onfinalclasses, or on third-party libraries Composer-loaded outside the DI system (think raw Guzzle, Symfony components) cannot be intercepted because the framework never wraps them. When in doubt, check whether the class appears ingenerated/code/aftersetup:di:compile— if it does, plugin is fair game; if it doesn’t, use a different extension point. -
02
Create the plugin class with before / around / after methods
Conventionally placed at
Vendor\Module\Plugin\OriginalClassPlugin. Method names follow the originals with a capitalised first letter and a verb prefix:beforeMethodName($subject, $arg1)runs before the original (return new args ornullto leave them),aroundMethodName($subject, callable $proceed, $arg1)wraps the entire call (you decide whether to call$proceed(...)), andafterMethodName($subject, $result, $arg1)runs after (must return$resultor a modified version).$subjectis the original instance and is always passed first. -
03
Declare the plugin in di.xml under the right area
Three placement options:
etc/di.xml(both frontend and adminhtml),etc/frontend/di.xml(storefront only),etc/adminhtml/di.xml(admin only). The declaration is<type name="Original\Class"><plugin name="unique_name" type="Vendor\Module\Plugin\OriginalClassPlugin" sortOrder="10" disabled="false"/></type>.namemust be unique within that type — reusing a name across modules silently overrides the earlier declaration.sortOrdercontrols execution order when multiple plugins target the same method;disabled="true"turns a plugin off, which is how you neutralise a third-party plugin you don’t own. -
04
Run setup:di:compile to generate interceptor proxies
In
developermode Magento generates interceptors on the fly; inproductionmode they must be pre-compiled.bin/magento setup:di:compilewalks the DI graph, finds every class that has at least one plugin declared against it, and writesVendor\Module\Class\Interceptor extends Vendor\Module\Classintogenerated/code/. The interceptor’s method body wires thebefore/around/afterchain insortOrderand falls through toparent::methodName()for the original. -
05
Magento runs the interceptor at runtime
Every
new Original\Class()created via DI returns theInterceptorproxy instead of the bare original. When the intercepted method is called, plugins fire in this order: allbeforeplugins run first (ascendingsortOrder), each can mutate the arguments; then thearoundchain runs (also ascending) with each$proceedcalling into the next; the original method runs at the bottom of the chain; then allafterplugins run (descendingsortOrder), each receiving the previous plugin’s$result. This is whysortOrdermatters and whyafterplugins must always return$result.
Four scenarios where a plugin is the right extension point
Plugins are powerful but not universal — use them where they earn their keep. These four scenarios are where plugin beats preference, observer, or rewrite every time.
-
Modifying a Magento or third-party class without touching its source
The canonical “extend a class non-invasively” use case. Want to tweak how
Magento\Catalog\Model\Product::getFinalPrice()behaves for a single store view? Plugin it. Want to add a header to every response from a third-party module’s controller? Plugin it. The original class stays unmodified, upgrade-safe, and the same class can be plugged by N modules concurrently — the framework chains them insortOrder. This is the “why plugins exist” answer in one sentence. -
Adding pre-validation or post-processing to a repository method
Classic pattern:
beforeSave($subject, $entity)on a repository to enforce a custom validation rule before the entity is persisted, orafterGetById($subject, $result, $id)to hydrate an additional attribute on every load.beforeplugins can throw — raising a\Magento\Framework\Exception\LocalizedExceptionaborts the save entirely, which is the right pattern for “reject this if X”.afterplugins mutate the returned entity — useful for stitching in data from a non-Magento source. -
Wrapping an expensive call in cache
The textbook
around-plugin use case.aroundCalculatePrice($subject, callable $proceed, $product)reads from cache, calls$proceed($product)only on miss, and writes the result back. Same pattern works for any deterministic computation: tax calculation, shipping rate lookup, third-party API response, configurable-product price index. Pair with a TTL-keyed cache type registered inetc/cache.xmlso cache invalidation hooks into Magento’s standardcache:cleanflow. -
Intercepting a method that fires no dispatchable event
Magento dispatches events at well-known points (controller predispatch, model save, etc.) and observers hook those. But many core methods fire no event — nothing in
Magento\Catalog\Model\Product::isAvailable()dispatches, for example. If you need to modifyisAvailable()behaviour the answer is a plugin, not an observer. Rule of thumb: if the method dispatches an event, prefer an observer (looser coupling, easier to test); if it doesn’t, plugin is the only non-invasive option.
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.