Magento Observers and Events — Event Dispatching and Custom Events
In this tutorial, you'll learn how Magento's event system works with observers, how to react to core events like order placement, and how to dispatch custom events from your own modules.
What You'll Learn
- How Magento's event-driven architecture enables loosely coupled extensions
- How to declare observers in events.xml for frontend, adminhtml, and global areas
- How to create observer classes that react to events
- How to dispatch custom events with data
- Which core events are most useful for common customizations
- Best practices for event naming and performance
Why It Matters
The event system is Magento's second major extension mechanism alongside plugins. While plugins intercept method calls, events let you react to actions that happen across the system. When a customer places an order, adds a product to the cart, or logs in, Magento fires events. You can write an observer that listens for any of these events and executes custom code. This is essential for tasks like sending custom emails, updating external systems, applying custom logic after checkout, or integrating with third-party APIs. Events make your code decoupled — your observer does not need to modify the core sales module to react to an order being placed.
Real-World Use
An online electronics store needs to send order data to their warehouse management system (WMS) every time an order is placed. The WMS needs the order items, quantities, and shipping address in XML format. Instead of modifying the core checkout process, you create an observer on the sales_order_place_after event. The observer collects the order data, transforms it to XML, and sends it to the WMS endpoint. If the WMS changes its API, you only update the observer. The checkout code remains untouched. If the warehouse is offline, the observer logs the failure and retries later without affecting the customer's checkout experience.
Learning Path
flowchart LR A["25: Module Structure"] --> B["26: Dependency Injection"] B --> C["27: Plugins"] C --> D["28: Observers and Events
You are here"]:::current classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
What Is the Event System
Magento's event system follows the observer design pattern. When something important happens in the system — a product is saved, an order is placed, a customer logs in — the system dispatches an event with associated data. Any number of observers can listen for that event and execute code in response.
The key advantage: loose coupling. The code that dispatches the event does not know which observers will react. Observers can be added or removed without changing the code that fires the event. This allows multiple extensions to react to the same action without conflicting.
Event Flow
Product Save Action
|
v
Event Dispatched: catalog_product_save_after
|
+---> Observer 1: Update search index
+---> Observer 2: Send notification
+---> Observer 3: Sync to ERP
|
v
Original action continues
The event is dispatched synchronously. All observers run before the next line of code executes.
events.xml Structure
Events are declared in events.xml files. Like di.xml, events.xml can be placed in multiple area directories:
| Location | Scope |
|---|---|
etc/events.xml |
Global (all areas) |
etc/frontend/events.xml |
Storefront only |
etc/adminhtml/events.xml |
Admin panel only |
etc/webapi_rest/events.xml |
REST API only |
etc/<a href="/apis/graphql/">Graphql</a>/events.xml |
GraphQL only |
events.xml Structure
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="sales_order_place_after">
<observer name="vendor_module_order_observer"
instance="Vendor\Module\Observer\OrderPlace"
shared="false"/>
</event>
</config>
| Attribute | Purpose |
|---|---|
event name |
The unique event identifier |
observer name |
Unique observer identifier within this event |
instance |
Fully qualified observer class name |
shared |
Whether to reuse the observer instance (default true) |
Observer Classes
An observer class implements \Magento\Framework\Event\ObserverInterface and defines an execute method.
<?php
namespace Vendor\Module\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
use Psr\Log\LoggerInterface;
class OrderPlace implements ObserverInterface
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function execute(Observer $observer)
{
$order = $observer->getEvent()->getOrder();
$this->logger->info('Order placed: ' . $order->getIncrementId());
// Custom logic: send to external system, update inventory, etc.
}
}
Accessing Event Data
The Observer object provides access to the event data:
$event = $observer->getEvent();
// Get specific data passed with the event
$order = $event->getOrder();
$product = $event->getProduct();
$quote = $event->getQuote();
$customer = $event->getCustomer();
// Get all data as an array
$data = $event->getData();
The available getter methods depend on the event. Each event passes specific objects. You can also access data generically:
$order = $observer->getData('order');
// or
$order = $observer->getEvent()->getData('order');
Core Events
Magento fires hundreds of events. Here are the most useful ones for common customizations:
Checkout Events
| Event | When | Data |
|---|---|---|
checkout_cart_add_product_complete |
After product added to cart | product, request, quote_item |
checkout_cart_update_item_complete |
After cart item quantity updated | item, info |
checkout_cart_product_add_after |
After add to cart (lower level) | quote_item, product |
checkout_onepage_controller_success_action |
After order placed on frontend | order_ids |
checkout_submit_all_after |
After checkout submission | quote, order |
Sales Events
| Event | When | Data |
|---|---|---|
sales_order_place_after |
After order is placed | order |
sales_order_save_after |
After order is saved | order |
sales_order_invoice_register |
After invoice is created | invoice, order |
sales_order_shipment_save_after |
After shipment is saved | shipment, order |
sales_order_creditmemo_save_after |
After credit memo is saved | creditmemo, order |
Customer Events
| Event | When | Data |
|---|---|---|
customer_login |
After customer logs in | customer |
customer_logout |
After customer logs out | customer |
customer_register_success |
After customer registration | customer |
customer_address_save_after |
After address is saved | customer_address, customer |
Catalog Events
| Event | When | Data |
|---|---|---|
catalog_product_save_after |
After product is saved | product |
catalog_product_delete_after |
After product is deleted | product |
catalog_category_save_after |
After category is saved | category |
catalog_product_import_before |
Before product import | <a href="/design-patterns/adapter/">Adapter</a> |
catalog_product_import_after |
After product import | adapter |
Admin Events
| Event | When | Data |
|---|---|---|
admin_user_login_success |
After admin login | user |
admin_system_config_changed_section_ |
After config section saved | (depends on section) |
controller_action_predispatch_adminhtml |
Before any admin action | controller_action |
Controller Events
| Event | When | Data |
|---|---|---|
controller_action_predispatch |
Before any controller action | controller_action |
controller_action_postdispatch |
After any controller action | controller_action |
Example: React to Add to Cart
Create an observer that logs every add-to-cart action and sends data to an analytics service:
events.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="checkout_cart_add_product_complete">
<observer name="vendor_module_add_to_cart"
instance="Vendor\Module\Observer\AddToCart"
shared="false"/>
</event>
</config>
Observer Class
<?php
namespace Vendor\Module\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
use Psr\Log\LoggerInterface;
class AddToCart implements ObserverInterface
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function execute(Observer $observer)
{
$product = $observer->getEvent()->getProduct();
$quoteItem = $observer->getEvent()->getQuoteItem();
$this->logger->info('Product added to cart', [
'sku' => $product->getSku(),
'name' => $product->getName(),
'qty' => $quoteItem->getQty(),
'price' => $quoteItem->getPrice()
]);
}
}
Example: Order Confirmation to External API
<?php
namespace Vendor\Module\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
use Magento\Framework\HTTP\Client\Curl;
use Psr\Log\LoggerInterface;
class OrderSync implements ObserverInterface
{
private $curl;
private $logger;
public function __construct(Curl $curl, LoggerInterface $logger)
{
$this->curl = $curl;
$this->logger = $logger;
}
public function execute(Observer $observer)
{
$order = $observer->getEvent()->getOrder();
$data = [
'order_id' => $order->getIncrementId(),
'total' => $order->getGrandTotal(),
'items' => []
];
foreach ($order->getAllItems() as $item) {
$data['items'][] = [
'sku' => $item->getSku(),
'qty' => $item->getQtyOrdered(),
'price' => $item->getPrice()
];
}
try {
$this->curl->post('https://wms.example.com/api/orders', json_encode($data));
} catch (\Exception $e) {
$this->logger->error('WMS sync failed: ' . $e->getMessage());
}
}
}
Dispatching Custom Events
You can dispatch your own events from any PHP class. This allows other modules to react to actions in your module.
<?php
namespace Vendor\Module\Model;
use Magento\Framework\Event\ManagerInterface as EventManager;
class PointsManager
{
private $eventManager;
public function __construct(EventManager $eventManager)
{
$this->eventManager = $eventManager;
}
public function awardPoints($customerId, $points)
{
// Award points logic...
// Dispatch custom event so other modules can react
$this->eventManager->dispatch(
'vendor_module_points_awarded',
[
'customer_id' => $customerId,
'points' => $points,
'customer' => $this->customerRepository->getById($customerId)
]
);
}
}
Event Naming Conventions
Follow Magento's naming conventions for custom events:
{module}_{action}_{context}
| Pattern | Example |
|---|---|
vendor_module_points_awarded |
Points awarded in loyalty module |
vendor_module_order_export_before |
Before order export |
vendor_module_api_request_send_after |
After API request |
vendor_module_customer_sync_after |
After customer sync |
Use lowercase with underscores. The name should clearly describe when the event fires.
Listening to Custom Events
Other modules listen to your custom event using the same events.xml structure:
<event name="vendor_module_points_awarded">
<observer name="other_module_points_listener"
instance="Other\Module\Observer\PointsAwarded"/>
</event>
<?php
namespace Other\Module\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
class PointsAwarded implements ObserverInterface
{
public function execute(Observer $observer)
{
$customerId = $observer->getEvent()->getData('customer_id');
$points = $observer->getEvent()->getData('points');
// Send email notification, update external loyalty system, etc.
}
}
Global vs Area Events
Events declared in different area files fire only in those areas:
etc/frontend/events.xml— observers only run during storefront requestsetc/adminhtml/events.xml— observers only run during admin requestsetc/events.xml(global) — observers run in all areas
This is important for performance. If an observer only makes sense on the frontend (like sending a post-checkout email), declare it in etc/frontend/events.xml. The observer is not loaded during admin requests, saving memory and execution time.
Performance Considerations
Observers run synchronously during the request. A slow observer delays the page response. Consider these strategies:
Keep observers fast. Database queries, API calls, and file operations in observers should be minimal. If you need to perform heavy work (sending emails, syncing with external systems), use Magento's message queue system instead.
Use
shared="false"judiciously. Settingshared="false"creates a new observer instance for every event dispatch. This increases memory usage. Only disable sharing if the observer maintains state that changes between dispatches.Avoid long-running operations in checkout events. Events like
checkout_submit_all_afterexecute during the checkout process. If your observer takes 10 seconds to call an external API, the customer waits 10 seconds for the order confirmation page. Use async processing for heavy tasks.Register events in the correct area. An observer declared in
etc/adminhtml/events.xmlis never loaded on the frontend, reducing memory and improving performance.
Using Message Queues for Heavy Observers
For heavy operations, dispatch a message to a queue instead of executing inline:
public function execute(Observer $observer)
{
$order = $observer->getEvent()->getOrder();
// Instead of API call here, publish to queue
$this->messageQueue->publish('order.sync', $order->getId());
}
The queue consumer processes the message asynchronously, keeping the checkout fast.
Events vs Plugins
When should you use an event observer vs a plugin?
| Scenario | Best Approach |
|---|---|
| React to an action (order placed, product saved) | Event observer |
| Modify method arguments or return values | Plugin |
| Fire-and-forget notifications | Event observer |
| Many extensions need to react to same action | Event observer |
| Need to conditionally skip execution | Plugin (around) |
| No event exists for your use case | Plugin (more granular control) |
| Integrate with external systems | Event observer (for decoupling) |
In general: use events when you want to react to something happening, use plugins when you want to modify how something works.
Common Mistakes
Using events when a plugin is more appropriate. Events only tell you that something happened. They do not let you modify the data flowing through the system. If you need to change the product name before it is saved, a before plugin on
saveis more appropriate than an observer oncatalog_product_save_before.Wrong area declaration. Placing an observer in
etc/frontend/events.xmlwhen it should be inetc/adminhtml/events.xmlcauses the observer to never fire for admin actions. Always verify the area where your event fires.Forgetting to clear caches. Event observer declarations are cached. If you add a new observer and it does not fire, run
bin/magento cache:clean configorbin/magento cache:flushto refresh the event configuration cache.Heavy synchronous operations. Making HTTP requests or running complex database queries inside an observer blocks the page response. Customers experience slow checkout, slow product saves, or timeouts. Use message queues for any operation that takes more than a few milliseconds.
Not checking if data exists. Event data is not guaranteed to be present. Always check
$observer->getEvent()->getOrder()for null before using it. Some events fire in multiple contexts, and certain data might not be available in all contexts.
Practice Questions
What is the difference between a global event and an area-specific event? Answer: A global event (declared in
etc/events.xml) fires in all areas: frontend, adminhtml, web API, and GraphQL. An area-specific event (declared inetc/frontend/events.xmloretc/adminhtml/events.xml) only fires in that area. Area-specific declarations improve performance by only loading observers when needed.How do you access the order object in a
sales_order_place_afterobserver? Answer: Use$observer->getEvent()->getOrder(). Thesales_order_place_afterevent passes the order object as event data. If the order might not be available, check for null before accessing its methods.What are the advantages of dispatching custom events in your module? Answer: Custom events allow other modules to react to your module's actions without modifying your code. This enables extension developers to integrate with your module without conflicts. It also decouples your module's core logic from secondary concerns like email notifications, API syncs, and logging.
Challenge: Create a module that introduces a "Gold Customer" tier. Use an observer on
checkout_submit_all_afterto calculate the customer's total spend after each order. If the total exceeds $1000, dispatch a custom eventvendor_loyalty_gold_status_earned. Create a second observer that listens to this custom event and sends a congratulatory email. Use area-specific declarations appropriately.
FAQ
{{< faq "What is the shared attribute in the observer declaration?" "The shared attribute controls whether the observer instance is reused across multiple dispatches of the same event (shared=\"true\", default) or a new instance is created each time (shared=\"false\"). Use shared=\"false\" when the observer maintains state that should not persist between dispatches." >}}
Mini Project
Your task: Create an order tracking and notification module using events.
- Create
Vendor_OrderTrackermodule withregistration.phpandmodule.xml. - Create an observer on
sales_order_place_afterthat saves the order data to a customvendor_ordertracker_logtable with columns:log_id,order_id,customer_email,total,status,created_at. - Create an observer on
sales_order_shipment_save_afterthat sends an email notification to the customer when their order ships. Use theTransportBuilderclass to send the email. - Create a custom event
vendor_ordertracker_order_exportdispatched from a CLI command. Another observer listens to this event and exports the order to a CSV file invar/export/. - Create the
Setup/InstallSchema.phpfor the custom table. - Enable the module and place a test order. Verify the log entry is created and the shipment triggers an email.
- Run the CLI command and verify the CSV export contains the order data.
This exercise covers the most common event-driven patterns in Magento: data logging, email notifications, and data export. These are the foundations of most B2B and integration projects.
What's Next
Now that you understand all four extension mechanisms (preferences, plugins, events, and observers), the next module covers API development:
Continue to Lesson 29: REST API and GraphQL — Build and consume Magento APIs.
Related lessons:
- Magento Plugins — Compare plugins vs events for your next feature
- Magento Dependency Injection — Understand observer constructor injection
- MySQL CRUD Operations — Design the database tables for your observers
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro