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

Freitag, 03. März 2017

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-Modulentwicklung

^