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

ZF2 - How to Listen for Events and Trigger a Service because of it?

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

The use case I'm trying to get to work is the following:

Just before an Address Entity (with Doctrine, but that's not important) gets saved (flushed), a Service must be triggered to check if the Coordinates for the Address are set, and if not, create and fill a new Coordinates Entity and link it to the Address. The Coordinates are to be gotten from Google Maps Geocoding API.

将在下面展示我对事物的理解以及理解方式,希望我能说清楚。据我所知,将分步显示中间添加的代码,并告诉您哪些有效,哪些无效。

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

监听器必须在 ZF2 的 ServiceManager 中注册。 (Shared)EventManager 的侦听器 "attaches" 某些条件。 EventManager 对一个对象来说是唯一的,但是 SharedEventManager 在应用程序中是 'global'。

在地址模块的 Module.php class 中,我添加了以下函数:

/**
 * @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...
    }
}

基于 function attach(...){} 中的 ->attach() 函数,我希望 Listener 可以作为触发事件的垫脚石。但是,这似乎不起作用,因为没有任何东西被触发。不是 addressCreated() 函数,也不是 GoogleCoordinatesService 中的 getCoordinates 函数。

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

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

文件 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,
    ],
],

Factory 是非常标准的 ZF2 东西,但要描绘完整的画面,这里是 GoogleCoordinatesServiceFactory.php class。 (已删除 comments/typehints/etc)

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
        );
    }
}

下面是GoogleCoordinatesServiceclass。然而,那里没有任何东西被触发执行。因为它甚至没有被调用,所以我确定问题出在上面的代码中,但无法找出原因。根据我阅读和尝试过的内容,我期望应该通过工厂调用 class 本身,并且应该触发 getCoordinates 函数。

所以,class。我删除了一堆标准 getters/setters、注释、文档块和类型提示以使其更短。

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 的 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"
}

是的。目前对如何让它工作有点不知所措。

任何帮助将不胜感激,并希望得到关于 "why" 解决方案有效的解释。这真的会帮助我制作更多这些服务:)

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

几天来我一直在思考这一切,但 this article 对我的帮助最大,或者它只是让我对问题的最初研究以及随后的试错 + 调试变得很顺利.

下面的代码是现在的状态,处于工作状态。随着代码的出现,我将尝试解释我是如何理解它的工作原理的。如果我在某些时候说错了,我希望有人纠正我。


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

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

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]);
            }
        }
    }
}

在上面提到必须使用 SharedEventManager 并创建 AbstractListener 之后,AddressListener class 就这样结束了。

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 的主要区别在于后者监听特定的 class 以发出触发器。在这种情况下,它将侦听 AddressController::class 以发出触发器 entity.postPersist。在 "hearing" 它被触发后,它将调用回调函数。在本例中,它使用此数组参数注册:[new AddressEvent(), 'addCoordinatesToAddress'],这意味着它将使用 class AddressEvent 和函数 addCoordinatesToAddress

要测试这是否有效,如果您正在处理这个答案,您可以在您自己的控制器中创建触发器。我一直在 AbstractActionControlleraddAction 工作,它被 AddressControlleraddAction 调用。在上面监听器的触发器下面:

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
}

上面代码中的->trigger()函数展示了以下参数的用法:

  • 'entity.postPersist' - 这是事件名称
  • $this - 这是调用事件的 "component" 或对象。在这种情况下,它将是 Address\Controller\AddressController
  • [get_class($entity) => $entity] - 这些是与此 Event 对象一起发送的参数。它将使您有可用的 $event->getParams()[Address::class],它将具有 $entity 值。

前两个参数会触发SharedEventManager中的Listener。要测试是否一切正常,可以修改监听器的附加函数。

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

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 注册监听器。这发生在模块 Module.php 文件中的 onBootstrap 函数中(在本例中为地址)。像下面这样注册。

public function onBootstrap(MvcEvent $e)
{
    $eventManager = $e->getApplication()->getEventManager();
    $eventManager->attach(new AddressListener());
}

如果您在 AbstractActionController 中调试 addAction 的代码,看到它通过了触发器,然后您进入了 test 函数,那么您的侦听器就可以工作了。

上面的代码还暗示了AddressListener class 可以用来附加多个监听器。因此,您还可以为 entity.prePersistentity.preFlushentity.postFlush 以及您能想到的任何其他内容注册内容。

接下来,将 Listener 恢复到开始时的状态(恢复 attach 函数并删除 test 函数)。

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

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;
    }
}

老实说,我不太清楚为什么要在 setEventManager 函数中设置 2 个标识符。但足以说明它用于为事件注册回调。 (如果有人愿意提供,可以使用 more/detailed 解释)

AddressListener 中,我们尝试调用 AddressEvent class 的 addCoordinatesToAddress 函数。所以我们将不得不创建它,我像下面那样做了。

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。我们传递这个 Event 对象和参数。这最后一步不是必需的,但如果您希望链接事件,它会很方便。


在问题中我提到了一个用例。上面的代码使 Service (GoogleGeocodingService) 能够满足它的要求,并结合工厂的配置,通过 Zend Magic 使用 ServiceManager 创建它。

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