Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Commit 6184ccf

Browse files
committed
feat: Implement lazy listeners and lazy listener subscriber
Combines the features of LazyListener and LazyEventListener into `Zend\EventManager\ListenerProvider\LazyListener`. `LazyListenerSubscriber` is based on `LazyListenerAggregate`, but simplifies it by having it compose `LazyListener` instances only (no creation within it).
1 parent 7e39bea commit 6184ccf

File tree

5 files changed

+526
-8
lines changed

5 files changed

+526
-8
lines changed

TODO-PSR-14.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,16 @@
5151
- [x] `detach(PrioritizedListenerAttachmentInterface $provider)`
5252
- [x] Create `AbstractListenerSubscriber` and/or `ListenerSubscriberTrait`
5353
- [x] define a default `detach()` implementation
54-
- [ ] Create `LazyListenerSubscriber` based on `LazyListenerAggregate`
55-
- [ ] Define an alternate LazyListener:
56-
- [ ] `__construct(ContainerInterface $container, string $event = null, int $priority = 1)`
57-
- [ ] implements functionality from both `LazyListener` and `LazyEventListener`, minus passing env to container
58-
- [ ] without an event, can be attached to any provider
59-
- [ ] with an event, can be attached to `LazyListenerSubscriber`
60-
- Constructor aggregates `LazyListener` _instances_ only
61-
- [ ] `attach()` skips any where `getEvent()` returns null
54+
- [x] Create `LazyListenerSubscriber` based on `LazyListenerAggregate`
55+
- [x] Define an alternate LazyListener:
56+
- [x] `__construct(ContainerInterface $container, string $event = null, int $priority = 1)`
57+
- [x] implements functionality from both `LazyListener` and `LazyEventListener`, minus passing env to container
58+
- [x] without an event, can be attached to any provider
59+
- [x] with an event, can be attached to `LazyListenerSubscriber`
60+
- [x] Constructor aggregates `LazyListener` _instances_ only
61+
- [x] raises exception when `getEvent()` returns null
6262
- [ ] Event Dispatcher implementation
63+
- [ ] Implement `PrioritizedListenerAttachmentInterface` (if BC)
6364
- [ ] Create a `PrioritizedListenerProvider` instance in the `EventManger`
6465
constructor, and have the various `attach()`, `detach()`, etc. methods
6566
proxy to it.

