一直在尝试学习如何实现服务,因为它们是由侦听器触发的。在过去的几天里,我们一直在认真阅读以使其正常工作,但一直发现很难。因此,我认为我对事物顺序的理解可能是有缺陷的。
我正在尝试工作的用例如下:
就在地址实体之前(有教义,但这不是 重要)保存(刷新),必须触发服务进行检查 如果设置了地址的坐标,如果没有,则创建 和 填充新的坐标实体并将其链接到地址。这 坐标将从谷歌地图地理编码API获取。
将在下面显示我理解事物的内容和方式,希望我能说清楚。将分步执行,以显示中间添加的代码,并告诉您据我所知,哪些有效,哪些无效。
现在,我对过去几天获得的所有信息的理解是这样的:
侦听器必须向 ZF2 的服务管理器注册。侦听器将某些条件"附加"到(共享)事件管理器。事件管理器对于对象是唯一的,但共享事件管理器在应用程序中是"全局的"。
在地址模块的Module.php
类中,我添加了以下函数:
/**
* @param EventInterface $e
*/
public function onBootstrap(EventInterface $e)
{
$eventManager = $e->getTarget()->getEventManager();
$eventManager->attach(new AddressListener());
}
这得到工作,地址侦听器被触发。
地址侦听器如下所示:
use AddressEntityAddress;
use AddressServiceGoogleCoordinatesService;
use ZendEventManagerEventManagerInterface;
use ZendEventManagerListenerAggregateInterface;
use ZendStdlibCallbackHandler;
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()
函数作为触发事物的垫脚石点。但是,这似乎不起作用,因为没有触发任何内容。不是addressCreated()
函数,也不是GoogleCoordinatesService
中的getCoordinates
函数。
上面的代码应该触发GoogleCoordinatesService
函数getCoordinates
。不过,该服务有一些要求,例如存在原则的实体管理器、它所涉及的地址实体和配置。
为此,我创建了以下配置。
文件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
类。但是,在那里执行的任何内容都不会被触发。由于它甚至没有被调用,我确信问题出在上面的代码中,但无法找出原因。从我阅读和尝试的内容来看,我希望类本身应该通过工厂被调用,并且应该触发getCoordinates
函数。
所以,班级。我删除了一堆标准的getter/setter,注释,docblocks和类型提示,以使其更短。
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 = "AddressEntityAddress" for use case
);
$this->getEntityManager()->flush();
} catch (Exception $e) {
// Error thrown
}
// Success stuff, like a trigger "entity.postFlush"
}
所以是的。目前对如何让它工作有点不知所措。
任何帮助将不胜感激,并希望解释解决方案的"原因"是有效的。这真的会帮助我提供更多这些服务:)
已经有一段时间了,但设法弄清楚了为什么它不起作用。我Listener
s附加到EventManager
s,但应该将它们附加到SharedEventManager
.这是因为我在AbstractActionController
中有触发器(在本例中),因此它们在实例化时都会创建自己的EventManager
(因为它们是唯一的)。
这几天很艰难,但这篇文章对我帮助最大,或者它可能只是让我对问题的原始研究和随后的试错+调试有了点击。
在代码下方,就像现在一样,处于工作状态。我将尝试在代码出现时解释我如何理解它的工作原理。如果我在某个时候弄错了,我希望有人纠正我。
首先,我们需要一个Listener
,一个注册组件和事件的类来"侦听"它们触发。(它们侦听某些(命名的)对象以触发某些事件)
很快意识到几乎每个监听器都需要$listeners = [];
和detach(EventManagerInterface $events){...}
功能。所以我创建了一个AbstractListener
类。
namespace MvcListener;
use ZendEventManagerEventManagerInterface;
use ZendEventManagerListenerAggregateInterface;
/**
* Class AbstractListener
* @package MvcListener
*/
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
类就这样结束了。
namespace AddressListener;
use AddressEventAddressEvent;
use AdminAddressControllerAddressController;
use MvcListenerAbstractListener;
use ZendEventManagerEventManagerInterface;
/**
* Class AddressListener
* @package AddressListener
*/
class AddressListener extends AbstractListener
{
/**
* @param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events)
{
$sharedManager = $events->getSharedManager();
$sharedManager->attach(AddressController::class, 'entity.postPersist', [new AddressEvent(), 'addCoordinatesToAddress']);
}
}
将事件附加到EventManager
与SharedEventManager
的主要区别在于,后者侦听特定类以发出触发器。在这种情况下,它将侦听AddressController::class
发出触发器entity.postPersist
。在"听到"它被触发时,它将调用一个回调函数。在这种情况下,使用此数组参数注册:[new AddressEvent(), 'addCoordinatesToAddress']
,这意味着它将使用类AddressEvent
和函数addCoordinatesToAddress
。
若要测试这是否有效,以及是否使用此答案,可以在自己的控制器中创建触发器。我一直在AbstractActionController
的addAction
工作,它被AddressController
的addAction
调用。在上面侦听器的触发器下方:
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 - 这是调用事件的"组件"或对象。在这种情况下,它将
AddressControllerAddressController
- [get_class($entity) => $entity] - 这些是与此
Event
对象一起发送的参数。这将导致您具有具有$entity
值的可用$event->getParams()[Address::class]
。
前两个参数将在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
注册侦听器。这发生在模块Module.php
文件的onBootstrap
函数中(在本例中为地址)。像下面这样注册。
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$eventManager->attach(new AddressListener());
}
如果您在AbstractActionController
中调试addAction
的代码,请看到它传递触发器,接下来您在test
函数中,然后您的监听器工作。
上面的代码还暗示AddressListener
类可用于附加多个侦听器。所以你也可以注册entity.prePersist
、entity.preFlush
、entity.postFlush
和你能想到的任何其他东西。
接下来,将侦听器恢复到开始时的状态(还原attach
函数并删除test
函数)。
我还注意到,几乎每个Event
处理类都需要能够设置和获取EventManager
。因此,为此,我创建了一个AbstractEvent
类,如下所示。
namespace MvcEvent;
use ZendEventManagerEventManager;
use ZendEventManagerEventManagerAwareInterface;
use ZendEventManagerEventManagerInterface;
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 个标识符。但可以说它用于注册事件的回调。(如果有人倾向于提供它,这可以使用更多/详细的解释)
在AddressListener
中,我们尝试调用AddressEvent
类的addCoordinatesToAddress
函数。所以我们将不得不创建它,我像下面一样做了。
namespace AddressEvent;
use AddressEntityAddress;
use AddressServiceGoogleGeocodingService;
use CountryEntityCoordinates;
use MvcEventAbstractEvent;
use ZendEventManagerEvent;
use ZendEventManagerExceptionInvalidArgumentException;
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
创建。
将新的Coordinates
对象添加到现有Address
对象的代码没有修改,所以我不会让它成为答案的一部分,你可以在问题中找到它。