Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Eigene Events auslösen: Custom Events für andere Module dispatchen

Eigene Events auslösen: Custom Events für andere Module dispatchen

~7 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026

Kapitel 29 bis 34 haben dieses Modul ausschließlich als Zuhörer gezeigt: es reagiert auf Bestellungen, Gutschriften und den Uhrzeiger. Ein Aspekt der Spezifikation - Rewards, Marketing, ein zukünftiges E-Mail-Modul - könnte aber genauso gut selbst wissen wollen, wann sich die Treue-Stufe eines Kunden ändert, ohne dafür PointsCalculator, LoyaltyTierBackend oder ExpirePoints zu kennen. Die Lösung: Mironsoft_Loyalty wird selbst zum Absender eines eigenen Events.

Wo die Lücke tatsächlich ist

LoyaltyTierBackend (Kapitel 26) berechnet loyalty_tier bei jedem Kunden-Speichern automatisch neu - aber weder AwardPointsOnOrderPlaced (Kapitel 30) noch ExpirePoints (Kapitel 33), die diese Neuberechnung indirekt auslösen, erfahren jemals, ob sich die Stufe dabei tatsächlich geändert hat - das Backend Model arbeitet still, ohne Rückkanal. Statt diese Prüfung doppelt in beiden Aufrufern nachzubauen, bekommt LoyaltyTierBackend selbst die Aufgabe: alten und neuen Wert vergleichen und bei einer echten Änderung ein eigenes Event dispatchen.

Namenskonvention für eigene Events

Ein generischer Name wie tier_changed würde mit jedem anderen Modul kollidieren können, das zufällig denselben Begriff verwendet. Die Konvention dieser Serie: immer mit dem vollen Modul-Präfix, analog zu den Config-Pfaden aus Kapitel 7 - hier mironsoft_loyalty_customer_tier_changed.

LoyaltyTierBackend erweitert

app/code/Mironsoft/Loyalty/Model/Customer/Attribute/Backend/LoyaltyTierBackend.php
<?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);
    }
}

So würde ein anderes Modul zuhören

Ein völlig unabhängiges, hypothetisches Marketing-Modul müsste Mironsoft_Loyalty dafür nicht einmal als Abhängigkeit in module.xml eintragen (auch wenn eine sequence in der Praxis sinnvoll ist, damit die Observer-Reihenfolge - Kapitel 36 - vorhersehbar bleibt) - es kennt nur den Event-Namen als String.

<!-- etc/events.xml eines fremden, hier nur illustrativen Moduls -->
<config>
    <event name="mironsoft_loyalty_customer_tier_changed">
        <observer name="vendor_marketing_notify_tier_upgrade"
                  instance="Vendor\Marketing\Observer\NotifyTierUpgrade"/>
    </event>
</config>

Achtung: beforeSave() ist der einzige Ort, an dem alter und neuer Wert am Attribut günstig verglichen werden können - aber er läuft, bevor die Transaktion wirklich committet ist. Schlägt das eigentliche Speichern danach doch noch fehl, wurde das Event trotzdem bereits dispatcht, obwohl die Tier-Änderung nie in der Datenbank ankam. Anders als bei den Entity-Save-Events in Kapitel 31 gibt es für Backend Models kein Äquivalent zu _save_commit_after - dieser Trade-off ist eine bewusste Einschränkung von Attribut-Backend-Modellen als Dispatch-Ort, nicht ein Versehen.

Tipp: Konstanten wie EVENT_TIER_CHANGED direkt an der dispatchenden Klasse zu deklarieren (statt den String an mehreren Stellen zu wiederholen) verhindert genau die Art von Tippfehler, die bei Event-Namen erst zur Laufzeit auffällt - dieselbe Motivation wie bei den TYPE_*-Konstanten in PointsLedgerInterface (Kapitel 6).

Ein Event kann mehrere Observer haben. Kapitel 36 klärt, in welcher Reihenfolge sie tatsächlich laufen - und was zu tun ist, wenn die Reihenfolge wirklich zählt.