ZF2 - 如何监听事件并触发服务?

时间:2017-08-31 09:00:21

标签: php event-handling zend-framework2

一直在尝试学习如何实现服务,因为它们被监听器触发。 Have been doing a serious lot of reading the last几天才能让它发挥作用,但一直觉得很困难。因此,我认为我对事物顺序的理解可能存在缺陷。

  

我尝试上班的用例如下

     

就在地址实体之前(有了Doctrine,但那不是   重要的是保存(刷新),必须触发服务检查   如果设置了地址的坐标,如果没有,则创建和   填写新的坐标实体并将其链接到地址。该   坐标将来自Google Maps Geocoding API。

将在下面展示我对什么以及如何理解事物,希望我能让自己明白。如果我知道的话,它会逐步显示中间添加的代码并告诉你哪些功能和功能都不起作用。

现在,我对过去几天所得到的所有信息的理解是:

监听器必须在ZF2的ServiceManager中注册。听众"附加" (共享)EventManager的某些条件。 EventManager对于一个对象是唯一的,但是SharedEventManager是全局的'在申请中。

在地址模块的Module.php课程中,我添加了以下功能:

/**
 * @param EventInterface $e
 */
public function onBootstrap(EventInterface $e)
{
    $eventManager = $e->getTarget()->getEventManager();
    $eventManager->attach(new AddressListener());
}

这会起作用,AddressListener会被触发。

AddressListener如下:

use Address\Entity\Address;
use Address\Service\GoogleCoordinatesService;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\Stdlib\CallbackHandler;

class AddressListener implements ListenerAggregateInterface
{
    /**
     * @var CallbackHandler
     */
    protected $listeners;

    /**
     * @param EventManagerInterface $events
     */
    public function attach(EventManagerInterface $events)
    {
        $sharedEvents = $events->getSharedManager();

        // Not sure how and what order params should be. The ListenerAggregateInterface docblocks didn't help me a lot with that either, as did the official ZF2 docs. So, been trying a few things...

        $this->listeners[] = $sharedEvents->attach(GoogleCoordinatesService::class, 'getCoordinates', [$this, 'addressCreated'], 100);

        $this->listeners[] = $sharedEvents->attach(Address::class, 'entity.preFlush', [GoogleCoordinatesService::class, 'getCoordinates'], 100);
    }

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

    public function addressCreated()
    {
        $foo = 'bar'; // This line is here to as debug break. Line is never used...
    }
}

根据->attach()中的function attach(...){}函数,我期待一个倾听者可以作为触发事物触发点的绊脚石。但是,这似乎不起作用,因为没有任何东西被触发。不是addressCreated()功能,而不是getCoordinates中的GoogleCoordinatesService功能。

上面的代码应该触发GoogleCoordinatesService函数getCoordinates。该服务有一些要求,例如EntityManager of Doctrine,它关注的地址实体和配置。

为此,我创建了以下配置。

文件google.config.php(加载,检查)

return [
    'google' => [
        'services' => [
            'maps' => [
                'services' => [
                    'geocoding' => [
                        'api_url' => 'https://maps.googleapis.com/maps/api/geocode/json?',
                        'api_key' => '',
                        'url_params' => [
                            'required' => [
                                'address',
                            ],
                            'optional' => [
                                'key'
                            ],
                        ],
                    ],
                ],
            ],
        ],
    ],
];

module.config.php我已经用工厂注册了服务

'service_manager' => [
    'factories' => [
        GoogleCoordinatesService::class => GoogleCoordinatesServiceFactory::class,
    ],
],

工厂是非常标准的ZF2,但是为了描绘一幅完整的图片,这里是GoogleCoordinatesServiceFactory.php类。 (删除了评论/类型提示/等)

class GoogleCoordinatesServiceFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator, $options = [])
    {
        $serviceManager = $serviceLocator->getServiceLocator();
        $entityManager = $serviceManager->get(EntityManager::class);
        $config = $serviceManager->get('Config');

        if (isset($options) && isset($options['address'])) {
            $address = $options['address'];
        } else {
            throw new InvalidArgumentException('Must provide an Address Entity.');
        }

        return new GoogleCoordinatesService(
            $entityManager,
            $config,
            $address
        );
    }
}

以下是GoogleCoordinatesService课程。然而,没有任何东西被触发在那里执行。因为它甚至没有被调用,我确定问题出在上面的代码中,但是找不到原因。从我读过和尝试的内容来看,我期望通过Factory调用类本身,并且应该触发getCoordinates函数。

所以,上课。我删除了一堆标准的getter / setter,comments,docblocks和typehints,以缩短它。

class GoogleCoordinatesService implements EventManagerAwareInterface
{
    protected $eventManager;
    protected $entityManager;
    protected $config;
    protected $address;

