1 <?php
2 /**
3 * Part of the Joomla Framework Event Package
4 *
5 * @copyright Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE
7 */
8
9 namespace Joomla\Event;
10
11 use InvalidArgumentException;
12 use Closure;
13
14 /**
15 * Implementation of a DispatcherInterface supporting
16 * prioritized listeners.
17 *
18 * @since 1.0
19 */
20 class Dispatcher implements DispatcherInterface
21 {
22 /**
23 * An array of registered events indexed by
24 * the event names.
25 *
26 * @var EventInterface[]
27 *
28 * @since 1.0
29 */
30 protected $events = array();
31
32 /**
33 * A regular expression that will filter listener method names.
34 *
35 * @var string
36 * @since 1.0
37 * @deprecated
38 */
39 protected $listenerFilter;
40
41 /**
42 * An array of ListenersPriorityQueue indexed
43 * by the event names.
44 *
45 * @var ListenersPriorityQueue[]
46 *
47 * @since 1.0
48 */
49 protected $listeners = array();
50
51 /**
52 * Set an event to the dispatcher.
53 * It will replace any event with the same name.
54 *
55 * @param EventInterface $event The event.
56 *
57 * @return Dispatcher This method is chainable.
58 *
59 * @since 1.0
60 */
61 public function setEvent(EventInterface $event)
62 {
63 $this->events[$event->getName()] = $event;
64
65 return $this;
66 }
67
68 /**
69 * Sets a regular expression to filter the class methods when adding a listener.
70 *
71 * @param string $regex A regular expression (for example '^on' will only register methods starting with "on").
72 *
73 * @return Dispatcher This method is chainable.
74 *
75 * @since 1.0
76 * @deprecated Incorporate a method in your listener object such as `getEvents` to feed into the `setListener` method.
77 */
78 public function setListenerFilter($regex)
79 {
80 $this->listenerFilter = $regex;
81
82 return $this;
83 }
84
85 /**
86 * Add an event to this dispatcher, only if it is not existing.
87 *
88 * @param EventInterface $event The event.
89 *
90 * @return Dispatcher This method is chainable.
91 *
92 * @since 1.0
93 */
94 public function addEvent(EventInterface $event)
95 {
96 if (!isset($this->events[$event->getName()]))
97 {
98 $this->events[$event->getName()] = $event;
99 }
100
101 return $this;
102 }
103
104 /**
105 * Tell if the given event has been added to this dispatcher.
106 *
107 * @param EventInterface|string $event The event object or name.
108 *
109 * @return boolean True if the listener has the given event, false otherwise.
110 *
111 * @since 1.0
112 */
113 public function hasEvent($event)
114 {
115 if ($event instanceof EventInterface)
116 {
117 $event = $event->getName();
118 }
119
120 return isset($this->events[$event]);
121 }
122
123 /**
124 * Get the event object identified by the given name.
125 *
126 * @param string $name The event name.
127 * @param mixed $default The default value if the event was not registered.
128 *
129 * @return EventInterface|mixed The event of the default value.
130 *
131 * @since 1.0
132 */
133 public function getEvent($name, $default = null)
134 {
135 if (isset($this->events[$name]))
136 {
137 return $this->events[$name];
138 }
139
140 return $default;
141 }
142
143 /**
144 * Remove an event from this dispatcher.
145 * The registered listeners will remain.
146 *
147 * @param EventInterface|string $event The event object or name.
148 *
149 * @return Dispatcher This method is chainable.
150 *
151 * @since 1.0
152 */
153 public function removeEvent($event)
154 {
155 if ($event instanceof EventInterface)
156 {
157 $event = $event->getName();
158 }
159
160 if (isset($this->events[$event]))
161 {
162 unset($this->events[$event]);
163 }
164
165 return $this;
166 }
167
168 /**
169 * Get the registered events.
170 *
171 * @return EventInterface[] The registered event.
172 *
173 * @since 1.0
174 */
175 public function getEvents()
176 {
177 return $this->events;
178 }
179
180 /**
181 * Clear all events.
182 *
183 * @return EventInterface[] The old events.
184 *
185 * @since 1.0
186 */
187 public function clearEvents()
188 {
189 $events = $this->events;
190 $this->events = array();
191
192 return $events;
193 }
194
195 /**
196 * Count the number of registered event.
197 *
198 * @return integer The numer of registered events.
199 *
200 * @since 1.0
201 */
202 public function countEvents()
203 {
204 return count($this->events);
205 }
206
207 /**
208 * Add a listener to this dispatcher, only if not already registered to these events.
209 * If no events are specified, it will be registered to all events matching it's methods name.
210 * In the case of a closure, you must specify at least one event name.
211 *
212 * @param object|Closure $listener The listener
213 * @param array $events An associative array of event names as keys
214 * and the corresponding listener priority as values.
215 *
216 * @return Dispatcher This method is chainable.
217 *
218 * @throws InvalidArgumentException
219 *
220 * @since 1.0
221 */
222 public function addListener($listener, array $events = array())
223 {
224 if (!is_object($listener))
225 {
226 throw new InvalidArgumentException('The given listener is not an object.');
227 }
228
229 // We deal with a closure.
230 if ($listener instanceof Closure)
231 {
232 if (empty($events))
233 {
234 throw new InvalidArgumentException('No event name(s) and priority
235 specified for the Closure listener.');
236 }
237
238 foreach ($events as $name => $priority)
239 {
240 if (!isset($this->listeners[$name]))
241 {
242 $this->listeners[$name] = new ListenersPriorityQueue;
243 }
244
245 $this->listeners[$name]->add($listener, $priority);
246 }
247
248 return $this;
249 }
250
251 // We deal with a "normal" object.
252 $methods = get_class_methods($listener);
253
254 if (!empty($events))
255 {
256 $methods = array_intersect($methods, array_keys($events));
257 }
258
259 // @deprecated
260 $regex = $this->listenerFilter ?: '.*';
261
262 foreach ($methods as $event)
263 {
264 // @deprecated - this outer `if` is deprecated.
265 if (preg_match("#$regex#", $event))
266 {
267 // Retain this inner code after removal of the outer `if`.
268 if (!isset($this->listeners[$event]))
269 {
270 $this->listeners[$event] = new ListenersPriorityQueue;
271 }
272
273 $priority = isset($events[$event]) ? $events[$event] : Priority::NORMAL;
274
275 $this->listeners[$event]->add($listener, $priority);
276 }
277 }
278
279 return $this;
280 }
281
282 /**
283 * Get the priority of the given listener for the given event.
284 *
285 * @param object|Closure $listener The listener.
286 * @param EventInterface|string $event The event object or name.
287 *
288 * @return mixed The listener priority or null if the listener doesn't exist.
289 *
290 * @since 1.0
291 */
292 public function getListenerPriority($listener, $event)
293 {
294 if ($event instanceof EventInterface)
295 {
296 $event = $event->getName();
297 }
298
299 if (isset($this->listeners[$event]))
300 {
301 return $this->listeners[$event]->getPriority($listener);
302 }
303
304 return null;
305 }
306
307 /**
308 * Get the listeners registered to the given event.
309 *
310 * @param EventInterface|string $event The event object or name.
311 *
312 * @return object[] An array of registered listeners sorted according to their priorities.
313 *
314 * @since 1.0
315 */
316 public function getListeners($event)
317 {
318 if ($event instanceof EventInterface)
319 {
320 $event = $event->getName();
321 }
322
323 if (isset($this->listeners[$event]))
324 {
325 return $this->listeners[$event]->getAll();
326 }
327
328 return array();
329 }
330
331 /**
332 * Tell if the given listener has been added.
333 * If an event is specified, it will tell if the listener is registered for that event.
334 *
335 * @param object|Closure $listener The listener.
336 * @param EventInterface|string $event The event object or name.
337 *
338 * @return boolean True if the listener is registered, false otherwise.
339 *
340 * @since 1.0
341 */
342 public function hasListener($listener, $event = null)
343 {
344 if ($event)
345 {
346 if ($event instanceof EventInterface)
347 {
348 $event = $event->getName();
349 }
350
351 if (isset($this->listeners[$event]))
352 {
353 return $this->listeners[$event]->has($listener);
354 }
355 }
356 else
357 {
358 foreach ($this->listeners as $queue)
359 {
360 if ($queue->has($listener))
361 {
362 return true;
363 }
364 }
365 }
366
367 return false;
368 }
369
370 /**
371 * Remove the given listener from this dispatcher.
372 * If no event is specified, it will be removed from all events it is listening to.
373 *
374 * @param object|Closure $listener The listener to remove.
375 * @param EventInterface|string $event The event object or name.
376 *
377 * @return Dispatcher This method is chainable.
378 *
379 * @since 1.0
380 */
381 public function removeListener($listener, $event = null)
382 {
383 if ($event)
384 {
385 if ($event instanceof EventInterface)
386 {
387 $event = $event->getName();
388 }
389
390 if (isset($this->listeners[$event]))
391 {
392 $this->listeners[$event]->remove($listener);
393 }
394 }
395
396 else
397 {
398 foreach ($this->listeners as $queue)
399 {
400 $queue->remove($listener);
401 }
402 }
403
404 return $this;
405 }
406
407 /**
408 * Clear the listeners in this dispatcher.
409 * If an event is specified, the listeners will be cleared only for that event.
410 *
411 * @param EventInterface|string $event The event object or name.
412 *
413 * @return Dispatcher This method is chainable.
414 *
415 * @since 1.0
416 */
417 public function clearListeners($event = null)
418 {
419 if ($event)
420 {
421 if ($event instanceof EventInterface)
422 {
423 $event = $event->getName();
424 }
425
426 if (isset($this->listeners[$event]))
427 {
428 unset($this->listeners[$event]);
429 }
430 }
431
432 else
433 {
434 $this->listeners = array();
435 }
436
437 return $this;
438 }
439
440 /**
441 * Count the number of registered listeners for the given event.
442 *
443 * @param EventInterface|string $event The event object or name.
444 *
445 * @return integer The number of registered listeners for the given event.
446 *
447 * @since 1.0
448 */
449 public function countListeners($event)
450 {
451 if ($event instanceof EventInterface)
452 {
453 $event = $event->getName();
454 }
455
456 return isset($this->listeners[$event]) ? count($this->listeners[$event]) : 0;
457 }
458
459 /**
460 * Trigger an event.
461 *
462 * @param EventInterface|string $event The event object or name.
463 *
464 * @return EventInterface The event after being passed through all listeners.
465 *
466 * @since 1.0
467 */
468 public function triggerEvent($event)
469 {
470 if (!($event instanceof EventInterface))
471 {
472 if (isset($this->events[$event]))
473 {
474 $event = $this->events[$event];
475 }
476
477 else
478 {
479 $event = new Event($event);
480 }
481 }
482
483 if (isset($this->listeners[$event->getName()]))
484 {
485 foreach ($this->listeners[$event->getName()] as $listener)
486 {
487 if ($event->isStopped())
488 {
489 return $event;
490 }
491
492 if ($listener instanceof Closure)
493 {
494 call_user_func($listener, $event);
495 }
496
497 else
498 {
499 call_user_func(array($listener, $event->getName()), $event);
500 }
501 }
502 }
503
504 return $event;
505 }
506 }
507