Observer / Event Pattern in Magento 2: Events dispatchen und Observer entwickeln

· Lesezeit: ca. 13 Minuten · Teil der Serie: Design Patterns in Magento 2

Event
dispatch
Design Pattern #4 · Behavioral / Publisher-Subscriber

Observer / Event
Pattern in Magento 2

Eigene Events dispatchen, Observer registrieren, events.xml, die wichtigsten Core Events und die Frage: Plugin oder Observer? – vollständig beantwortet.

⏱ 13 Min. Lose Kopplung PHP 8.4 Multi-Modul

Observer Pattern: Entkopplung durch Events

Das Observer Pattern – in modernen Systemen auch als Publisher-Subscriber oder Event-Driven Pattern bekannt – löst das Problem der Kopplung zwischen Modulen auf elegante Weise. Ein Modul dispatcht ein Event und erklärt damit: „Hier ist etwas passiert." Es weiß nicht und muss nicht wissen, wer darauf reagiert. Andere Module registrieren Observer, die auf genau dieses Event warten.

Das Ergebnis: Keine direkte Abhängigkeit zwischen dem Modul, das das Event auslöst, und den Modulen, die darauf reagieren. Module können hinzugefügt oder entfernt werden, ohne den Event-Dispatcher zu berühren.

1. Events dispatchen: der EventManager

Ein Event wird mit dem Magento\Framework\Event\ManagerInterface ausgelöst. Man injiziert das Interface, ruft dispatch() mit einem Event-Namen und optionalen Daten auf. Der Event-Name ist eine Snake-Case-Zeichenkette, die das Event eindeutig identifiziert.


<?php
declare(strict_types=1);

namespace Mironsoft\Blog\Model;

use Magento\Framework\Event\ManagerInterface as EventManager;
use Mironsoft\Blog\Api\Data\PostInterface;
use Mironsoft\Blog\Api\PostRepositoryInterface;

class PostPublisher
{
    public function __construct(
        private readonly PostRepositoryInterface $postRepository,
        private readonly EventManager $eventManager
    ) {}

    /**
     * Publishes a blog post and dispatches events before and after.
     */
    public function publish(int $postId): PostInterface
    {
        $post = $this->postRepository->getById($postId);

        // Dispatch BEFORE event — observers can read/modify context
        $this->eventManager->dispatch(
            'mironsoft_blog_post_publish_before',
            ['post' => $post]
        );

        $post->setStatus('published');
        $post->setPublishedAt(date('Y-m-d H:i:s'));
        $savedPost = $this->postRepository->save($post);

        // Dispatch AFTER event — observers react to completed action
        $this->eventManager->dispatch(
            'mironsoft_blog_post_publish_after',
            [
                'post'       => $savedPost,
                'post_id'    => $savedPost->getId(),
                'author_id'  => $savedPost->getAuthorId(),
            ]
        );

        return $savedPost;
    }
}

Naming Convention: Event-Namen sind Snake-Case. Magento nutzt das Muster modulname_entität_aktion oder modulname_entität_aktion_before/after. Eigene Events immer mit Modul-Präfix benennen, um Namenskollisionen zu vermeiden.

2. Observer erstellen

Ein Observer implementiert das ObserverInterface mit einer einzigen Methode: execute(Observer $observer). Alle Event-Daten werden über das $observer-Objekt abgerufen. Observer sind zustandslos – kein Zustand zwischen Aufrufen.


<?php
declare(strict_types=1);

namespace Mironsoft\Notifications\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Mironsoft\Blog\Api\Data\PostInterface;
use Mironsoft\Notifications\Service\EmailNotifier;
use Psr\Log\LoggerInterface;

/**
 * Observer: Sends author notification email when a blog post is published.
 */
class SendPublishNotificationObserver implements ObserverInterface
{
    public function __construct(
        private readonly EmailNotifier $emailNotifier,
        private readonly LoggerInterface $logger
    ) {}

