Plugins, observers or preferences? Choosing a Magento 2 extension point
How I decide where to hook into Adobe Commerce, and why picking the lightest option matters even more if you're heading towards SaaS.
Every customisation in Adobe Commerce comes down to one question: where do I hook in? The answer has a big effect on how painful your next upgrade is.
Start with the lightest option
- Configuration. If admin config or a
di.xmlargument can do it, stop there. - Events and observers. Use these to react to something that has already happened.
- Plugins (interceptors). Use these to change the input or output of a public method.
- Preferences. Only as a last resort, because only one module can win.
A plugin that survives upgrades
Use after plugins rather than around plugins where you can. They're cheaper, they play nicely with other modules, and you can't forget to call $proceed.
<?php
declare(strict_types=1);
namespace Vendor\Catalog\Plugin;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
final class AddLeadTimeToProduct
{
public function __construct(
private readonly LeadTimeProvider $leadTimeProvider,
) {}
public function afterGet(
ProductRepositoryInterface $subject,
ProductInterface $product,
): ProductInterface {
$product->getExtensionAttributes()
?->setLeadTimeDays($this->leadTimeProvider->forSku($product->getSku()));
return $product;
}
}
<!-- etc/di.xml -->
<type name="Magento\Catalog\Api\ProductRepositoryInterface">
<plugin name="vendor_catalog_lead_time"
type="Vendor\Catalog\Plugin\AddLeadTimeToProduct" />
</type>
When an observer is the better fit
Observers are for side effects: something happened, and something else should happen because of it. They can't change a method's return value, and that limit is a feature. Nobody reading your observer has to wonder whether it quietly changed a price.
<?php
declare(strict_types=1);
namespace Vendor\Erp\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\MessageQueue\PublisherInterface;
use Magento\Sales\Api\Data\OrderInterface;
final class QueueOrderExport implements ObserverInterface
{
public function __construct(
private readonly PublisherInterface $publisher,
) {}
public function execute(Observer $observer): void
{
/** @var OrderInterface $order */
$order = $observer->getEvent()->getData('order');
// Don't call the ERP here. Queue it, so checkout never waits on another system.
$this->publisher->publish('vendor.erp.order.export', (string) $order->getEntityId());
}
}
The important line is the comment. An observer on sales_order_place_after that makes a synchronous HTTP call is one of the most common causes of slow or failed checkouts I see in audits.
The around-plugin trap
around plugins look convenient because you get the input and the output in one place. They cost more than they look:
- They hide the call chain. If you forget
$proceed(), or call it twice, every plugin after yours behaves differently. - They add stack depth to every call. On hot methods such as price or stock lookups, that shows up in profiles.
- They block other modules. A
beforeorafterplugin composes cleanly with other modules. Anaroundplugin that short-circuits doesn't.
If you need both input and output, a before plus an after plugin is usually clearer than one around.
A quick decision table
| You want to… | Use |
|---|---|
| Change behaviour with a setting | Configuration or a di.xml argument |
| React after something happened | Observer, and queue anything slow |
| Adjust a public method's arguments | before plugin |
| Adjust a public method's result | after plugin |
| Replace a method's logic entirely | around plugin, and write down why |
| Replace a whole class | Preference, as a last resort |
Why this matters for SaaS
Adobe Commerce as a Cloud Service doesn't run your PHP. Observers on well-known events map fairly naturally to I/O Events handled in App Builder. before and after plugins on service contracts often have a webhook equivalent. Preferences have no equivalent at all. Picking the lightest extension point today is the cheapest migration work you'll ever do.