Writing an Observer
An observer is one class with one execute() method that reads the event payload defensively, because it may be bound to several events.
public function execute(\Magento\Framework\Event\Observer $observer): voidAn observer is a class that implements Magento\Framework\Event\ObserverInterface. The interface declares one method, execute(Observer $observer), and its return value is thrown away. The only thing an observer can do is act on what it was handed.
Reading the payload
The argument is a wrapper. It holds the Magento\Framework\Event object under the key event, and it also holds every key of the dispatched data array directly. Both of these reach the same object:
$order = $observer->getEvent()->getOrder();
$order = $observer->getData('order');
The first line is shorthand for the second. Observer and Event both extend Magento\Framework\DataObject, whose __call() turns getOrder() into getData('order'). They are the same call, so expect to read either form in other people's code.
Do not assume what dispatched it
One observer class can be bound to several events, and a generic event such as catalog_product_save_after fires for every product in the system, including imports and CLI runs. A key that was not passed returns null instead of failing. Check what you got before using it, with instanceof against an interface.
Making it load
If the class does not implement ObserverInterface, developer mode throws a LogicException naming the class, while production mode logs a warning and skips it silently. After adding the class and its events.xml entry, run bin/magento cache:flush; in production mode also run bin/magento setup:di:compile.
Example
<?php
// app/code/Vendor/OrderFeed/Observer/LogPlacedOrder.php
declare(strict_types=1);
namespace Vendor\OrderFeed\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Sales\Api\Data\OrderInterface;
use Psr\Log\LoggerInterface;
class LogPlacedOrder implements ObserverInterface
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function execute(Observer $observer): void
{
// Shorthand for $observer->getData('order'). The same object.
$order = $observer->getEvent()->getOrder();
if (!$order instanceof OrderInterface) {
return;
}
$this->logger->info(
'Order placed',
[
'increment_id' => $order->getIncrementId(),
'grand_total' => $order->getGrandTotal(),
]
);
}
}When to use it
- Log every placed order to a file for later reconciliation.
- Record which admin user saved a product, for an audit trail.
- React to a cart line being added so a storefront banner can be flagged.
- Read an order total safely in code that also observes customer saves.
More examples
One class bound to two events
The same class answers both events, which is why its execute() cannot assume a key. The product event carries product, the customer event carries customer, and both also carry data_object pointing at the saved object, so getData('data_object') is the portable read. Use $observer->getEvent()->getName() when the behaviour has to differ by event. In etc/adminhtml the binding fires for admin requests only. Run bin/magento cache:flush.
<!-- app/code/Vendor/Audit/etc/adminhtml/events.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="catalog_product_save_after">
<observer name="vendor_audit_entity_saved"
instance="Vendor\Audit\Observer\EntitySaved"/>
</event>
<event name="customer_save_after">
<observer name="vendor_audit_entity_saved"
instance="Vendor\Audit\Observer\EntitySaved"/>
</event>
</config>Snake case in the magic getter
getQuoteItem() is resolved by DataObject::__call(), which inserts an underscore before each capital and lowercases the result, so it asks for quote_item. That conversion is the reason a data key written in camelCase is unreachable through a magic getter. Reach for the explicit getData() call whenever you are unsure how a key was spelled by the dispatcher.
<?php
// app/code/Vendor/CartLog/Observer/ProductAdded.php (excerpt)
use Magento\Quote\Api\Data\CartItemInterface;
public function execute(Observer $observer): void
{
// checkout_cart_product_add_after carries 'quote_item' and 'product'.
$item = $observer->getEvent()->getQuoteItem();
// Longhand for exactly the same lookup:
$item = $observer->getData('quote_item');
if (!$item instanceof CartItemInterface) {
return;
}
$this->logger->info('Added to cart', ['sku' => $item->getSku()]);
}Two ways to write the constructor
Both forms give the class a LoggerInterface. Magento's dependency injection reads the constructor signature either way, so promotion changes nothing about how the object is built. Magento 2.4.9 core mostly uses the longhand, so you will read it constantly. Changing any constructor signature regenerates factories and interceptors, so run bin/magento setup:di:compile in production mode after editing one.
<?php
// app/code/Vendor/OrderFeed/Observer/LogPlacedOrder.php (constructor, two equivalent forms)
declare(strict_types=1);
// Longhand: declare the property, then assign it.
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
// Shorthand: constructor property promotion, PHP 8.0 and later. Same result.
public function __construct(
private readonly LoggerInterface $logger
) {
}
Discussion