<?php /** * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ namespace Zend\EventManager; use ArrayAccess; use ArrayObject; use Traversable; use Zend\Stdlib\CallbackHandler; use Zend\Stdlib\PriorityQueue; /** * Event manager: notification system * * Use the EventManager when you want to create a per-instance notification * system for your objects. */ class EventManager implements EventManagerInterface { /** * Subscribed events and their listeners * @var array Array of PriorityQueue objects */ protected $events = array(); /** * @var string Class representing the event being emitted */ protected $eventClass = 'Zend\EventManager\Event'; /** * Identifiers, used to pull shared signals from SharedEventManagerInterface instance * @var array */ protected $identifiers = array(); /** * Shared event manager * @var false|null|SharedEventManagerInterface */ protected $sharedManager = null; /** * Constructor * * Allows optionally specifying identifier(s) to use to pull signals from a * SharedEventManagerInterface. * * @param null|string|int|array|Traversable $identifiers */ public function __construct($identifiers = null) { $this->setIdentifiers($identifiers); } /** * Set the event class to utilize * * @param string $class * @return EventManager */ public function setEventClass($class) { $this->eventClass = $class; return $this; } /** * Set shared event manager * * @param SharedEventManagerInterface $sharedEventManager * @return EventManager */ public function setSharedManager(SharedEventManagerInterface $sharedEventManager) { $this->sharedManager = $sharedEventManager; StaticEventManager::setInstance($sharedEventManager); return $this; } /** * Remove any shared event manager currently attached * * @return void */ public function unsetSharedManager() { $this->sharedManager = false; } /** * Get shared event manager * * If one is not defined, but we have a static instance in * StaticEventManager, that one will be used and set in this instance. * * If none is available in the StaticEventManager, a boolean false is * returned. * * @return false|SharedEventManagerInterface */ public function getSharedManager() { // "false" means "I do not want a shared manager; don't try and fetch one" if (false === $this->sharedManager || $this->sharedManager instanceof SharedEventManagerInterface ) { return $this->sharedManager; } if (!StaticEventManager::hasInstance()) { return false; } $this->sharedManager = StaticEventManager::getInstance(); return $this->sharedManager; } /** * Get the identifier(s) for this EventManager * * @return array */ public function getIdentifiers() { return $this->identifiers; } /** * Set the identifiers (overrides any currently set identifiers) * * @param string|int|array|Traversable $identifiers * @return EventManager Provides a fluent interface */ public function setIdentifiers($identifiers) { if (is_array($identifiers) || $identifiers instanceof Traversable) { $this->identifiers = array_unique((array) $identifiers); } elseif ($identifiers !== null) { $this->identifiers = array($identifiers); } return $this; } /** * Add some identifier(s) (appends to any currently set identifiers) * * @param string|int|array|Traversable $identifiers * @return EventManager Provides a fluent interface */ public function addIdentifiers($identifiers) { if (is_array($identifiers) || $identifiers instanceof Traversable) { $this->identifiers = array_unique(array_merge($this->identifiers, (array) $identifiers)); } elseif ($identifiers !== null) { $this->identifiers = array_unique(array_merge($this->identifiers, array($identifiers))); } return $this; } /** * Trigger all listeners for a given event * * Can emulate triggerUntil() if the last argument provided is a callback. * * @param string $event * @param string|object $target Object calling emit, or symbol describing target (such as static method name) * @param array|ArrayAccess $argv Array of arguments; typically, should be associative * @param null|callable $callback * @return ResponseCollection All listener return values * @throws Exception\InvalidCallbackException */ public function trigger($event, $target = null, $argv = array(), $callback = null) { if ($event instanceof EventInterface) { $e = $event; $event = $e->getName(); $callback = $target; } elseif ($target instanceof EventInterface) { $e = $target; $e->setName($event); $callback = $argv; } elseif ($argv instanceof EventInterface) { $e = $argv; $e->setName($event); $e->setTarget($target); } else { $e = new $this->eventClass(); $e->setName($event); $e->setTarget($target); $e->setParams($argv); } if ($callback && !is_callable($callback)) { throw new Exception\InvalidCallbackException('Invalid callback provided'); } // Initial value of stop propagation flag should be false $e->stopPropagation(false); return $this->triggerListeners($event, $e, $callback); } /** * Trigger listeners until return value of one causes a callback to * evaluate to true * * Triggers listeners until the provided callback evaluates the return * value of one as true, or until all listeners have been executed. * * @param string $event * @param string|object $target Object calling emit, or symbol describing target (such as static method name) * @param array|ArrayAccess $argv Array of arguments; typically, should be associative * @param callable $callback * @return ResponseCollection * @throws Exception\InvalidCallbackException if invalid callable provided */ public function triggerUntil($event, $target, $argv = null, $callback = null) { if ($event instanceof EventInterface) { $e = $event; $event = $e->getName(); $callback = $target; } elseif ($target instanceof EventInterface) { $e = $target; $e->setName($event); $callback = $argv; } elseif ($argv instanceof EventInterface) { $e = $argv; $e->setName($event); $e->setTarget($target); } else { $e = new $this->eventClass(); $e->setName($event); $e->setTarget($target); $e->setParams($argv); } if (!is_callable($callback)) { throw new Exception\InvalidCallbackException('Invalid callback provided'); } // Initial value of stop propagation flag should be false $e->stopPropagation(false); return $this->triggerListeners($event, $e, $callback); } /** * Attach a listener to an event * * The first argument is the event, and the next argument describes a * callback that will respond to that event. A CallbackHandler instance * describing the event listener combination will be returned. * * The last argument indicates a priority at which the event should be * executed. By default, this value is 1; however, you may set it for any * integer value. Higher values have higher priority (i.e., execute first). * * You can specify "*" for the event name. In such cases, the listener will * be triggered for every event. * * @param string|array|ListenerAggregateInterface $event An event or array of event names. If a ListenerAggregateInterface, proxies to {@link attachAggregate()}. * @param callable|int $callback If string $event provided, expects PHP callback; for a ListenerAggregateInterface $event, this will be the priority * @param int $priority If provided, the priority at which to register the callable * @return CallbackHandler|mixed CallbackHandler if attaching callable (to allow later unsubscribe); mixed if attaching aggregate * @throws Exception\InvalidArgumentException */ public function attach($event, $callback = null, $priority = 1) { // Proxy ListenerAggregateInterface arguments to attachAggregate() if ($event instanceof ListenerAggregateInterface) { return $this->attachAggregate($event, $callback); } // Null callback is invalid if (null === $callback) { throw new Exception\InvalidArgumentException(sprintf( '%s: expects a callback; none provided', __METHOD__ )); } // Array of events should be registered individually, and return an array of all listeners if (is_array($event)) { $listeners = array(); foreach ($event as $name) { $listeners[] = $this->attach($name, $callback, $priority); } return $listeners; } // If we don't have a priority queue for the event yet, create one if (empty($this->events[$event])) { $this->events[$event] = new PriorityQueue(); } // Create a callback handler, setting the event and priority in its metadata $listener = new CallbackHandler($callback, array('event' => $event, 'priority' => $priority)); // Inject the callback handler into the queue $this->events[$event]->insert($listener, $priority); return $listener; } /** * Attach a listener aggregate * * Listener aggregates accept an EventManagerInterface instance, and call attach() * one or more times, typically to attach to multiple events using local * methods. * * @param ListenerAggregateInterface $aggregate * @param int $priority If provided, a suggested priority for the aggregate to use * @return mixed return value of {@link ListenerAggregateInterface::attach()} */ public function attachAggregate(ListenerAggregateInterface $aggregate, $priority = 1) { return $aggregate->attach($this, $priority); } /** * Unsubscribe a listener from an event * * @param CallbackHandler|ListenerAggregateInterface $listener * @return bool Returns true if event and listener found, and unsubscribed; returns false if either event or listener not found * @throws Exception\InvalidArgumentException if invalid listener provided */ public function detach($listener) { if ($listener instanceof ListenerAggregateInterface) { return $this->detachAggregate($listener); } if (!$listener instanceof CallbackHandler) { throw new Exception\InvalidArgumentException(sprintf( '%s: expected a ListenerAggregateInterface or CallbackHandler; received "%s"', __METHOD__, (is_object($listener) ? get_class($listener) : gettype($listener)) )); } $event = $listener->getMetadatum('event'); if (!$event || empty($this->events[$event])) { return false; } $return = $this->events[$event]->remove($listener); if (!$return) { return false; } if (!count($this->events[$event])) { unset($this->events[$event]); } return true; } /** * Detach a listener aggregate * * Listener aggregates accept an EventManagerInterface instance, and call detach() * of all previously attached listeners. * * @param ListenerAggregateInterface $aggregate * @return mixed return value of {@link ListenerAggregateInterface::detach()} */ public function detachAggregate(ListenerAggregateInterface $aggregate) { return $aggregate->detach($this); } /** * Retrieve all registered events * * @return array */ public function getEvents() { return array_keys($this->events); } /** * Retrieve all listeners for a given event * * @param string $event * @return PriorityQueue */ public function getListeners($event) { if (!array_key_exists($event, $this->events)) { return new PriorityQueue(); } return $this->events[$event]; } /** * Clear all listeners for a given event * * @param string $event * @return void */ public function clearListeners($event) { if (!empty($this->events[$event])) { unset($this->events[$event]); } } /** * Prepare arguments * * Use this method if you want to be able to modify arguments from within a * listener. It returns an ArrayObject of the arguments, which may then be * passed to trigger() or triggerUntil(). * * @param array $args * @return ArrayObject */ public function prepareArgs(array $args) { return new ArrayObject($args); } /** * Trigger listeners * * Actual functionality for triggering listeners, to which both trigger() and triggerUntil() * delegate. * * @param string $event Event name * @param EventInterface $e * @param null|callable $callback * @return ResponseCollection */ protected function triggerListeners($event, EventInterface $e, $callback = null) { $responses = new ResponseCollection; $listeners = $this->getListeners($event); // Add shared/wildcard listeners to the list of listeners, // but don't modify the listeners object $sharedListeners = $this->getSharedListeners($event); $sharedWildcardListeners = $this->getSharedListeners('*'); $wildcardListeners = $this->getListeners('*'); if (count($sharedListeners) || count($sharedWildcardListeners) || count($wildcardListeners)) { $listeners = clone $listeners; // Shared listeners on this specific event $this->insertListeners($listeners, $sharedListeners); // Shared wildcard listeners $this->insertListeners($listeners, $sharedWildcardListeners); // Add wildcard listeners $this->insertListeners($listeners, $wildcardListeners); } foreach ($listeners as $listener) { $listenerCallback = $listener->getCallback(); // Trigger the listener's callback, and push its result onto the // response collection $responses->push(call_user_func($listenerCallback, $e)); // If the event was asked to stop propagating, do so if ($e->propagationIsStopped()) { $responses->setStopped(true); break; } // If the result causes our validation callback to return true, // stop propagation if ($callback && call_user_func($callback, $responses->last())) { $responses->setStopped(true); break; } } return $responses; } /** * Get list of all listeners attached to the shared event manager for * identifiers registered by this instance * * @param string $event * @return array */ protected function getSharedListeners($event) { if (!$sharedManager = $this->getSharedManager()) { return array(); } $identifiers = $this->getIdentifiers(); //Add wildcard id to the search, if not already added if (!in_array('*', $identifiers)) { $identifiers[] = '*'; } $sharedListeners = array(); foreach ($identifiers as $id) { if (!$listeners = $sharedManager->getListeners($id, $event)) { continue; } if (!is_array($listeners) && !($listeners instanceof Traversable)) { continue; } foreach ($listeners as $listener) { if (!$listener instanceof CallbackHandler) { continue; } $sharedListeners[] = $listener; } } return $sharedListeners; } /** * Add listeners to the master queue of listeners * * Used to inject shared listeners and wildcard listeners. * * @param PriorityQueue $masterListeners * @param PriorityQueue $listeners * @return void */ protected function insertListeners($masterListeners, $listeners) { foreach ($listeners as $listener) { $priority = $listener->getMetadatum('priority'); if (null === $priority) { $priority = 1; } elseif (is_array($priority)) { // If we have an array, likely using PriorityQueue. Grab first // element of the array, as that's the actual priority. $priority = array_shift($priority); } $masterListeners->insert($listener, $priority); } } }