    /**
     * GoogleCoordinatesServices constructor.
     * @param EntityManager $entityManager
     * @param Config|array $config
     * @param Address $address
     * @throws InvalidParamNameException
     */
    public function __construct(EntityManager $entityManager, $config, Address $address)
    {    
        $this->config = $config;
        $this->address = $address;
        $this->entityManager = $entityManager;
    }

    public function getCoordinates()
    {
        $url = $this->getConfig()['api_url'] . 'address=' . $this->urlFormatAddress($this->getAddress());

        $response = json_decode(file_get_contents($url), true);

        if ($response['status'] == 'OK') {
            $coordinates = new Coordinates();
            $coordinates
                ->setLatitude($response['results'][0]['geometry']['location']['lat'])
                ->setLongitude($response['results'][0]['geometry']['location']['lng']);

            $this->getEntityManager()->persist($coordinates);

            $this->getAddress()->setCoordinates($coordinates);
            $this->getEntityManager()->persist($this->getAddress());

            $this->getEntityManager()->flush();

            $this->getEventManager()->trigger(
                'addressReceivedCoordinates',
                null,
                ['address' => $this->getAddress()]
            );
        } else {
            // TODO throw/set error/status
        }
    }

    public function urlFormatAddress(Address $address)
    {
        $string = // format the address into a string

        return urlencode($string);
    }

    public function getEventManager()
    {
        if ($this->eventManager === null) {
            $this->setEventManager(new EventManager());
        }

        return $this->eventManager;
    }

    public function setEventManager(EventManagerInterface $eventManager)
    {
        $eventManager->addIdentifiers([
            __CLASS__,
            get_called_class()
        ]);

        $this->eventManager = $eventManager;
        return $this;
    }

    // Getters/Setters for EntityManager, Config and Address
}

所以,这是在触发某个事件时处理它的设置。现在它应该被触发了。对于这个用例,我在自己的AbstractActionController中设置了一个触发器(扩展了ZF2' s AbstractActionController)。这样做:

if ($form->isValid()) {
    $entity = $form->getObject();
    $this->getEntityManager()->persist($entity);

    try {
        // Trigger preFlush event, pass along Entity. Other Listeners can subscribe to this name.
        $this->getEventManager()->trigger(
            'entity.preFlush',
            null,
            [get_class($entity) => $entity] // key = "Address\Entity\Address" for use case
        );

        $this->getEntityManager()->flush();
    } catch (\Exception $e) {
        // Error thrown
    }
    // Success stuff, like a trigger "entity.postFlush"
}

所以是的。目前在如何让它工作有点失落。

任何帮助都会非常感激,并且会喜欢解释"为什么"这是一个解决方案的工作原理。这真的可以帮助我提供更多这些服务:)

1 个答案:

答案 0 :(得分:2)

已经有一段时间了,但已经设法弄清楚它为什么不起作用。我将Listener附加到EventManager s,但应该将它们附加到SharedEventManager。这是因为我在AbstractActionController中有触发器(在本例中),因此它们在实例化时都会创建自己的EventManager(因为它们是唯一的)。

几天来一直困扰着这一切,但是this article帮助了我最多,或者也许只是在我的问题和随后的试验&错误+调试。

现在的代码下方,处于正常工作状态。我将尝试解释代码,因为我知道它是如何工作的。如果我在某些时候弄错了,我希望有人纠正我。

首先,我们需要一个Listener,一个将组件和事件注册到" listen"让他们触发。 (他们侦听某些(命名)对象以触发某些事件)

很快就意识到每个Listener都需要$listeners = [];detach(EventManagerInterface $events){...}函数。所以我创建了一个AbstractListener类。

namespace Mvc\Listener;

use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;

/**
 * Class AbstractListener
 * @package Mvc\Listener
 */
abstract class AbstractListener 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]);
            }
        }
    }
}

在上述关于必须使用SharedEventManagerAbstractListener创建的实现之后,AddressListener类已经最终结束了。

namespace Address\Listener;

use Address\Event\AddressEvent;
use Admin\Address\Controller\AddressController;
use Mvc\Listener\AbstractListener;
use Zend\EventManager\EventManagerInterface;

/**
 * Class AddressListener
 * @package Address\Listener
 */
class AddressListener extends AbstractListener
{
    /**
     * @param EventManagerInterface $events
     */
    public function attach(EventManagerInterface $events)
    {
        $sharedManager = $events->getSharedManager();
        $sharedManager->attach(AddressController::class, 'entity.postPersist', [new AddressEvent(), 'addCoordinatesToAddress']);
    }
}

将事件附加到EventManagerSharedEventManager的主要区别在于后者侦听特定类以发出触发器。在这种情况下,它将监听AddressController::class发出触发器entity.postPersist。在"听到"触发它会调用回调函数。在这种情况下,使用此数组参数注册:[new AddressEvent(), 'addCoordinatesToAddress'],这意味着它将使用类AddressEvent和函数addCoordinatesToAddress

