Skip to content
Sohel Rana
← Writing

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.

2 min read
Magento 2Architecture

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

  1. Configuration. If admin config or a di.xml argument can do it, stop there.
  2. Events and observers. Use these to react to something that has already happened.
  3. Plugins (interceptors). Use these to change the input or output of a public method.
  4. 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 before or after plugin composes cleanly with other modules. An around plugin 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 settingConfiguration or a di.xml argument
React after something happenedObserver, and queue anything slow
Adjust a public method's argumentsbefore plugin
Adjust a public method's resultafter plugin
Replace a method's logic entirelyaround plugin, and write down why
Replace a whole classPreference, 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.

Get new articles in your reader via the RSS feed.