    /**
     * Executes when 'mironsoft_blog_post_publish_after' is dispatched.
     */
    public function execute(Observer $observer): void
    {
        /** @var PostInterface $post */
        $post = $observer->getData('post');

        if (!$post || !$post->getAuthorId()) {
            return; // Guard: do nothing if data is missing
        }

        try {
            $this->emailNotifier->sendPublishConfirmation(
                $post->getAuthorId(),
                $post->getTitle()
            );
        } catch (\Exception $e) {
            // Observers should NEVER throw exceptions that interrupt the dispatch chain
            // Log and continue — other observers must still run
            $this->logger->error('Failed to send publish notification', [
                'post_id' => $post->getId(),
                'error'   => $e->getMessage(),
            ]);
        }
    }
}

Wichtige Regel: Observer sollten niemals unbehandelte Exceptions werfen. Eine Exception in einem Observer unterbricht die gesamte Event-Dispatch-Chain und kann nachfolgende Observer überspringen. Immer try/catch mit Logging verwenden.

3. events.xml: Observer registrieren

Observer werden in der events.xml eines Moduls registriert. Die Datei kann in etc/ (global), etc/frontend/ oder etc/adminhtml/ liegen – je nachdem, in welchem Scope der Observer aktiv sein soll.


<!-- app/code/Mironsoft/Notifications/etc/events.xml (global scope) -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">

    <!-- Observer for custom event -->
    <event name="mironsoft_blog_post_publish_after">
        <observer name="mironsoft_notifications_send_publish_email"
                  instance="Mironsoft\Notifications\Observer\SendPublishNotificationObserver"/>
    </event>

    <!-- Observer for a Magento Core event -->
    <event name="catalog_product_save_after">
        <observer name="mironsoft_notifications_product_saved"
                  instance="Mironsoft\Notifications\Observer\ProductSavedObserver"/>
    </event>

    <!-- Observer can be disabled -->
    <event name="sales_order_place_after">
        <observer name="mironsoft_notifications_order_placed"
                  instance="Mironsoft\Notifications\Observer\OrderPlacedObserver"
                  disabled="false"/>
    </event>

</config>

<!-- Frontend-only observer: etc/frontend/events.xml -->
<?xml version="1.0"?>
<config ...>
    <!-- Only fires in frontend scope, not in adminhtml or CLI -->
    <event name="cms_page_render">
        <observer name="mironsoft_track_cms_page_view"
                  instance="Mironsoft\Analytics\Observer\Frontend\TrackPageViewObserver"/>
    </event>
</config>

4. Event-Daten übergeben und lesen

Event-Daten werden als assoziatives Array im dispatch()-Aufruf übergeben. Im Observer werden sie über $observer->getData('key') oder die magischen Getter abgerufen. Objekte werden per Referenz übergeben – Änderungen am Objekt sind global sichtbar.


<?php
// In der Dispatch-Stelle:
$this->eventManager->dispatch('mironsoft_order_status_changed', [
    'order'      => $order,           // Objekt — Änderungen werden global sichtbar!
    'old_status' => $oldStatus,       // String
    'new_status' => $newStatus,       // String
    'changed_by' => $adminUserId,     // int
]);

// Im Observer:
public function execute(Observer $observer): void
{
    // getData() mit Schlüssel
    $order     = $observer->getData('order');
    $oldStatus = $observer->getData('old_status');
    $newStatus = $observer->getData('new_status');

    // Magische Getter (camelCase)
    $changedBy = $observer->getChangedBy();

    // Event-Objekt selbst
    $event = $observer->getEvent();
    $eventName = $event->getName(); // 'mironsoft_order_status_changed'

    // Objekte können modifiziert werden — Änderungen sind global
    if ($order && $newStatus === 'complete') {
        $order->setData('completion_notified', true);
        // Diese Änderung ist OHNE save() direkt am Objekt sichtbar
    }
}

5. Die wichtigsten Magento Core Events

Magento 2 dispatcht hunderte Core Events. Hier sind die am häufigsten genutzten in der täglichen Entwicklung:


