Observer or Plugin

Use an observer when an event already fires where you need it; use a plugin when you need the arguments or the return value.

Both change behaviour without editing the original file, and they are not interchangeable. An observer reacts to an event that the original code chose to dispatch. A plugin wraps a public method whether or not that method expected to be wrapped. So ask two questions: is an event dispatched at the exact point you care about, and do you need to touch data on its way in or out.

What you needUse
React to something, and an event is dispatched at that pointObserver
Change what a method returnsafter plugin
Change the arguments a method receivesbefore plugin
Stop a method running at allaround plugin, last resort
React to something reached through many code paths, such as any model saveObserver on the generic event
Change a final, private, protected or static methodNeither; interception cannot reach it

Why the table looks like that

An observer receives the payload and nothing else. It cannot see the method's arguments, and its return value is discarded. A plugin lives in the generated interceptor around the method, so before can rewrite the arguments and after can rewrite the result.

When both would work, prefer the observer. It needs no generated interceptor for a class that may be built thousands of times in a product listing, and it will not fight another module's plugin on the same method.

Run bin/magento cache:flush after editing events.xml or di.xml, and bin/magento setup:di:compile in production mode, because a new plugin changes the target class's generated interceptor.

Example

Example · php
<?php
// app/code/Vendor/Wholesale/Observer/HidePurchaseOrder.php
declare(strict_types=1);

namespace Vendor\Wholesale\Observer;

use Magento\Framework\DataObject;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Payment\Model\MethodInterface;

class HidePurchaseOrder implements ObserverInterface
{
    public function execute(Observer $observer): void
    {
        $method = $observer->getEvent()->getMethodInstance();
        $result = $observer->getEvent()->getResult();

        if (!$method instanceof MethodInterface || !$result instanceof DataObject) {
            return;
        }

        if ($method->getCode() === 'purchaseorder') {
            $result->setData('is_available', false);
        }
    }
}

When to use it

  • Hide a payment method for a particular customer group or country.
  • Append delivery wording to the label a payment method shows at checkout.
  • Decide, during code review, whether a colleague's plugin should have been an observer.
  • Work out why a plugin on a protected method never runs.

More examples

Binding the observer

payment_method_is_active is dispatched by Magento\Payment\Model\Method\Adapter::isAvailable() and by the older AbstractMethod, with the keys result, method_instance and quote. It is the rare event whose payload is designed to be written to: the dispatcher reads is_available back off the result object and returns it. The binding is in the global etc/events.xml so it also applies when an admin creates an order. Payment method availability is cached nowhere, but the configuration is, so run bin/magento cache:flush.

Example · xml
<!-- app/code/Vendor/Wholesale/etc/events.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="payment_method_is_active">
        <observer name="vendor_wholesale_hide_purchase_order"
                  instance="Vendor\Wholesale\Observer\HidePurchaseOrder"/>
    </event>
</config>

No event here, so a plugin

No event is dispatched around getTitle(), and the thing you need to change is the returned string, so an observer cannot help. An after plugin receives the subject and the result and returns the replacement. Note that $result is left untyped with a docblock instead: Magento's configuration-backed getters can return null when the value is unset, and a strict string parameter would then throw. At checkout the reader now sees the longer label beside the Check / Money order option.

Example · php
<?php
// app/code/Vendor/Wholesale/Plugin/AppendCheckmoWording.php
declare(strict_types=1);

namespace Vendor\Wholesale\Plugin;

use Magento\Payment\Model\MethodInterface;

class AppendCheckmoWording
{
    /**
     * @param string|null $result
     */
    public function afterGetTitle(MethodInterface $subject, $result): string
    {
        if ($subject->getCode() !== 'checkmo') {
            return (string) $result;
        }

        return (string) $result . ' (allow three working days to clear)';
    }
}

Declaring that plugin

Declaring the plugin against the interface applies it to every payment method implementation, which is what you want when you only act on one code. sortOrder exists here because plugins on the same method are ordered; observers have no equivalent attribute. Apply with bin/magento cache:flush in developer mode, and bin/magento setup:di:compile in production mode so the interceptor is regenerated.

Example · xml
<!-- app/code/Vendor/Wholesale/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Payment\Model\MethodInterface">
        <plugin name="vendor_wholesale_append_checkmo_wording"
                type="Vendor\Wholesale\Plugin\AppendCheckmoWording"
                sortOrder="10"/>
    </type>
</config>

Discussion

  • Be the first to comment on this lesson.