要测试这是否有效,如果您正在使用此答案,则可以在自己的Controller中创建触发器。我一直在addAction的{​​{1}}工作,AbstractActionController的{​​{1}}会调用addAction。在上面的Listener触发器下方:

AddressController

上面代码中的if ($form->isValid()) { $entity = $form->getObject(); $this->getEntityManager()->persist($entity); $this->getEventManager()->trigger( 'entity.postPersist', $this, [get_class($entity) => $entity] ); try { $this->getEntityManager()->flush(); } catch (\Exception $e) { // Error stuff } // Remainder of function } 函数显示了以下参数的用法:

  • ' entity.postPersist' - 这是事件名称
  • $ this - 这是"组件"或对象要求事件。在这种情况下,它将是->trigger()
  • [get_class($ entity)=> $ entity] - 这些是与此Address\Controller\AddressController对象一起发送的参数。这将使您获得Event$event->getParams()[Address::class]的{​​{1}}。

前两个参数将触发$entity中的监听器。要测试一切是否有效,可以修改Listener的附加功能。

将其修改为此并在Listener中创建一个函数,以便您可以看到它正常工作:

SharedEventManager

最后,为了确保上述内容确实有效,必须在public function attach(EventManagerInterface $events) { $sharedManager = $events->getSharedManager(); $sharedManager->attach(AddressController::class, 'entity.postPersist', [$this, 'test']); } public function test(Event $event) { var_dump($event); exit; } 注册监听器。这发生在模块的EventManager文件中的onBootstrap函数中(本例中为Address)。注册如下。

Module.php

如果您调试public function onBootstrap(MvcEvent $e) { $eventManager = $e->getApplication()->getEventManager(); $eventManager->attach(new AddressListener()); } addAction的代码,请看它通过触发器然后您在AbstractActionController函数中,然后您的侦听器工作。

上述代码还暗示test类可用于附加多个侦听器。因此,您还可以注册AddressListenerentity.prePersistentity.preFlush以及您能想到的任何内容。

接下来,将监听器恢复为开头的状态(恢复entity.postFlush功能并删除attach功能。)

我还注意到,几乎每个test处理类都需要能够设置并获取Event。因此,为此,我创建了一个EventManager类,如下所示。

AbstractEvent

说实话,我不太清楚为什么我们在namespace Mvc\Event; use Zend\EventManager\EventManager; use Zend\EventManager\EventManagerAwareInterface; use Zend\EventManager\EventManagerInterface; abstract class AbstractEvent implements EventManagerAwareInterface { /** * @var EventManagerInterface */ protected $events; /** * @param EventManagerInterface $events */ public function setEventManager(EventManagerInterface $events) { $events->setIdentifiers([ __CLASS__, get_class($this) ]); $this->events = $events; } /** * @return EventManagerInterface */ public function getEventManager() { if (!$this->events) { $this->setEventManager(new EventManager()); } return $this->events; } } 函数中设置2个标识符。但足以说它用于注册事件的回调。 (如果有人感到如此倾向于提供它,可以使用更多/详细的解释)

setEventManager我们尝试调用AddressListener类的addCoordinatesToAddress函数。所以我们必须创建它,我在下面做了。

AddressEvent

在上面你可以看到,首先我们检查我们期望的参数是否与namespace Address\Event; use Address\Entity\Address; use Address\Service\GoogleGeocodingService; use Country\Entity\Coordinates; use Mvc\Event\AbstractEvent; use Zend\EventManager\Event; use Zend\EventManager\Exception\InvalidArgumentException; class AddressEvent extends AbstractEvent { public function addCoordinatesToAddress(Event $event) { $params = $event->getParams(); if (!isset($params[Address::class]) || !$params[Address::class] instanceof Address) { throw new InvalidArgumentException(__CLASS__ . ' was expecting param with key ' . Address::class . ' and value instance of same Entity.'); } /** @var Address $address */ $address = $params[Address::class]; if (!$address->getCoordinates() instanceof Coordinates) { /** @var GoogleGeocodingService $geocodingService */ $geocodingService = $event->getTarget()->getEvent()->getApplication()->getServiceManager()->get(GoogleGeocodingService::class); $geocodingService->addCoordinatesToAddress($address); } $params = compact('address'); $this->getEventManager()->trigger(__FUNCTION__, $this, $params); } } 参数一起传递。我们知道我们应该期待什么以及密钥应该具有什么名称,因此我们要明确检查。

接下来,我们检查收到的Event $event实体对象是否已经有与之关联的Address对象,如果它没有,我们会调用服务来实现它。

运行Coordinates语句后,我们会触发另一个if()。我们传递此trigger对象和参数。最后一步不是必需的,但如果您希望链接事件,则可以很方便。

在问题中我提到了一个用例。上面的代码使EventService)能够通过它的要求,并结合Factory的配置,通过Zend Magic和GoogleGeocodingService创建。

将新ServiceManager对象添加到现有Coordinates对象的代码未被修改,因此我不会将其作为答案的一部分,您可以在问题中找到它。