# PRODUKTE
catalog_product_save_before          # vor dem Speichern eines Produkts
catalog_product_save_after           # nach dem Speichern
catalog_product_load_after           # nach dem Laden
catalog_product_delete_before        # vor dem Löschen
catalog_product_import_finish_before # nach CSV-Import

# KATEGORIEN
catalog_category_save_after
catalog_category_delete_after

# BESTELLUNGEN
sales_order_place_before             # vor dem Aufgeben einer Bestellung
sales_order_place_after              # nach dem Aufgeben
sales_order_payment_pay              # bei Zahlungseingang
sales_order_invoice_save_after       # nach Rechnungserstellung
sales_order_shipment_save_after      # nach Versanderstellung
sales_order_creditmemo_save_after    # nach Gutschrifterstellung
sales_order_status_history_save_after

# WARENKORB
checkout_cart_add_product_complete   # nach Hinzufügen zum Warenkorb
checkout_cart_remove_item_after      # nach Entfernen
checkout_cart_update_items_after     # nach Mengenänderung
checkout_submit_all_after            # nach Checkout-Abschluss

# KUNDEN
customer_register_success            # nach Registrierung
customer_login                       # nach Login
customer_logout                      # nach Logout
customer_save_after_data_object      # nach Kundenspeicherung

# ADMIN
adminhtml_block_html_before          # vor Admin-Block-Render
controller_action_predispatch        # vor jedem Controller-Aufruf
controller_action_postdispatch       # nach jedem Controller-Aufruf

# LAYOUT
layout_load_before                   # vor Layout-Laden
layout_generate_blocks_before        # vor Block-Generierung

6. Asynchrone Events mit Message Queue

Für zeitaufwendige Observer-Aktionen (E-Mail versenden, externe API aufrufen, Reports generieren) empfiehlt sich die asynchrone Verarbeitung über Magentos Message Queue. Statt die Aktion synchron im Observer auszuführen, wird sie in eine Queue geschrieben und von einem Consumer-Prozess asynchron abgearbeitet.


<?php
declare(strict_types=1);

namespace Mironsoft\Notifications\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\MessageQueue\PublisherInterface;

/**
 * Observer: Queues the email notification instead of sending it synchronously.
 * The actual email is sent by the MQ consumer process.
 */
class QueuePublishNotificationObserver implements ObserverInterface
{
    private const TOPIC_NAME = 'mironsoft.blog.publish.notification';

    public function __construct(
        private readonly PublisherInterface $publisher
    ) {}

    public function execute(Observer $observer): void
    {
        $post = $observer->getData('post');
        if (!$post) {
            return;
        }

        // Publish to queue — returns immediately, no blocking
        $this->publisher->publish(
            self::TOPIC_NAME,
            ['post_id' => $post->getId(), 'author_id' => $post->getAuthorId()]
        );
    }
}

7. Plugin vs. Observer: die Entscheidungshilfe

Die häufigste Frage in der Magento-Entwicklung: Soll ich ein Plugin oder einen Observer verwenden? Die Antwort hängt von der Anforderung ab:

  • Plugin verwenden wenn: Der Rückgabewert einer Methode modifiziert werden soll. Die Argumente einer Methode geändert werden sollen. Das Verhalten muss spezifisch an eine Methode gebunden sein. Kein Event vorhanden ist.
  • Observer verwenden wenn: Auf ein bestehendes Magento Core Event reagiert werden soll. Module vollständig entkoppelt bleiben sollen. Mehrere unabhängige Aktionen auf dasselbe Ereignis reagieren sollen. Die Reihenfolge und der Rückgabewert keine Rolle spielen.

<?php
// Entscheidungsbaum:

// 1. Gibt es ein Core Event für den Anwendungsfall?
//    → Ja: Observer verwenden
//    → Nein: weiter

// 2. Soll der Rückgabewert einer Methode geändert werden?
//    → Ja: After Plugin
//    → Nein: weiter

