How to attach event listener via configuration instead of module bootstrap?

杀马特。学长 韩版系。学妹 提交于 2019-12-08 05:59:34

问题


In ZF3 you would normally attach your event listener for the MvcEvent's in your module's Module.php like so:

<?php

namespace MyModule;

class Module
{
    public function onBootstrap(MvcEvent $event)
    {
        $eventManager = $event->getApplication()->getEventManager();

        $eventManager->attach(MvcEvent::EVENT_DISPATCH, function(MvcEvent $event) {
            // Do someting...
        });
    }
}

Now there are two typical situations where your Module.php can grow big:

  1. Your module has to handle multiple (or even all) MvcEvent's and maybe even treat them in different ways.
  2. Your module has to perform multiple actions on a single MvcEvent.

What I'd like to be able to do is to specify a class name in my module.config.php along with one or multiple MvcEvent names to keep my Module.php nice and clean.

Is there a way to do this in Zend Framework 3?


回答1:


@Nukeface has a great example but it does not directly answer my specific question.

To answer my own question:

This is possible with the use of listeners. A listener can be configured in the configuration files but it cannot be mapped to an event directly from the configuration alone.

It is possible to check for a specific setting in the configuration and determine what classes to map to what events. Even MvcEvents can be mapped this way.

Here's how to set it up:

1. The listener

We want to listen to multiple MvcEvents with one simple class. Note the class it extends.

namespace Demo\Listener;

class MyListener extends EventClassMapListener
{
    public function handleEvent(MvcEvent $event)
    {
        // Do something
        \Zend\Debug\Debug::dump($event->getName());
    }
}

2. The abstract listener class

The above class needs a bit more body but that can be provided by the abstract listener class:

namespace Demo\Listener;

abstract class EventClassMapListener implements ListenerAggregateInterface
{
    private $configuration;

    public function __construct(array $configuration)
    {
        $this->configuration = $configuration;
    }

    public function attach(EventManagerInterface $events, $priority = 1)
    {
        $sharedManager = $events->getSharedManager();
        foreach ($this->configuration as $identifier => $settings) {
            foreach ($settings as $event => $configPriority) {
                $sharedManager->attach($identifier, $event, [$this, 'handleEvent'], $configPriority ?: $priority);
            }
        }
    }

    public function detach(EventManagerInterface $events)
    {
        // Do the opposite of attach
    }

    abstract public function handleEvent(MvcEvent $event);
}

3. The factory

Now we need a factory that we can reuse for all our classes that need to listen to multiple events:

namespace Demo\Factory\Listener;

class EventClassmapListenerFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $globalConfiguration = $container->get('config');
        $configuration       = [];

        if (array_key_exists('event_classmap', $globalConfiguration)
            && array_key_exists($requestedName, $globalConfiguration['event_classmap'])
        ) {
            $configuration = $globalConfiguration['event_classmap'][$requestedName];
        }

        return new $requestedName($configuration);
    }
}

4. Configuration

In your module.config.php:

'service_manager' => [
    'factories' => [
        Listener\MyListener::class => Factory\Listener\EventClassmapListenerFactory::class,
    ],
],

'listeners' => [
    Listener\MyListener::class,
],

'event_classmap' => [
    // Name of the class that needs to listen to events
    Listener\MyListener::class => [
        // Identifier
        \Zend\Mvc\Application::class => [
            // List of event names and priorities
            MvcEvent::EVENT_BOOTSTRAP => 1,
        ],
        // Another identifier
        MyEventEmitterClass::class => [
            MyEventEmitterClass::EVENT_ONE,
            MyEventEmitterClass::EVENT_TWO,
            MyEventEmitterClass::EVENT_THREE,
        ],
    ],
],

Conclusion:

Although it might not really refined, I really like this idea. It is now fairly easy to add another listener and make it listen to a list of events from one or more emitters.

My opinion after some research

A listener itself should state what it wants to listen to, to keep things strict. Putting that information in a configuration file might result in a more complicated situation when it is not needed.




回答2:


You need a few things for Listener classes:

  1. Events
  2. Listeners
  3. Handlers
  4. Factories
  5. Config

Now, 2 & 3 are usually in the same class as you would usually have a Listener class for a specific purpose. Such as "Listen for Rocket launch and steer Rocket to Mars".

As such, you would need to "create" these "events" to listen for somewhere. Such as a DemoEvents class!

namespace Demo\Event;

use Zend\EventManager\Event;

class DemoEvent extends Event
{
    const THE_STRING_TO_LISTEN_FOR = 'rocket.ready.for.launch';
    const ANOTHER_STRING_TO_LISTEN_FOR = 'rocket.steer.to.mars';
}

Now that we have "events", we need to "listen" for them. For that we need a Listener. Because I'm limiting this to 1 example, the Handler (function(-ality) to be executed when the "event" we're "listening" for is "heard") will be in the same class.

namespace Demo\Listener;

use Demo\Event\DemoEvent;
use Zend\EventManager\Event;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;

class DemoListener implements ListenerAggregateInterface
{
    /**
     * @var array
     */
    protected $listeners = [];

    /**
     * @param EventManagerInterface $events
     */
    public function detach(EventManagerInterface $events)
    {
        foreach ($this->listeners as $index => $listener) {
            if ($events->detach($listener)) {
                unset($this->listeners[$index]);
            }
        }
    }

    /**
     * @param EventManagerInterface $events
     */
    public function attach(EventManagerInterface $events, $priority = 1)
    {
        $sharedManager = $events->getSharedManager();

        $sharedManager->attach(Demo::class, DemoEvent::THE_STRING_TO_LISTEN_FOR, [$this, 'doSomethingOnTrigger'], -10000);
    }

    /**
     * Apart from triggering specific Listener function and de-registering itself, it does nothing else. Add your own functionality
     *
     * @param Event $event
     */
    public function doSomethingOnTrigger(Event $event)
    {
       // Gets passed along parameters from the ->trigger() function elsewhere
        $params = $event->getParams(); 

        $specificClass = $params[SpecificClass::class];

        // Do something useful here
        $specificClass->launchRocketIntoOrbit();

        // Detach self to prevent running again
        $specificClass->getEventManager()->getSharedManager()->clearListeners(get_class($specificClass), $event->getName());

        // NOTE: USE THIS TRIGGER METHODOLOGY ELSEWHERE USING THE STRING FROM THE ATTACH() FUNCTION TO TRIGGER THIS FUNCTION
        // Trigger events specific for the Entity/class (this "daisy-chains" events, allowing for follow-up functionality)
        $specificClass->getEventManager()->trigger(
            DemoEvent::ANOTHER_STRING_TO_LISTEN_FOR,
            $specificClass ,
            [get_class($specificClass) => $specificClass ] // Params getting passed along
        );
    }
}

Excellent. We now have a events, a listener and a handler. We just need a factory to create this class when needed.

namespace Demo\Factory;

use Demo\Listener;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;

class DemoListenerFactory implements FactoryInterface
{
    /**
     * @param ContainerInterface $container
     * @param string $requestedName
     * @param array|null $options
     * @return object|DemoListener
     * @throws \Psr\Container\ContainerExceptionInterface
     * @throws \Psr\Container\NotFoundExceptionInterface
     */
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        // If you're implementation of the Listener has any requirements, load them here and add a constructor in the DemoListener class

        return new DemoListener();
    }
}

Lastly, we need some config. Obviously we need to register the Listener + Factory combination. Let's do that first.

namespace Demo;

use Demo\Listener\DemoListener;
use Demo\Listener\DemoListenerFactory;

'service_manager' => [
    'factories' => [
        DemoListener::class => DemoListenerFactory::class,
    ],
],

Now for a little known bit of config to make sure that the Listener gets registered as a Listener:

'listeners' => [
    DemoListener::class
],

Yep, that's it.

Make sure to add both of these bits of config at the first level of config, they're siblings.



来源:https://stackoverflow.com/questions/49181751/how-to-attach-event-listener-via-configuration-instead-of-module-bootstrap

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!