Your Own Events
Dispatch your own event with a name and a data array, and treat both as a published contract the moment anyone observes it.
$this->eventManager->dispatch('vendor_module_subject_after', ['key' => $value]);Any module can dispatch its own event. Inject Magento\Framework\Event\ManagerInterface and call dispatch() with a name and an array. Nothing has to be declared first: an event with no observers costs one configuration lookup and returns.
Naming
The schema for events.xml allows only [a-zA-Z0-9_] in an event name, so no dots and no hyphens. Both dispatch() and the configuration reader lowercase the name. The convention is vendor_module_subject_action, with _before and _after for a pair around a piece of work. The vendor and module prefix is what keeps your event from colliding with a core one.
The payload is the contract
There is no schema for event data. Your call site is the only documentation, so list the keys and their types in a docblock on the dispatching method. Once anyone observes your event, renaming a key breaks them silently: getData() returns null and nothing is logged.
Letting observers answer
Observers cannot return anything. If you want them to influence what happens next, put a mutable object in the payload and read it back after the dispatch. Magento does this itself with payment_method_is_active, which passes a result object carrying is_available. Pass a plain Magento\Framework\DataObject rather than your own working object, so an observer cannot reach state you did not mean to expose.
A dispatch is plain PHP and needs no command. Observers bound to it do: bin/magento cache:flush.
Example
<?php
// app/code/Vendor/Loyalty/Service/PointsAwarder.php
declare(strict_types=1);
namespace Vendor\Loyalty\Service;
use Magento\Framework\DataObject\Factory as DataObjectFactory;
use Magento\Framework\Event\ManagerInterface as EventManager;
class PointsAwarder
{
private EventManager $eventManager;
private DataObjectFactory $dataObjectFactory;
public function __construct(
EventManager $eventManager,
DataObjectFactory $dataObjectFactory
) {
$this->eventManager = $eventManager;
$this->dataObjectFactory = $dataObjectFactory;
}
/**
* vendor_loyalty_points_award_before
* customer_id int
* transport Magento\Framework\DataObject, key "points" (int), may be changed
*
* vendor_loyalty_points_award_after
* customer_id int
* points int, the amount finally awarded
*/
public function award(int $customerId, int $points): int
{
$transport = $this->dataObjectFactory->create(['points' => $points]);
$this->eventManager->dispatch(
'vendor_loyalty_points_award_before',
['customer_id' => $customerId, 'transport' => $transport]
);
$points = (int) $transport->getData('points');
$this->eventManager->dispatch(
'vendor_loyalty_points_award_after',
['customer_id' => $customerId, 'points' => $points]
);
return $points;
}
}
// Output: award(42, 100) returns 100 when nothing observes the before event.When to use it
- Let other modules react when your import finishes processing a supplier feed.
- Give integrators a documented point to veto or adjust a loyalty award.
- Announce that a custom shipment label was generated so a third party can email it.
- Offer an extension point in your module instead of asking people to override your class.
More examples
The observer that changes the answer
Bound to vendor_loyalty_points_award_before, this observer writes to the shared DataObject, and PointsAwarder reads the new value back on the line after the dispatch. This is the only mechanism by which an observer affects its dispatcher, and it works only because the dispatcher chose to offer it. Note the instanceof guard: if the observer is ever bound to a second event that has no transport key, getTransport() returns null and the arithmetic would be on nothing.
<?php
// app/code/Vendor/Promo/Observer/DoublePoints.php
declare(strict_types=1);
namespace Vendor\Promo\Observer;
use Magento\Framework\DataObject;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
class DoublePoints implements ObserverInterface
{
public function execute(Observer $observer): void
{
$transport = $observer->getEvent()->getTransport();
if (!$transport instanceof DataObject) {
return;
}
$transport->setData('points', (int) $transport->getData('points') * 2);
}
}What an integrator writes against your event
A custom event is bound exactly like a core one; the event manager does not distinguish them. Nothing validates that vendor_loyalty_points_award_before exists, so a typo here produces a file that loads cleanly and does nothing. This is the whole reason to write the event name and its keys into a docblock or a README: the observer author has no other source. Run bin/magento cache:flush after adding the file.
<!-- app/code/Vendor/Promo/etc/events.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="vendor_loyalty_points_award_before">
<observer name="vendor_promo_double_points"
instance="Vendor\Promo\Observer\DoublePoints"/>
</event>
</config>Dispatch once, not once per row
Observers are called inline, so a per-row dispatch multiplies someone else's code by your row count. For bulk work, dispatch a separate bulk event with the whole set. The trade-off is that observers then have to loop themselves, and an observer written for the per-row event will not fire, so say in your documentation which event covers which path rather than quietly changing one into the other.
<?php
// app/code/Vendor/Loyalty/Service/BatchAwarder.php (excerpt)
// Costly: one dispatch per row means every observer bound to the event
// runs once per row, synchronously, inside this loop.
foreach ($rows as $row) {
$this->eventManager->dispatch(
'vendor_loyalty_points_award_after',
['customer_id' => (int) $row['customer_id'], 'points' => (int) $row['points']]
);
}
// Cheaper: one dispatch, the whole set in the payload.
$this->eventManager->dispatch(
'vendor_loyalty_points_awarded_bulk',
['awards' => $rows]
);
Discussion