// 3. Sollen die Argumente einer Methode geändert werden?
//    → Ja: Before Plugin
//    → Nein: weiter

// 4. Soll die Methode unter Bedingungen nicht ausgeführt werden?
//    → Ja: Around Plugin (mit Bedacht!)
//    → Nein: Eigenes Event dispatchen + Observer

// FAUSTREGEL:
// Plugin: "Ich will Methode X in Klasse Y verändern"
// Observer: "Ich will reagieren, wenn Ereignis Z passiert"

8. Praxisbeispiele

Beispiel 1: Lagerbestand nach Bestellung prüfen


<?php
namespace Mironsoft\Inventory\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Sales\Api\Data\OrderInterface;

class CheckInventoryAfterOrderObserver implements ObserverInterface
{
    public function __construct(
        private readonly \Mironsoft\Inventory\Service\LowStockNotifier $notifier
    ) {}

    /**
     * Checks stock levels after an order is placed.
     * Listens on: checkout_submit_all_after
     */
    public function execute(Observer $observer): void
    {
        /** @var OrderInterface $order */
        $order = $observer->getData('order');
        if (!$order) {
            return;
        }

        foreach ($order->getAllItems() as $item) {
            $this->notifier->checkAndNotify($item->getSku(), $item->getQtyOrdered());
        }
    }
}

Beispiel 2: SEO-Attribute nach Produktspeicherung aktualisieren


<?php
namespace Mironsoft\SeoTools\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Catalog\Api\Data\ProductInterface;

class UpdateSeoMetaObserver implements ObserverInterface
{
    public function __construct(
        private readonly \Mironsoft\SeoTools\Service\MetaGenerator $metaGenerator
    ) {}

    /**
     * Auto-generates SEO meta description if empty.
     * Listens on: catalog_product_save_before
     */
    public function execute(Observer $observer): void
    {
        /** @var ProductInterface $product */
        $product = $observer->getData('product');

        if ($product && !$product->getMetaDescription()) {
            // Strip HTML, limit to 160 chars
            $description = strip_tags($product->getDescription() ?? '');
            $product->setMetaDescription(mb_substr($description, 0, 160));
        }
    }
}

Mironsoft

Event-Driven Magento 2 Entwicklung

Magento 2 Modul mit sauberem Event-System?

Wir entwickeln Magento 2 Module mit sauber entkoppelten Komponenten: eigene Events, Observer, asynchrone Queue-Verarbeitung und vollständige PHPUnit-Test-Abdeckung.

Event-Audit
Analyse bestehender Observer auf Fehler, Exception-Handling und Performance-Probleme.
Queue-Integration
Schwere Observer-Aktionen in asynchrone Message-Queue-Consumer auslagern.
Observer-Tests
PHPUnit-Tests für Observer mit Event-Mocking und vollständiger Abdeckung der Fehlerszenarien.

9. Zusammenfassung

Das Observer Pattern entkoppelt Module vollständig: Der Event-Dispatcher weiß nichts über seine Observer, und Observer wissen nichts voneinander. Events sind Ankerpunkte für Erweiterungen – ohne dass das auslösende Modul geändert werden muss. Für Echtzeit-Aktionen synchron dispatchen, für aufwendige Operationen Message Queue nutzen.

Observer / Event Pattern in Magento 2 – Regeln auf einen Blick

Event dispatchen

EventManager::dispatch(name, data). Vor und nach wichtigen Aktionen dispatchen. Eigene Namen mit Modul-Präfix: vendor_module_action_before/after.

Observer implementieren

ObserverInterface implementieren. execute(Observer $observer). Niemals Exceptions unbehandelt werfen. Immer try/catch mit Logging.

events.xml

Global in etc/events.xml, scope-spezifisch in etc/frontend/ oder etc/adminhtml/. Event-Name → Observer-Klasse → disabled="false".

Plugin vs. Observer

Plugin: Rückgabewert/Argumente ändern. Observer: auf Ereignis reagieren ohne Rückgabewert. Observer für vollständige Entkopplung zwischen Modulen. Plugin wenn kein passendes Event existiert.

