Chat on WhatsApp
Magento glossary

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.

How it works

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.

  1. 01

    Pick the class and method to intercept

    The target must be a public, non-final, non-static method of an instantiable class — Magento generates an Interceptor proxy only for classes that are constructed through DI. Methods on virtualType entries, on final classes, 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 in generated/code/ after setup:di:compile — if it does, plugin is fair game; if it doesn’t, use a different extension point.

  2. 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 or null to leave them), aroundMethodName($subject, callable $proceed, $arg1) wraps the entire call (you decide whether to call $proceed(...)), and afterMethodName($subject, $result, $arg1) runs after (must return $result or a modified version). $subject is the original instance and is always passed first.

  3. 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>. name must be unique within that type — reusing a name across modules silently overrides the earlier declaration. sortOrder controls 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.

  4. 04

    Run setup:di:compile to generate interceptor proxies

    In developer mode Magento generates interceptors on the fly; in production mode they must be pre-compiled. bin/magento setup:di:compile walks the DI graph, finds every class that has at least one plugin declared against it, and writes Vendor\Module\Class\Interceptor extends Vendor\Module\Class into generated/code/. The interceptor’s method body wires the before / around / after chain in sortOrder and falls through to parent::methodName() for the original.

  5. 05

    Magento runs the interceptor at runtime

    Every new Original\Class() created via DI returns the Interceptor proxy instead of the bare original. When the intercepted method is called, plugins fire in this order: all before plugins run first (ascending sortOrder), each can mutate the arguments; then the around chain runs (also ascending) with each $proceed calling into the next; the original method runs at the bottom of the chain; then all after plugins run (descending sortOrder), each receiving the previous plugin’s $result. This is why sortOrder matters and why after plugins must always return $result.

When to use

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 in sortOrder. 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, or afterGetById($subject, $result, $id) to hydrate an additional attribute on every load. before plugins can throw — raising a \Magento\Framework\Exception\LocalizedException aborts the save entirely, which is the right pattern for “reject this if X”. after plugins 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 in etc/cache.xml so cache invalidation hooks into Magento’s standard cache:clean flow.

  • 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 modify isAvailable() 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.

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.