[FEATURE] Introduce PSR-14-based EventDispatcher as alternative for hooks
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / Unit / EventDispatcher / ListenerProviderTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Core\Tests\Unit\EventDispatcher;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Prophecy\Prophecy\ObjectProphecy;
19 use Psr\Container\ContainerInterface;
20 use Psr\EventDispatcher\ListenerProviderInterface;
21 use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
22 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
23
24 /**
25 * Test case
26 */
27 class ListenerProviderTest extends UnitTestCase
28 {
29 /**
30 * @var ContainerInterface|ObjectProphecy
31 */
32 protected $containerProphecy;
33
34 /**
35 * @var ListenerProvider
36 */
37 protected $listenerProvider;
38
39 protected function setUp(): void
40 {
41 parent::setUp();
42
43 $this->containerProphecy = $this->prophesize();
44 $this->containerProphecy->willImplement(ContainerInterface::class);
45
46 $this->listenerProvider = new ListenerProvider(
47 $this->containerProphecy->reveal()
48 );
49 }
50
51 /**
52 * @test
53 */
54 public function implementsPsrInterface()
55 {
56 $this->assertInstanceOf(ListenerProviderInterface::class, $this->listenerProvider);
57 }
58
59 /**
60 * @test
61 */
62 public function addedListenersAreReturnedByGetAllListenerDefinitions()
63 {
64 $this->listenerProvider->addListener('Event\\Name', 'listener1');
65 $this->listenerProvider->addListener('Event\\Name', 'listener2', 'methodName');
66
67 $this->assertEquals($this->listenerProvider->getAllListenerDefinitions(), [
68 'Event\\Name' => [
69 [ 'service' => 'listener1', 'method' => null ],
70 [ 'service' => 'listener2', 'method' => 'methodName' ],
71 ]
72 ]);
73 }
74
75 /**
76 * @test
77 * @dataProvider listeners
78 */
79 public function dispatchesEvent($listener, string $method = null)
80 {
81 $event = new \stdClass;
82 $event->invoked = 0;
83
84 $this->containerProphecy->get('listener')->willReturn($listener);
85 $this->listenerProvider->addListener(\stdClass::class, 'listener', $method);
86
87 foreach ($this->listenerProvider->getListenersForEvent($event) as $listener) {
88 $listener($event);
89 }
90
91 $this->assertEquals(1, $event->invoked);
92 }
93
94 /**
95 * @test
96 * @dataProvider listeners
97 */
98 public function associatesToEventParentClass($listener, string $method = null)
99 {
100 $extendedEvent = new class extends \stdClass {
101 public $invoked = 0;
102 };
103
104 $this->containerProphecy->get('listener')->willReturn($listener);
105 $this->listenerProvider->addListener(\stdClass::class, 'listener', $method);
106 foreach ($this->listenerProvider->getListenersForEvent($extendedEvent) as $listener) {
107 $listener($extendedEvent);
108 }
109
110 $this->assertEquals(1, $extendedEvent->invoked);
111 }
112
113 /**
114 * @test
115 * @dataProvider listeners
116 */
117 public function associatesToImplementedInterfaces($listener, string $method = null)
118 {
119 $eventImplementation = new class implements \IteratorAggregate {
120 public $invoked = 0;
121
122 public function getIterator(): \Traversable
123 {
124 throw new \BadMethodCallException;
125 }
126 };
127
128 $this->containerProphecy->get('listener')->willReturn($listener);
129 $this->listenerProvider->addListener(\IteratorAggregate::class, 'listener', $method);
130 foreach ($this->listenerProvider->getListenersForEvent($eventImplementation) as $listener) {
131 $listener($eventImplementation);
132 }
133
134 $this->assertEquals(1, $eventImplementation->invoked);
135 }
136
137 /**
138 * @test
139 */
140 public function addListenerPreservesOrder()
141 {
142 $this->listenerProvider->addListener(\stdClass::class, 'listener1');
143 $this->listenerProvider->addListener(\stdClass::class, 'listener2');
144
145 $event = new \stdClass;
146 $event->sequence = '';
147 $this->containerProphecy->get('listener1')->willReturn(function (object $event): void {
148 $event->sequence .= 'a';
149 });
150 $this->containerProphecy->get('listener2')->willReturn(function (object $event): void {
151 $event->sequence .= 'b';
152 });
153 foreach ($this->listenerProvider->getListenersForEvent($event) as $listener) {
154 $listener($event);
155 }
156
157 $this->assertEquals('ab', $event->sequence);
158 }
159
160 /**
161 * @test
162 */
163 public function throwsExceptionForInvalidCallable()
164 {
165 $this->expectException(\InvalidArgumentException::class);
166 $this->expectExceptionCode(1549988537);
167
168 $event = new \stdClass;
169 $this->containerProphecy->get('listener')->willReturn(new \stdClass);
170 $this->listenerProvider->addListener(\stdClass::class, 'listener');
171 foreach ($this->listenerProvider->getListenersForEvent($event) as $listener) {
172 $listener($event);
173 }
174 }
175
176 /**
177 * Provider for event listeners.
178 * Either an invokable, class/method combination or a closure.
179 */
180 public function listeners(): array
181 {
182 return [
183 [
184 // Invokable
185 'listener' => new class {
186 public function __invoke(object $event): void
187 {
188 $event->invoked = 1;
189 }
190 },
191 'method' => null,
192 ],
193 [
194 // Class + method
195 'listener' => new class {
196 public function onEvent(object $event): void
197 {
198 $event->invoked = 1;
199 }
200 },
201 'method' => 'onEvent',
202 ],
203 [
204 // Closure
205 'listener' => function (object $event): void {
206 $event->invoked = 1;
207 },
208 'method' => null,
209 ]
210 ];
211 }
212 }