Wie Module mit dem Event-System von Symfony miteinander interagieren können

in  Zikula Apps , , , ,

Wie Module mit dem Event-System von Symfony miteinander interagieren können

In Zikula gibt es verschiedene Konzepte zur Zusammenarbeit zwischen unterschiedlichen Modulen. Beispielsweise gibt es die Capabilities, mit denen Module bestimmte Fähigkeiten deklarieren oder einfordern können. Und mit dem Hook-System können Module in andere Module eingebunden werden, ihre Daten validieren oder weiterverarbeiten.

Unter der Haube basieren Hooks auf Events. Dieser Beitrag zeigt, wie man das Event-System von Symfony direkt verwenden und damit eine weitere, wesentlich flexiblere Möglichkeit zur modulübergreifenden Interaktion nutzbar macht.

Die Basis von Events in Symfony ist die EventDispatcher-Komponente. Im Wesentlichen werden Informationen über auftretende Ereignisse versendet. Diese Informationen können dann von Event Listenern oder Event Subscribern aufgegriffen werden, was die Ausführung von zusätzlichem Code erlaubt.

Im Zikula Core werden in zahlreichen anderen Bereichen Events verwendet. Mit ModuleStudio erstellte Module stellen dafür direkt passende Event Subscriber bereit. Damit kann man beispielsweise darauf reagieren, dass ein Benutzer angelegt oder gelöscht wurde, eine Gruppe erstellt wurde, ein Benutzer sich registriert oder angemeldet hat, und so weiter.

Ferner bringen ModuleStudio-Module auch eigene Filter-Events mit, die beim Laden, Anlegen, Verändern oder Löschen von Daten ausgelöst werden. Diese Filter-Events erlauben dass passgenaue Reagieren auf Ereignisse für bestimmte Entitäten. Das ist insbesondere dann hilfreich, wenn es mehrere Module gibt, die lose miteinander gekoppelt werden.

Im folgenden Beispiel nehmen wir an, dass es einen Webshop gibt, der unter anderem aus den Modulen Customers, Products und Frontend besteht. Das Frontend möchte bestimmte Dinge tun, sobald sich etwas bei den Kunden oder Produkten verändert hat.

Wir erstellen dazu im ShopFrontend-Modul einen DependencyChangeListener. Damit darin Daten des Frontend-Moduls verändert oder entfernt werden können, bekommt dieser Event Subscriber die EntityFactory und den WorkflowHelper des Frontend-Moduls injiziert. Als erstes wird eine Service-Definition erstellt, in der die Argumente spezifiziert und die Klasse mit Hilfe eines Tags als Event Subscriber deklariert wird.

    acme_shopfrontend_module.dependency_change_listener:
        class: Acme\ShopFrontendModule\Listener\DependencyChangeListener
        arguments:
            - '@acme_shopfrontend_module.entity_factory'
            - '@acme_shopfrontend_module.workflow_helper'
        tags:
            - { name: kernel.event_subscriber }

Die Implementierung der Klasse ist nun relativ geradlinig. Ein Event Subscriber definiert in der statischen Methode getSubscribedEvents(), auf welche Events er mit welchen Methoden reagieren möchte. Über die injizierte EntityFactory lassen sich sowohl der Entity Manager von Doctrine (und damit Query Builder etc.) sowie die Repositories sämtlicher Entitäten im Modul erreichen. Der WorkflowHelper wird indes zum Anlegen neuer und zum Entfernen bestehender Daten verwendet. Der folgende Code skizziert die Struktur der Klasse.

<?php
namespace Acme\ShopFrontendModule\Listener;

use Acme\CustomersModule\CustomersEvents;
use Acme\CustomersModule\Event\FilterCustomerEvent;
use Acme\ShopFrontendModule\Entity\Factory\ShopFrontendFactory;
use Acme\ShopFrontendModule\Helper\WorkflowHelper;
use Acme\ProductsModule\ProductsEvents;
use Acme\ProductsModule\Event\FilterProductEvent;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Event handler implementation class for shop frontend dependency changes.
 */
class DependencyChangeListener implements EventSubscriberInterface
{
    /**
     * @var ShopFrontendFactory
     */
    private $entityFactory;

    /**
     * @var WorkflowHelper
     */
    private $workflowHelper;

