Magento gives you three main ways to change core behaviour without editing core files: plugins (interceptors), observers (events) and preferences (class substitution). Choosing the wrong one is behind most fragile customisations we unpick in audits - the rule of thumb is simple, but the details matter.
Plugins: The Default Choice
A plugin intercepts a public method call with before, after or around logic:
<type name="Magento\Catalog\Model\Product">
<plugin name="acme_product_name_suffix"
type="Acme\Catalog\Plugin\ProductNameSuffix"/>
</type>
public function afterGetName(Product $subject, string $result): string
{
return $result . ' (VAT included)';
}
beforecan modify argumentsaftercan modify the return valuearoundcan wrap, modify both, or skip the original entirely (call$proceed()or don’t)
Plugins are the most surgical tool: multiple modules’ plugins chain together cleanly via sortOrder, they work on any public method of a non-final class, and they are the mechanism least likely to conflict with other extensions.
Limits: plugins cannot touch private, protected, static or final methods, or classes instantiated before interception is possible. And an around plugin that forgets $proceed() silently replaces core behaviour - use before/after unless you genuinely need around.
Observers: Reacting to Events
Observers listen for events the core (or a module) explicitly dispatches:
<event name="sales_order_place_after">
<observer name="acme_send_to_erp"
instance="Acme\Erp\Observer\SendOrder"/>
</event>
Use observers when you want to react to something that happened - order placed, customer registered, product saved - rather than change how it happens. The contract is explicit: an event is a documented extension point that survives upgrades better than intercepting an arbitrary method.
Limits: you can only observe events that exist. If the core does not dispatch an event where you need one, you are back to plugins (or dispatching your own event from a plugin).
Preferences: The Sledgehammer
A preference replaces one class with another globally:
<preference for="Magento\Catalog\Model\Product"
type="Acme\Catalog\Model\CustomProduct"/>
Every place the object manager creates the original class, it creates yours instead. This is powerful and dangerous in equal measure:
- Only one module can win a preference on a given class - two extensions preferring the same class is a guaranteed conflict
- Your replacement must satisfy the original’s full contract, including constructor arguments, or DI compilation fails
- Core upgrades that change the original class can silently break your subclass
Preferences are appropriate for substituting an interface implementation you control end-to-end, or replacing a service with a genuinely alternative implementation. They are almost never right for “tweak this one method” - that is a plugin.
The Decision Rule
- Is there an event for it? Use an observer.
- No event, but a public method? Use a plugin,
before/afterfirst. - Genuinely replacing an implementation? Consider a preference - and document why a plugin could not do it.
Following that order keeps your customisations composable with other extensions and survivable across upgrades. Breaking it is why some stores cannot be upgraded without a month of archaeology.