10. FAQ: Observer / Event Pattern in Magento 2

1 Wie finde ich alle Magento Core Events?
Grep nach dispatch( im Core: grep -r "dispatch('" vendor/magento/module-catalog/ --include="*.php". Oder: Magento DevDocs Events Reference. Oder: Extension Magento Events List im Admin. Für alle Module: grep -rn 'eventManager->dispatch' vendor/magento/.
2 Kann ein Observer die auslösende Methode abbrechen?
Nein. Observer können nicht die auslösende Methode stoppen. Sie können Flags auf Datenobjekten setzen (die dann die Originalmethode auswertet), aber die Method-Execution selbst läuft weiter. Für echten Methodenabbruch: Around Plugin mit $proceed nicht aufrufen.
3 In welcher Reihenfolge werden Observer ausgeführt?
Observer haben kein sortOrder-Attribut. Die Reihenfolge entspricht der Modul-Ladereihenfolge aus module.xml (sequence-Tag). Ist die Reihenfolge kritisch, muss die Logik in einem Observer gebündelt oder auf Plugins mit sortOrder gewechselt werden.
4 Was passiert wenn ein Observer eine Exception wirft?
Eine unbehandelte Exception unterbricht die gesamte Dispatch-Chain — alle nachfolgenden Observer für dasselbe Event werden übersprungen, und die Exception propagiert zur aufrufenden Methode. Deshalb: immer try/catch mit Logging im Observer. Niemals Exceptions nach außen werfen.
5 Kann ich denselben Observer für mehrere Events registrieren?
Ja – dieselbe instance-Klasse kann in events.xml unter mehreren <event>-Knoten mit verschiedenen Namen eingetragen werden. Im execute() dann per $observer->getEvent()->getName() unterscheiden, welches Event gefeuert hat.
6 Wie teste ich einen Observer mit PHPUnit?
Observer sind direkt testbar: execute() aufrufen mit einem gemockten Observer-Objekt, das getData() auf die Test-Daten konfiguriert zurückgibt. Dann prüfen ob die injizierten Services mit den erwarteten Argumenten aufgerufen wurden. Kein Event-Dispatching nötig im Unit-Test.
7 Was ist der Unterschied zwischen globalem und scope-spezifischem events.xml?
etc/events.xml ist global aktiv (frontend, adminhtml, CLI, cron). etc/frontend/events.xml nur im Frontend, etc/adminhtml/events.xml nur im Admin-Panel. Scope-spezifische Observer sind bevorzugt – sie werden nicht unnötig in anderen Kontexten geladen und verbessern die Performance.
8 Wie übergebe ich Daten die Observer verändern können?
Objekte werden per Referenz übergeben – Änderungen sind direkt am Original sichtbar. Für primitive Werte: ein DataObject-Wrapper nutzen (new DataObject(['value' => $x])). Observer ändern den Wert im DataObject, der Dispatcher liest ihn nach dispatch() per $wrapper->getValue() aus.
9 Wann soll ich asynchrone Queue statt synchronen Observer verwenden?
Queue verwenden wenn: Die Aktion länger als ~100ms dauert (E-Mail, externe API, Report). Fehler nicht den Hauptfluss blockieren sollen. Retry bei Fehler gewünscht ist. Synchrone Observer für schnelle, kritische Aktionen: Logging, Cache-Invalidierung, Flags setzen. Faustregel: Wenn der Benutzer nicht auf das Ergebnis wartet → Queue.
10 Wie debugge ich Observer die nicht ausgeführt werden?
Checkliste: 1. bin/magento cache:flush – events.xml wird gecacht. 2. Richtiger Scope? (global/frontend/adminhtml). 3. Modul aktiviert? bin/magento module:status. 4. Observer-Name eindeutig? Konflikte bei gleichen Namen. 5. bin/magento setup:di:compile ausführen. 6. Xdebug-Breakpoint in execute() setzen.