src/ListenerProvider/LazyListener.php

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
/**
3+
* @see https://github.com/zendframework/zend-eventmanager for the canonical source repository
4+
* @copyright Copyright (c) 2019 Zend Technologies USA Inc. (https://www.zend.com)
5+
* @license https://github.com/zendframework/zend-eventmanager/blob/master/LICENSE.md New BSD License
6+
*/
7+
8+
namespace Zend\EventManager\ListenerProvider;
9+
10+
use Psr\Container\ContainerInterface;
11+
use Zend\EventManager\Exception;
12+
13+
/**
14+
* Lazy listener instance.
15+
*
16+
* Used to allow lazy creation of listeners via a dependency injection
17+
* container.
18+
*/
19+
class LazyListener
20+
{
21+
/**
22+
* @var ContainerInterface Container from which to pull listener.
23+
*/
24+
private $container;
25+
26+
/**
27+
* @var null|string Event name to which to attach; for use with
28+
* ListenerSubscriberInterface instances.
29+
*/
30+
private $event;
31+
32+
/**
33+
* @var object Service pulled from container
34+
*/
35+
private $listener;
36+
37+
/**
38+
* @var string Method name to invoke on listener.
39+
*/
40+
private $method;
41+
42+
/**
43+
* @var null|int Priority at which to attach; for use with
44+
* ListenerSubscriberInterface instances.
45+
*/
46+
private $priority;
47+
48+
/**
49+
* @var string Service name of listener.
50+
*/
51+
private $service;
52+
53+
/**
54+
* @param ContainerInterface $container Container from which to pull
55+
* listener service
56+
* @param string $listener Name of listener service to retrive from
57+
* container
58+
* @param null|string $method Name of method on listener service to use
59+
* when calling listener; defaults to __invoke.
60+
* @param null|string $event Name of event to which to attach; for use
61+
* with ListenerSubscriberInterface instances. In that scenario, null
62+
* indicates it should attach to any event.
63+
* @param null|int $priority Priority at which to attach; for use with
64+
* ListenerSubscriberInterface instances. In that scenario, null indicates
65+
* that the default priority should be used.
66+
* @throws Exception\InvalidArgumentException for invalid $listener arguments
67+
* @throws Exception\InvalidArgumentException for invalid $method arguments
68+
* @throws Exception\InvalidArgumentException for invalid $event arguments
69+
*/
70+
public function __construct(
71+
ContainerInterface $container,
72+
$listener,
73+
$method = '__invoke',
74+
$event = null,
75+
$priority = null
76+
) {
77+
if (! is_string($listener) || empty($listener)) {
78+
throw new Exception\InvalidArgumentException(sprintf(
79+
'%s requires a non-empty string $listener argument'
80+
. ' representing a service name; received %s',
81+
__CLASS__,
82+
gettype($listener)
83+
));
84+
}
85+
86+
if (! is_string($method)
87+
|| ! preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $method)
88+
) {
89+
throw new Exception\InvalidArgumentException(sprintf(
90+
'%s requires a valid string $method argument; received %s',
91+
__CLASS__,
92+
is_string($method) ? sprintf('"%s"', $method) : gettype($method)
93+
));
94+
}
95+
96+
if (null !== $event && (! is_string($event) || empty($event))) {
97+
throw new Exception\InvalidArgumentException(sprintf(
98+
'%s requires a null or non-empty string $event argument; received %s',
99+
__CLASS__,
100+
is_string($event) ? sprintf('"%s"', $event) : gettype($event)
101+
));
102+
}
103+
104+
$this->container = $container;
105+
$this->service = $listener;
106+
$this->method = $method;
107+
$this->event = $event;
108+
$this->priority = $priority;
109+
}
110+
111+
/**
112+
* Use the listener as an invokable, allowing direct attachment to an event manager.
113+
*
114+
* @param object $event
115+
* @return void
116+
*/
117+
public function __invoke($event)
118+
{
119+
$listener = $this->fetchListener();
120+
$method = $this->method;
121+
$listener->{$method}($event);
122+
}
123+
124+
/**
125+
* @return null|string
126+
*/
127+
public function getEvent()
128+
{
129+
return $this->event;
130+
}
131+
132+
/**
133+
* Return the priority, or, if not set, the default provided.
134+
*
135+
* @param int $default
136+
* @return int
137+
*/
138+
public function getPriority($default = 1)
139+
{
140+
return null !== $this->priority ? (int) $this->priority : (int) $default;
141+
}
142+
143+
/**
144+
* @return callable
145+
*/
146+
private function fetchListener()
147+
{
148+
if ($this->listener) {
149+
return $this->listener;
150+
}
151+
152+
$this->listener = $this->container->get($this->service);
153+
154+
return $this->listener;
155+
}
156+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
/**
3+
* @see https://github.com/zendframework/zend-eventmanager for the canonical source repository
4+
* @copyright Copyright (c) 2019 Zend Technologies USA Inc. (https://www.zend.com)
5+
* @license https://github.com/zendframework/zend-eventmanager/blob/master/LICENSE.md New BSD License
6+
*/
7+
8+
namespace Zend\EventManager\ListenerProvider;
9+
10+
use Zend\EventManager\Exception;
11+
12+
/**
13+
* Listener subscriber for attaching lazy listeners.
14+
*
15+
* Lazy listeners are listeners where creation is deferred until they are
16+
* triggered; this removes the most costly mechanism of pulling a listener
17+
* from a container unless the listener is actually invoked.
18+
*
19+
* Usage is:
20+
*
21+
* <code>
22+
* $subscriber = new LazyListenerSubscriber($listOfLazyListeners);
23+
* $subscriber->attach($provider, $priority);
24+
* ));
25+
* </code>
26+
*/
27+
class LazyListenerSubscriber implements ListenerSubscriberInterface
28+
{
29+
/**
30+
* LazyListener instances.
31+
*
32+
* @var LazyListener[]
33+
*/
34+
private $listeners = [];
35+
36+
/**
37+
* @throws Exception\InvalidArgumentException if any member of $listeners
38+
* is not a LazyListener instance.
39+
* @throws Exception\InvalidArgumentException if any member of $listeners
40+
* does not have a defined event to which to attach.
41+
*/
42+
public function __construct(array $listeners)
43+
{
44+
$this->validateListeners($listeners);
45+
$this->listeners = $listeners;
46+
}
47+
48+
/**
49+
* Subscribe listeners to the provider.
50+
*
51+
* Loops through all composed lazy listeners, and attaches them to the
52+
* provider.
53+
*/
54+
public function attach(PrioritizedListenerAttachmentInterface $provider, $priority = 1)
55+
{
56+
foreach ($this->listeners as $listener) {
57+
$provider->attach(
58+
$listener->getEvent(),
59+
$listener,
60+
$listener->getPriority($priority)
61+
);
62+
}
63+
}
64+
65+
public function detach(PrioritizedListenerAttachmentInterface $provider)
66+
{
67+
foreach ($this->listeners as $listener) {
68+
$provider->detach($listener, $listener->getEvent());
69+
}
70+
}
71+
72+
/**
73+
* @throws Exception\InvalidArgumentException if any member of $listeners
74+
* is not a LazyListener instance.
75+
* @throws Exception\InvalidArgumentException if any member of $listeners
76+
* does not have a defined event to which to attach.
77+
*/
78+
private function validateListeners(array $listeners)
79+
{
80+
foreach ($listeners as $index => $listener) {
81+
if (! $listener instanceof LazyListener) {
82+
throw new Exception\InvalidArgumentException(sprintf(
83+
'%s only accepts %s instances; received listener of type %s at index %s',
84+
__CLASS__,
85+
LazyListener::class,
86+
gettype($listener),
87+
$index
88+
));
89+
}
90+
91+
if (null === $listener->getEvent()) {
92+
throw new Exception\InvalidArgumentException(sprintf(
93+
'%s requires that all %s instances compose a non-empty string event to which to attach;'
94+
. ' none provided for listener at index %s',
95+
__CLASS__,
96+
LazyListener::class,
97+
$index
98+
));
99+
}
100+
}
101+
}
102+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
/**
3+
* @see https://github.com/zendframework/zend-eventmanager for the canonical source repository
4+
* @copyright Copyright (c) 2019 Zend Technologies USA Inc. (https://www.zend.com)
5+
* @license https://github.com/zendframework/zend-eventmanager/blob/master/LICENSE.md New BSD License
6+
*/
7+
8+
namespace ZendTest\EventManager\ListenerProvider;
9+
10+
use PHPUnit\Framework\TestCase;
11+
use Prophecy\Argument;
12+
use Zend\EventManager\Exception\InvalidArgumentException;
13+
use Zend\EventManager\ListenerProvider\LazyListener;
14+
use Zend\EventManager\ListenerProvider\LazyListenerSubscriber;
15+
use Zend\EventManager\ListenerProvider\PrioritizedListenerAttachmentInterface;
16+
17+
class LazyListenerAggregateTest extends TestCase
18+
{
19+
public function setUp()
20+
{
21+
$this->container = $this->prophesize(ContainerInterface::class);
22+
}
23+
24+
public function invalidListenerTypes()
25+
{
26+
return [
27+
'null' => [null],
28+
'true' => [true],
29+
'false' => [false],
30+
'zero' => [0],
31+
'int' => [1],
32+
'zero-float' => [0.0],
33+
'float' => [1.1],
34+
'string' => ['listener'],
35+
'array' => [['listener']],
36+
'object' => [(object) ['event' => 'event', 'listener' => 'listener', 'method' => 'method']],
37+
];
38+
}
39+
40+
/**
41+
* @dataProvider invalidListenerTypes
42+
*/
43+
public function testPassingInvalidListenerTypesAtInstantiationRaisesException($listener)
44+
{
45+
$this->expectException(InvalidArgumentException::class);
46+
$this->expectExceptionMessage('only accepts ' . LazyListener::class . ' instances');
47+
new LazyListenerSubscriber([$listener]);
48+
}
49+
50+
public function testPassingLazyListenersMissingAnEventAtInstantiationRaisesException()
51+
{
52+
$listener = $this->prophesize(LazyListener::class);
53+
$listener->getEvent()->willReturn(null);
54+
55+
$this->expectException(InvalidArgumentException::class);
56+
$this->expectExceptionMessage('compose a non-empty string event');
57+
new LazyListenerSubscriber([$listener->reveal()]);
58+
}
59+
60+
public function testAttachesLazyListenersToProviderUsingEventAndPriority()
61+
{
62+
$listener = $this->prophesize(LazyListener::class);
63+
$listener->getEvent()->willReturn('test');
64+
$listener->getPriority(1000)->willReturn(100);
65+
66+
$subscriber = new LazyListenerSubscriber([$listener->reveal()]);
67+
68+
$provider = $this->prophesize(PrioritizedListenerAttachmentInterface::class);
69+
$provider->attach('test', $listener->reveal(), 100)->shouldBeCalledTimes(1);
70+
71+
$this->assertNull($subscriber->attach($provider->reveal(), 1000));
72+
73+
return [
74+
'listener' => $listener,
75+
'subscriber' => $subscriber,
76+
'provider' => $provider,
77+
];
78+
}
79+
80+
/**
81+
* @depends testAttachesLazyListenersToProviderUsingEventAndPriority
82+
*/
83+
public function testDetachesLazyListenersFromProviderUsingEvent(array $dependencies)
84+
{
85+
$listener = $dependencies['listener'];
86+
$subscriber = $dependencies['subscriber'];
87+
$provider = $dependencies['provider'];
88+
89+
$provider->detach($listener->reveal(), 'test')->shouldBeCalledTimes(1);
90+
$this->assertNull($subscriber->detach($provider->reveal()));
91+
}
92+
}

0 commit comments

Comments
 (0)