How I implement Observers in Adobe Commerce (Magento)

How does the observer work in Magento 2?

Magento 2 provides a mechanism to subscribe to an event. It is interesting to note, that the event-observer mechanism is something that is less recommended in comparison to plugins (interceptors). Anyway, I use observers a lot due to the fact, that Magento 2 modules dispatch the events from their classes. And those classes were implemented in the way, that there is no way, sometimes, to add a plugin. With that said, let's see how I use observers classes, events and service classes in my day-to-day Magento 2 work.

Register an Observer

Any time I want to register an observer class, I use the events.xml file. This file is responsible for providing information about all observers that should be executed at some point. That said, every time, I experience a need to add an observer to extend Magento 2 functionality, I register an Observer.

Here are the events.xml example:

xml
<config>
<event name="sales_order_invoice_save_before">
<observer name="pronko_storecredit_invoice_save_before"
instance="Pronko\StoreCredit\Observer\Order\Invoice\SaveBeforeObserver" />
</event>
</config>

At a minimum, in the events.xml file, I should provide 3 things. The name of the event, I would like to trigger by observer class. The class name in the instance argument of the observer XML node. Finally, the virtual name of the observer provides context and information or the purpose of the observer.

Now that I have the configuration, let's create the observer class.

Observer Class

The observer class is responsible for processing the sales_order_invoice_save_before event once it is triggered.

namespace Pronko\StoreCredit\Observer\Order\Invoice;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Exception\LocalizedException;
use Pronko\StoreCredit\Service\StoreCreditOrderRepository;

class SaveBeforeObserver implements ObserverInterface
{
/**
* Construct
*
* @param StoreCreditOrderRepository $storeCreditOrder
*/
public function __construct(
private readonly StoreCreditOrderRepository $storeCreditOrder,
) {
}

/**
* Execute method
*
* @param Observer $observer
* @return void
* @throws LocalizedException
*/
public function execute(Observer $observer): void
{
$invoice = $observer->getData('invoice');

$creditOrder = $this->storeCreditOrder->getByOrderId((int) $invoice->getOrderId());

if (!$creditOrder->getId()) {
return;
}

$orderAmount = $creditOrder->getAmount();
$orderBaseAmount = $creditOrder->getBaseAmount();

$grandTotal = $invoice->getGrandTotal() + $orderAmount;
$grandTotal = abs($grandTotal) < 0.0001 ? 0 : $grandTotal;
$baseGrandTotal = $invoice->getBaseGrandTotal() + $orderBaseAmount;
$baseGrandTotal = abs($baseGrandTotal) < 0.0001 ? 0 : $baseGrandTotal;

$invoice->setGrandTotal($grandTotal);
$invoice->setBaseGrandTotal($baseGrandTotal);
}
}

The SaveBeforeObserver must implement the Magento\Framework\Event\ObserverInterface interface and the execute() method. The $observer argument of the execute() method provides access to the event data, that has been triggered as well as the event data. Usually, I use the getData() method of the Observer class to get access to the data I require. It helps to avoid any magic method usage since the Observer class implements the DataObject class that provides the hasData(), getData(), etc. methods.

Once I have the event data I need in the execute() method, I can then add any dependencies via the __construct() method of the observer class. One such dependency is the repository service class which is responsible for providing the ability to load a row from a database. I can pull any other additional dependencies if I require via the __contruct() method.

Service Class

I usually create a service class that is responsible for all the logic that I may put into the observer class. It is better because the service class is only responsible for the business logic part of my functionality when the observer class is an entry point that provides data. Think of the observer class as an action controller. I doubt putting any business logic into the controller class.

With the service class, I can focus only on the business logic.

Here is an example of the business logic that can be extracted from the observer class.

$orderAmount = $creditOrder->getAmount();
$orderBaseAmount = $creditOrder->getBaseAmount();

$grandTotal = $invoice->getGrandTotal() + $orderAmount;
$grandTotal = abs($grandTotal) < 0.0001 ? 0 : $grandTotal;
$baseGrandTotal = $invoice->getBaseGrandTotal() + $orderBaseAmount;
$baseGrandTotal = abs($baseGrandTotal) < 0.0001 ? 0 : $baseGrandTotal;

$invoice->setGrandTotal($grandTotal);
$invoice->setBaseGrandTotal($baseGrandTotal);

What should I do with the above code? I can put it into the method of the service class. Let's call this method as register().

public function register(OrderInterface $creditOrder, InvoiceInterface $invoice): void
{
$orderAmount = $creditOrder->getAmount();
$orderBaseAmount = $creditOrder->getBaseAmount();

$grandTotal = $invoice->getGrandTotal() + $orderAmount;
$grandTotal = abs($grandTotal) < 0.0001 ? 0 : $grandTotal;
$baseGrandTotal = $invoice->getBaseGrandTotal() + $orderBaseAmount;
$baseGrandTotal = abs($baseGrandTotal) < 0.0001 ? 0 : $baseGrandTotal;

$invoice->setGrandTotal($grandTotal);
$invoice->setBaseGrandTotal($baseGrandTotal);
}

The register() method now requires both $creditOrder and $invoice dependencies. That shouldn't be an issue to pass from the observer class or any other class that requires the business logic to be executed.

The observer class can now be refactored and the new service class dependency replaces the chunk of the code from the execute() method.

//Observer class

public function __construct(
private readonly StoreCreditOrderRepository $storeCreditOrder,
private readonly InvoiceService $service
) {
}

public function execute(Observer $observer): void
{
$invoice = $observer->getData('invoice');

$creditOrder = $this->storeCreditOrder->getByOrderId((int) $invoice->getOrderId());

if (!$creditOrder->getId()) {
return;
}

$this->service->register($creditOrder, $invoice);
}

Observer Areas

It is possible to register the observer class execution per area. For instance, if I want to execute the observer class only for events that happen in the admin area, I can create the etc/adminhtml/event.xml file and register the observer class in that file. Alternatively, if I want to execute the observer class every time a storefront event occurs, I can use the etc/frontend/event.xml file and register the observer.

In other cases, when the observer class should be executed for all areas that include base, adminhtml, frontend, and crontab (yes, there is a separate area for running cron), I can place my event.xml file in the etc/events.xml location.

Outro

Observers in Magento 2 are still a powerful mechanism when it comes to extending core Magento 2 functionality. I can create an observer class, and register it based on the business need in one of the Magento 2 areas (e.g. frontend, adminhtml) and implement the required changes. With the help of the service classes, I can even create unit tests that each responsible for covering the functionality as per the class implemented.