1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\EventManager;
11
12use ArrayAccess;
13use ArrayObject;
14use Traversable;
15use Zend\Stdlib\CallbackHandler;
16use Zend\Stdlib\PriorityQueue;
17
18/**
19 * Event manager: notification system
20 *
21 * Use the EventManager when you want to create a per-instance notification
22 * system for your objects.
23 */
24class EventManager implements EventManagerInterface
25{
26    /**
27     * Subscribed events and their listeners
28     * @var array Array of PriorityQueue objects
29     */
30    protected $events = array();
31
32    /**
33     * @var string Class representing the event being emitted
34     */
35    protected $eventClass = 'Zend\EventManager\Event';
36
37    /**
38     * Identifiers, used to pull shared signals from SharedEventManagerInterface instance
39     * @var array
40     */
41    protected $identifiers = array();
42
43    /**
44     * Shared event manager
45     * @var false|null|SharedEventManagerInterface
46     */
47    protected $sharedManager = null;
48
49    /**
50     * Constructor
51     *
52     * Allows optionally specifying identifier(s) to use to pull signals from a
53     * SharedEventManagerInterface.
54     *
55     * @param  null|string|int|array|Traversable $identifiers
56     */
57    public function __construct($identifiers = null)
58    {
59        $this->setIdentifiers($identifiers);
60    }
61
62    /**
63     * Set the event class to utilize
64     *
65     * @param  string $class
66     * @return EventManager
67     */
68    public function setEventClass($class)
69    {
70        $this->eventClass = $class;
71        return $this;
72    }
73
74    /**
75     * Set shared event manager
76     *
77     * @param SharedEventManagerInterface $sharedEventManager
78     * @return EventManager
79     */
80    public function setSharedManager(SharedEventManagerInterface $sharedEventManager)
81    {
82        $this->sharedManager = $sharedEventManager;
83        StaticEventManager::setInstance($sharedEventManager);
84        return $this;
85    }
86
87    /**
88     * Remove any shared event manager currently attached
89     *
90     * @return void
91     */
92    public function unsetSharedManager()
93    {
94        $this->sharedManager = false;
95    }
96
97    /**
98     * Get shared event manager
99     *
100     * If one is not defined, but we have a static instance in
101     * StaticEventManager, that one will be used and set in this instance.
102     *
103     * If none is available in the StaticEventManager, a boolean false is
104     * returned.
105     *
106     * @return false|SharedEventManagerInterface
107     */
108    public function getSharedManager()
109    {
110        // "false" means "I do not want a shared manager; don't try and fetch one"
111        if (false === $this->sharedManager
112            || $this->sharedManager instanceof SharedEventManagerInterface
113        ) {
114            return $this->sharedManager;
115        }
116
117        if (!StaticEventManager::hasInstance()) {
118            return false;
119        }
120
121        $this->sharedManager = StaticEventManager::getInstance();
122        return $this->sharedManager;
123    }
124
125    /**
126     * Get the identifier(s) for this EventManager
127     *
128     * @return array
129     */
130    public function getIdentifiers()
131    {
132        return $this->identifiers;
133    }
134
135    /**
136     * Set the identifiers (overrides any currently set identifiers)
137     *
138     * @param string|int|array|Traversable $identifiers
139     * @return EventManager Provides a fluent interface
140     */
141    public function setIdentifiers($identifiers)
142    {
143        if (is_array($identifiers) || $identifiers instanceof Traversable) {
144            $this->identifiers = array_unique((array) $identifiers);
145        } elseif ($identifiers !== null) {
146            $this->identifiers = array($identifiers);
147        }
148        return $this;
149    }
150
151    /**
152     * Add some identifier(s) (appends to any currently set identifiers)
153     *
154     * @param string|int|array|Traversable $identifiers
155     * @return EventManager Provides a fluent interface
156     */
157    public function addIdentifiers($identifiers)
158    {
159        if (is_array($identifiers) || $identifiers instanceof Traversable) {
160            $this->identifiers = array_unique(array_merge($this->identifiers, (array) $identifiers));
161        } elseif ($identifiers !== null) {
162            $this->identifiers = array_unique(array_merge($this->identifiers, array($identifiers)));
163        }
164        return $this;
165    }
166
167    /**
168     * Trigger all listeners for a given event
169     *
170     * @param  string|EventInterface $event
171     * @param  string|object     $target   Object calling emit, or symbol describing target (such as static method name)
172     * @param  array|ArrayAccess $argv     Array of arguments; typically, should be associative
173     * @param  null|callable     $callback Trigger listeners until return value of this callback evaluate to true
174     * @return ResponseCollection All listener return values
175     * @throws Exception\InvalidCallbackException
176     */
177    public function trigger($event, $target = null, $argv = array(), $callback = null)
178    {
179        if ($event instanceof EventInterface) {
180            $e        = $event;
181            $event    = $e->getName();
182            $callback = $target;
183        } elseif ($target instanceof EventInterface) {
184            $e = $target;
185            $e->setName($event);
186            $callback = $argv;
187        } elseif ($argv instanceof EventInterface) {
188            $e = $argv;
189            $e->setName($event);
190            $e->setTarget($target);
191        } else {
192            $e = new $this->eventClass();
193            $e->setName($event);
194            $e->setTarget($target);
195            $e->setParams($argv);
196        }
197
198        if ($callback && !is_callable($callback)) {
199            throw new Exception\InvalidCallbackException('Invalid callback provided');
200        }
201
202        // Initial value of stop propagation flag should be false
203        $e->stopPropagation(false);
204
205        return $this->triggerListeners($event, $e, $callback);
206    }
207
208    /**
209     * Trigger listeners until return value of one causes a callback to
210     * evaluate to true
211     *
212     * Triggers listeners until the provided callback evaluates the return
213     * value of one as true, or until all listeners have been executed.
214     *
215     * @param  string|EventInterface $event
216     * @param  string|object $target Object calling emit, or symbol describing target (such as static method name)
217     * @param  array|ArrayAccess $argv Array of arguments; typically, should be associative
218     * @param  callable $callback
219     * @return ResponseCollection
220     * @deprecated Please use trigger()
221     * @throws Exception\InvalidCallbackException if invalid callable provided
222     */
223    public function triggerUntil($event, $target, $argv = null, $callback = null)
224    {
225        trigger_error(
226            'This method is deprecated and will be removed in the future. Please use trigger() instead.',
227            E_USER_DEPRECATED
228        );
229        return $this->trigger($event, $target, $argv, $callback);
230    }
231
232    /**
233     * Attach a listener to an event
234     *
235     * The first argument is the event, and the next argument describes a
236     * callback that will respond to that event. A CallbackHandler instance
237     * describing the event listener combination will be returned.
238     *
239     * The last argument indicates a priority at which the event should be
240     * executed. By default, this value is 1; however, you may set it for any
241     * integer value. Higher values have higher priority (i.e., execute first).
242     *
243     * You can specify "*" for the event name. In such cases, the listener will
244     * be triggered for every event.
245     *
246     * @param  string|array|ListenerAggregateInterface $event An event or array of event names. If a ListenerAggregateInterface, proxies to {@link attachAggregate()}.
247     * @param  callable|int $callback If string $event provided, expects PHP callback; for a ListenerAggregateInterface $event, this will be the priority
248     * @param  int $priority If provided, the priority at which to register the callable
249     * @return CallbackHandler|mixed CallbackHandler if attaching callable (to allow later unsubscribe); mixed if attaching aggregate
250     * @throws Exception\InvalidArgumentException
251     */
252    public function attach($event, $callback = null, $priority = 1)
253    {
254        // Proxy ListenerAggregateInterface arguments to attachAggregate()
255        if ($event instanceof ListenerAggregateInterface) {
256            return $this->attachAggregate($event, $callback);
257        }
258
259        // Null callback is invalid
260        if (null === $callback) {
261            throw new Exception\InvalidArgumentException(sprintf(
262                '%s: expects a callback; none provided',
263                __METHOD__
264            ));
265        }
266
267        // Array of events should be registered individually, and return an array of all listeners
268        if (is_array($event)) {
269            $listeners = array();
270            foreach ($event as $name) {
271                $listeners[] = $this->attach($name, $callback, $priority);
272            }
273            return $listeners;
274        }
275
276        // If we don't have a priority queue for the event yet, create one
277        if (empty($this->events[$event])) {
278            $this->events[$event] = new PriorityQueue();
279        }
280
281        // Create a callback handler, setting the event and priority in its metadata
282        $listener = new CallbackHandler($callback, array('event' => $event, 'priority' => $priority));
283
284        // Inject the callback handler into the queue
285        $this->events[$event]->insert($listener, $priority);
286        return $listener;
287    }
288
289    /**
290     * Attach a listener aggregate
291     *
292     * Listener aggregates accept an EventManagerInterface instance, and call attach()
293     * one or more times, typically to attach to multiple events using local
294     * methods.
295     *
296     * @param  ListenerAggregateInterface $aggregate
297     * @param  int $priority If provided, a suggested priority for the aggregate to use
298     * @return mixed return value of {@link ListenerAggregateInterface::attach()}
299     */
300    public function attachAggregate(ListenerAggregateInterface $aggregate, $priority = 1)
301    {
302        return $aggregate->attach($this, $priority);
303    }
304
305    /**
306     * Unsubscribe a listener from an event
307     *
308     * @param  CallbackHandler|ListenerAggregateInterface $listener
309     * @return bool Returns true if event and listener found, and unsubscribed; returns false if either event or listener not found
310     * @throws Exception\InvalidArgumentException if invalid listener provided
311     */
312    public function detach($listener)
313    {
314        if ($listener instanceof ListenerAggregateInterface) {
315            return $this->detachAggregate($listener);
316        }
317
318        if (!$listener instanceof CallbackHandler) {
319            throw new Exception\InvalidArgumentException(sprintf(
320                '%s: expected a ListenerAggregateInterface or CallbackHandler; received "%s"',
321                __METHOD__,
322                (is_object($listener) ? get_class($listener) : gettype($listener))
323            ));
324        }
325
326        $event = $listener->getMetadatum('event');
327        if (!$event || empty($this->events[$event])) {
328            return false;
329        }
330        $return = $this->events[$event]->remove($listener);
331        if (!$return) {
332            return false;
333        }
334        if (!count($this->events[$event])) {
335            unset($this->events[$event]);
336        }
337        return true;
338    }
339
340    /**
341     * Detach a listener aggregate
342     *
343     * Listener aggregates accept an EventManagerInterface instance, and call detach()
344     * of all previously attached listeners.
345     *
346     * @param  ListenerAggregateInterface $aggregate
347     * @return mixed return value of {@link ListenerAggregateInterface::detach()}
348     */
349    public function detachAggregate(ListenerAggregateInterface $aggregate)
350    {
351        return $aggregate->detach($this);
352    }
353
354    /**
355     * Retrieve all registered events
356     *
357     * @return array
358     */
359    public function getEvents()
360    {
361        return array_keys($this->events);
362    }
363
364    /**
365     * Retrieve all listeners for a given event
366     *
367     * @param  string $event
368     * @return PriorityQueue
369     */
370    public function getListeners($event)
371    {
372        if (!array_key_exists($event, $this->events)) {
373            return new PriorityQueue();
374        }
375        return $this->events[$event];
376    }
377
378    /**
379     * Clear all listeners for a given event
380     *
381     * @param  string $event
382     * @return void
383     */
384    public function clearListeners($event)
385    {
386        if (!empty($this->events[$event])) {
387            unset($this->events[$event]);
388        }
389    }
390
391    /**
392     * Prepare arguments
393     *
394     * Use this method if you want to be able to modify arguments from within a
395     * listener. It returns an ArrayObject of the arguments, which may then be
396     * passed to trigger().
397     *
398     * @param  array $args
399     * @return ArrayObject
400     */
401    public function prepareArgs(array $args)
402    {
403        return new ArrayObject($args);
404    }
405
406    /**
407     * Trigger listeners
408     *
409     * Actual functionality for triggering listeners, to which trigger() delegate.
410     *
411     * @param  string           $event Event name
412     * @param  EventInterface $e
413     * @param  null|callable    $callback
414     * @return ResponseCollection
415     */
416    protected function triggerListeners($event, EventInterface $e, $callback = null)
417    {
418        $responses = new ResponseCollection;
419        $listeners = $this->getListeners($event);
420
421        // Add shared/wildcard listeners to the list of listeners,
422        // but don't modify the listeners object
423        $sharedListeners         = $this->getSharedListeners($event);
424        $sharedWildcardListeners = $this->getSharedListeners('*');
425        $wildcardListeners       = $this->getListeners('*');
426        if (count($sharedListeners) || count($sharedWildcardListeners) || count($wildcardListeners)) {
427            $listeners = clone $listeners;
428
429            // Shared listeners on this specific event
430            $this->insertListeners($listeners, $sharedListeners);
431
432            // Shared wildcard listeners
433            $this->insertListeners($listeners, $sharedWildcardListeners);
434
435            // Add wildcard listeners
436            $this->insertListeners($listeners, $wildcardListeners);
437        }
438
439        foreach ($listeners as $listener) {
440            $listenerCallback = $listener->getCallback();
441
442            // Trigger the listener's callback, and push its result onto the
443            // response collection
444            $responses->push(call_user_func($listenerCallback, $e));
445
446            // If the event was asked to stop propagating, do so
447            if ($e->propagationIsStopped()) {
448                $responses->setStopped(true);
449                break;
450            }
451
452            // If the result causes our validation callback to return true,
453            // stop propagation
454            if ($callback && call_user_func($callback, $responses->last())) {
455                $responses->setStopped(true);
456                break;
457            }
458        }
459
460        return $responses;
461    }
462
463    /**
464     * Get list of all listeners attached to the shared event manager for
465     * identifiers registered by this instance
466     *
467     * @param  string $event
468     * @return array
469     */
470    protected function getSharedListeners($event)
471    {
472        if (!$sharedManager = $this->getSharedManager()) {
473            return array();
474        }
475
476        $identifiers     = $this->getIdentifiers();
477        //Add wildcard id to the search, if not already added
478        if (!in_array('*', $identifiers)) {
479            $identifiers[] = '*';
480        }
481        $sharedListeners = array();
482
483        foreach ($identifiers as $id) {
484            if (!$listeners = $sharedManager->getListeners($id, $event)) {
485                continue;
486            }
487
488            if (!is_array($listeners) && !($listeners instanceof Traversable)) {
489                continue;
490            }
491
492            foreach ($listeners as $listener) {
493                if (!$listener instanceof CallbackHandler) {
494                    continue;
495                }
496                $sharedListeners[] = $listener;
497            }
498        }
499
500        return $sharedListeners;
501    }
502
503    /**
504     * Add listeners to the master queue of listeners
505     *
506     * Used to inject shared listeners and wildcard listeners.
507     *
508     * @param  PriorityQueue $masterListeners
509     * @param  array|Traversable $listeners
510     * @return void
511     */
512    protected function insertListeners($masterListeners, $listeners)
513    {
514        foreach ($listeners as $listener) {
515            $priority = $listener->getMetadatum('priority');
516            if (null === $priority) {
517                $priority = 1;
518            } elseif (is_array($priority)) {
519                // If we have an array, likely using PriorityQueue. Grab first
520                // element of the array, as that's the actual priority.
521                $priority = array_shift($priority);
522            }
523            $masterListeners->insert($listener, $priority);
524        }
525    }
526}
527