    /**
     * DependencyChangeListener constructor.
     *
     * @param ShopFrontendFactory $entityFactory  Entity factory
     * @param WorkflowHelper      $workflowHelper Workflow helper
     */
    public function __construct(ShopFrontendFactory $entityFactory, WorkflowHelper $workflowHelper)
    {
        $this->entityFactory = $entityFactory;
        $this->workflowHelper = $workflowHelper;
    }

    /**
     * Makes our handlers known to the event system.
     */
    public static function getSubscribedEvents()
    {
        return [
            CustomersEvents::CUSTOMER_PRE_REMOVE  => ['customerChanged', 5],
            CustomersEvents::CUSTOMER_POST_UPDATE => ['customerChanged', 5],
            ProductsEvents::PRODUCT_POST_PERSIST => ['productChanged', 5],
            ProductsEvents::PRODUCT_PRE_REMOVE   => ['productChanged', 5],
            ProductsEvents::PRODUCT_POST_UPDATE  => ['productChanged', 5]
        ];
    }

    /**
     * Listener for customer related events.
     *
     * @param FilterCustomerEvent $event The event instance
     */
    public function customerChanged(FilterCustomerEvent $event)
    {
        $qb = $this->getQueryBuilderForFrontendObjectUpdate();
        $qb->where('tbl.customerId = :customerId' )
           ->setParameter('customerId', $event->getCustomer()->getId());

        if (CustomersEvents::CUSTOMER_PRE_REMOVE == $event->getName()) {
            $qb->set('tbl.customerId', 0);
        }

        $qb->getQuery()->execute();
    }

    /**
     * Listener for product related events.
     *
     * @param FilterProductEvent $event The event instance
     */
    public function productChanged(FilterProductEvent $event)
    {
        if (ProductsEvents::PRODUCT_POST_PERSIST == $event->getName()) {
            // react on newly persisted product
        } elseif (ProductsEvents::PRODUCT_PRE_REMOVE == $event->getName()) {
            // react on product to be deleted
        } elseif (ProductsEvents::PRODUCT_POST_UPDATE == $event->getName()) {
            // react on updated product
        }
    }

    /**
     * Returns a query builder.
     *
     * @return QueryBuilder
     */
    private function getQueryBuilder()
    {
        return $this->entityFactory->getObjectManager()->createQueryBuilder();
    }

    /**
     * Returns a query builder.
     *
     * @return QueryBuilder
     */
    private function getQueryBuilderForFrontendObjectUpdate()
    {
        $qb = $this->getQueryBuilder();
        $qb->update('Acme\ShopFrontendModule\Entity\SomeEntity', 'tbl')
            ->set('tbl.myFlag', true);

        return $qb;
    }
}

Dieses Beispiel zeigt gut, welche Flexibilität in der Verwendung von Event Subscribern liegt. Der Phantasie sind hier praktisch keine Grenzen gesetzt.

Weitere Beiträge in Kategorie Zikula Apps

Zikula Benutzer und Gruppen in DokuWiki verwenden
- In Zikula lassen sich mit Hilfe unterschiedlicher Authentifizierungsmethoden Nutzer auf verschiedenen Quellen einbinden und mischen. So kann man sich beispielsweise mit dem OAuth-Modul via Facebook, …
Ein Blick auf die Entwicklungen des News-Moduls
- Das News-Modul von Zikula blickt auf eine lange Historie zurück. Schon als ich vor etwa 20 Jahren das erste mal mit PostNuke in Berührung kam, war dort ein News-Modul an Bord. War die Funktionalität …
Zikula Aktualisierung 2.0.13 mit Sicherheitspatches von Symfony
- Der Zikula Core ist soeben in der Version 2.0.13 erschienen, da eine Reihe von sicherheitsbezogenen Änderungen in Symfony eingeflossen sind. Hier der Link zu den einzelnen Änderungen: Changelog für …
Unterschiedliche Startseiten je Domain oder Einstellung einbinden
- Eine häufige Anforderung besteht darin, die Startseite eines Projektes individuell anzupassen. Zikula bietet zwar die Möglichkeit, eine Controller-Aktion sowie die zu übergebenden Argumente in der …
Spannende Neuerungen im Zikula Core
- In Zikula 3 werden endlich weitere hilfreiche Funktionen von Symfony verwendet. Zikula 2.x hat bereits auf Symfony 3.4.x aufgesetzt, aber aus Rücksicht auf die Abwärtskompatibilität noch nicht alle …