Dispatching Your Own Events for Other Modules
Dispatching Your Own Events for Other Modules
~7 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026
Chapters 29 through 34 showed this module purely as a listener: it reacts to orders, credit memos, and the clock. But one aspect of the specification - rewards, marketing, a future email module - might just as well want to know when a customer's loyalty tier changes, without needing to know anything about PointsCalculator, LoyaltyTierBackend, or ExpirePoints. The solution: Mironsoft_Loyalty itself becomes the publisher of its own event.
Where the gap actually is
LoyaltyTierBackend (chapter 26) automatically recalculates loyalty_tier on every customer save - but neither AwardPointsOnOrderPlaced (chapter 30) nor ExpirePoints (chapter 33), which indirectly trigger that recalculation, ever learn whether the tier actually changed as a result - the backend model works silently, with no feedback channel. Instead of rebuilding that check twice in both callers, LoyaltyTierBackend itself takes on the job: compare the old and new value, and dispatch its own event on an actual change.
Naming convention for custom events
A generic name like tier_changed could collide with any other module that happens to use the same term. This series' convention: always the full module prefix, mirroring the config paths from chapter 7 - here mironsoft_loyalty_customer_tier_changed.
LoyaltyTierBackend, extended
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Model\Customer\Attribute\Backend;
use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend;
use Magento\Framework\Event\ManagerInterface;
use Mironsoft\Loyalty\Model\Config\LoyaltyConfig;
use Mironsoft\Loyalty\Model\Service\PointsCalculator;
/**
* Recalculates loyalty_tier on save (chapter 26) and, new in chapter 35, notifies
* other modules of an actual tier change via a custom event.
*/
class LoyaltyTierBackend extends AbstractBackend
{
public const EVENT_TIER_CHANGED = 'mironsoft_loyalty_customer_tier_changed';
/**
* @param PointsCalculator $pointsCalculator Pure business rule for determining a tier from a point total.
* @param LoyaltyConfig $loyaltyConfig Typed reader for the tier_thresholds configuration value.
* @param ManagerInterface $eventManager Dispatches EVENT_TIER_CHANGED for other modules to observe.
*/
public function __construct(
private readonly PointsCalculator $pointsCalculator,
private readonly LoyaltyConfig $loyaltyConfig,
private readonly ManagerInterface $eventManager
) {
}
/**
* Overwrites loyalty_tier with a freshly calculated value and dispatches
* EVENT_TIER_CHANGED when the recalculated value actually differs from the
* value the entity held before this save. $object deliberately carries no
* native type hint, unchanged from chapter 26's contravariance explanation.
*
* @param \Magento\Framework\DataObject $object The entity currently being saved (a Customer model here).
* @return $this
*/
public function beforeSave($object)
{
$previousTier = $object->getOrigData($this->getAttribute()->getAttributeCode());
$pointsBalance = (int) $object->getData('loyalty_points_balance');
$tier = $this->pointsCalculator->determineTier(
$pointsBalance,
$this->loyaltyConfig->getTierThresholdsJson()
);
$object->setData($this->getAttribute()->getAttributeCode(), $tier);
if ($previousTier !== null && $previousTier !== $tier) {
$this->eventManager->dispatch(self::EVENT_TIER_CHANGED, [
'customer' => $object,
'previous_tier' => $previousTier,
'new_tier' => $tier,
]);
}
return parent::beforeSave($object);
}
}How another module would listen
A completely independent, hypothetical marketing module wouldn't even need to list Mironsoft_Loyalty as a dependency in module.xml for this (though a sequence is a good idea in practice, to keep observer order - chapter 36 - predictable) - it only needs to know the event name as a string.
<!-- etc/events.xml of an unrelated, purely illustrative module -->
<config>
<event name="mironsoft_loyalty_customer_tier_changed">
<observer name="vendor_marketing_notify_tier_upgrade"
instance="Vendor\Marketing\Observer\NotifyTierUpgrade"/>
</event>
</config>Achtung: beforeSave() is the only convenient place to compare the attribute's old and new value - but it runs before the transaction has actually committed. If the real save fails afterward anyway, the event has already been dispatched even though the tier change never made it into the database. Unlike the entity save events in chapter 31, backend models have no equivalent to _save_commit_after - this trade-off is a deliberate limitation of attribute backend models as a dispatch point, not an oversight.
Tipp: Declaring constants like EVENT_TIER_CHANGED directly on the dispatching class (instead of repeating the string in several places) prevents exactly the kind of typo in an event name that only surfaces at runtime - the same motivation behind the TYPE_* constants on PointsLedgerInterface (chapter 6).
An event can have several observers. Chapter 36 clarifies in what order they actually run - and what to do when the order genuinely matters.