blob: a4ae3def0fcababa60f59393e42a3aa8169ba892 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace LdapRecord\Events;
4
5use LdapRecord\Support\Arr;
6
7/**
8 * Class Dispatcher.
9 *
10 * Handles event listening and dispatching.
11 *
12 * This code was taken out of the Laravel Framework core
13 * with broadcasting and queuing omitted to remove
14 * an extra dependency that would be required.
15 *
16 * @author Taylor Otwell
17 *
18 * @see https://github.com/laravel/framework
19 */
20class Dispatcher implements DispatcherInterface
21{
22 /**
23 * The registered event listeners.
24 *
25 * @var array
26 */
27 protected $listeners = [];
28
29 /**
30 * The wildcard listeners.
31 *
32 * @var array
33 */
34 protected $wildcards = [];
35
36 /**
37 * The cached wildcard listeners.
38 *
39 * @var array
40 */
41 protected $wildcardsCache = [];
42
43 /**
44 * @inheritdoc
45 */
46 public function listen($events, $listener)
47 {
48 foreach ((array) $events as $event) {
49 if (strpos($event, '*') !== false) {
50 $this->setupWildcardListen($event, $listener);
51 } else {
52 $this->listeners[$event][] = $this->makeListener($listener);
53 }
54 }
55 }
56
57 /**
58 * Setup a wildcard listener callback.
59 *
60 * @param string $event
61 * @param mixed $listener
62 *
63 * @return void
64 */
65 protected function setupWildcardListen($event, $listener)
66 {
67 $this->wildcards[$event][] = $this->makeListener($listener, true);
68
69 $this->wildcardsCache = [];
70 }
71
72 /**
73 * @inheritdoc
74 */
75 public function hasListeners($eventName)
76 {
77 return isset($this->listeners[$eventName]) || isset($this->wildcards[$eventName]);
78 }
79
80 /**
81 * @inheritdoc
82 */
83 public function until($event, $payload = [])
84 {
85 return $this->dispatch($event, $payload, true);
86 }
87
88 /**
89 * @inheritdoc
90 */
91 public function fire($event, $payload = [], $halt = false)
92 {
93 return $this->dispatch($event, $payload, $halt);
94 }
95
96 /**
97 * @inheritdoc
98 */
99 public function dispatch($event, $payload = [], $halt = false)
100 {
101 // When the given "event" is actually an object we will assume it is an event
102 // object and use the class as the event name and this event itself as the
103 // payload to the handler, which makes object based events quite simple.
104 [$event, $payload] = $this->parseEventAndPayload(
105 $event,
106 $payload
107 );
108
109 $responses = [];
110
111 foreach ($this->getListeners($event) as $listener) {
112 $response = $listener($event, $payload);
113
114 // If a response is returned from the listener and event halting is enabled
115 // we will just return this response, and not call the rest of the event
116 // listeners. Otherwise we will add the response on the response list.
117 if ($halt && ! is_null($response)) {
118 return $response;
119 }
120
121 // If a boolean false is returned from a listener, we will stop propagating
122 // the event to any further listeners down in the chain, else we keep on
123 // looping through the listeners and firing every one in our sequence.
124 if ($response === false) {
125 break;
126 }
127
128 $responses[] = $response;
129 }
130
131 return $halt ? null : $responses;
132 }
133
134 /**
135 * Parse the given event and payload and prepare them for dispatching.
136 *
137 * @param mixed $event
138 * @param mixed $payload
139 *
140 * @return array
141 */
142 protected function parseEventAndPayload($event, $payload)
143 {
144 if (is_object($event)) {
145 [$payload, $event] = [[$event], get_class($event)];
146 }
147
148 return [$event, Arr::wrap($payload)];
149 }
150
151 /**
152 * @inheritdoc
153 */
154 public function getListeners($eventName)
155 {
156 $listeners = $this->listeners[$eventName] ?? [];
157
158 $listeners = array_merge(
159 $listeners,
160 $this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
161 );
162
163 return class_exists($eventName, false)
164 ? $this->addInterfaceListeners($eventName, $listeners)
165 : $listeners;
166 }
167
168 /**
169 * Get the wildcard listeners for the event.
170 *
171 * @param string $eventName
172 *
173 * @return array
174 */
175 protected function getWildcardListeners($eventName)
176 {
177 $wildcards = [];
178
179 foreach ($this->wildcards as $key => $listeners) {
180 if ($this->wildcardContainsEvent($key, $eventName)) {
181 $wildcards = array_merge($wildcards, $listeners);
182 }
183 }
184
185 return $this->wildcardsCache[$eventName] = $wildcards;
186 }
187
188 /**
189 * Determine if the wildcard matches or contains the given event.
190 *
191 * This function is a direct excerpt from Laravel's Str::is().
192 *
193 * @param string $wildcard
194 * @param string $eventName
195 *
196 * @return bool
197 */
198 protected function wildcardContainsEvent($wildcard, $eventName)
199 {
200 $patterns = Arr::wrap($wildcard);
201
202 if (empty($patterns)) {
203 return false;
204 }
205
206 foreach ($patterns as $pattern) {
207 // If the given event is an exact match we can of course return true right
208 // from the beginning. Otherwise, we will translate asterisks and do an
209 // actual pattern match against the two strings to see if they match.
210 if ($pattern == $eventName) {
211 return true;
212 }
213
214 $pattern = preg_quote($pattern, '#');
215
216 // Asterisks are translated into zero-or-more regular expression wildcards
217 // to make it convenient to check if the strings starts with the given
218 // pattern such as "library/*", making any string check convenient.
219 $pattern = str_replace('\*', '.*', $pattern);
220
221 if (preg_match('#^'.$pattern.'\z#u', $eventName) === 1) {
222 return true;
223 }
224 }
225
226 return false;
227 }
228
229 /**
230 * Add the listeners for the event's interfaces to the given array.
231 *
232 * @param string $eventName
233 * @param array $listeners
234 *
235 * @return array
236 */
237 protected function addInterfaceListeners($eventName, array $listeners = [])
238 {
239 foreach (class_implements($eventName) as $interface) {
240 if (isset($this->listeners[$interface])) {
241 foreach ($this->listeners[$interface] as $names) {
242 $listeners = array_merge($listeners, (array) $names);
243 }
244 }
245 }
246
247 return $listeners;
248 }
249
250 /**
251 * Register an event listener with the dispatcher.
252 *
253 * @param \Closure|string $listener
254 * @param bool $wildcard
255 *
256 * @return \Closure
257 */
258 public function makeListener($listener, $wildcard = false)
259 {
260 if (is_string($listener)) {
261 return $this->createClassListener($listener, $wildcard);
262 }
263
264 return function ($event, $payload) use ($listener, $wildcard) {
265 if ($wildcard) {
266 return $listener($event, $payload);
267 }
268
269 return $listener(...array_values($payload));
270 };
271 }
272
273 /**
274 * Create a class based listener.
275 *
276 * @param string $listener
277 * @param bool $wildcard
278 *
279 * @return \Closure
280 */
281 protected function createClassListener($listener, $wildcard = false)
282 {
283 return function ($event, $payload) use ($listener, $wildcard) {
284 if ($wildcard) {
285 return call_user_func($this->createClassCallable($listener), $event, $payload);
286 }
287
288 return call_user_func_array(
289 $this->createClassCallable($listener),
290 $payload
291 );
292 };
293 }
294
295 /**
296 * Create the class based event callable.
297 *
298 * @param string $listener
299 *
300 * @return callable
301 */
302 protected function createClassCallable($listener)
303 {
304 [$class, $method] = $this->parseListenerCallback($listener);
305
306 return [new $class(), $method];
307 }
308
309 /**
310 * Parse the class listener into class and method.
311 *
312 * @param string $listener
313 *
314 * @return array
315 */
316 protected function parseListenerCallback($listener)
317 {
318 return strpos($listener, '@') !== false
319 ? explode('@', $listener, 2)
320 : [$listener, 'handle'];
321 }
322
323 /**
324 * @inheritdoc
325 */
326 public function forget($event)
327 {
328 if (strpos($event, '*') !== false) {
329 unset($this->wildcards[$event]);
330 } else {
331 unset($this->listeners[$event]);
332 }
333 }
334}