1: <?php
2: /**
3: *
4: *
5: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
6: * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
7: *
8: * Licensed under The MIT License
9: * For full copyright and license information, please see the LICENSE.txt
10: * Redistributions of files must retain the above copyright notice.
11: *
12: * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
13: * @link http://cakephp.org CakePHP(tm) Project
14: * @package Cake.Event
15: * @since CakePHP(tm) v 2.1
16: * @license http://www.opensource.org/licenses/mit-license.php MIT License
17: */
18:
19: App::uses('CakeEventListener', 'Event');
20:
21: /**
22: * The event manager is responsible for keeping track of event listeners, passing the correct
23: * data to them, and firing them in the correct order, when associated events are triggered. You
24: * can create multiple instances of this object to manage local events or keep a single instance
25: * and pass it around to manage all events in your app.
26: *
27: * @package Cake.Event
28: */
29: class CakeEventManager {
30:
31: /**
32: * The default priority queue value for new, attached listeners
33: *
34: * @var integer
35: */
36: public static $defaultPriority = 10;
37:
38: /**
39: * The globally available instance, used for dispatching events attached from any scope
40: *
41: * @var CakeEventManager
42: */
43: protected static $_generalManager = null;
44:
45: /**
46: * List of listener callbacks associated to
47: *
48: * @var object $Listeners
49: */
50: protected $_listeners = array();
51:
52: /**
53: * Internal flag to distinguish a common manager from the singleton
54: *
55: * @var boolean
56: */
57: protected $_isGlobal = false;
58:
59: /**
60: * Returns the globally available instance of a CakeEventManager
61: * this is used for dispatching events attached from outside the scope
62: * other managers were created. Usually for creating hook systems or inter-class
63: * communication
64: *
65: * If called with the first parameter, it will be set as the globally available instance
66: *
67: * @param CakeEventManager $manager
68: * @return CakeEventManager the global event manager
69: */
70: public static function instance($manager = null) {
71: if ($manager instanceof CakeEventManager) {
72: self::$_generalManager = $manager;
73: }
74: if (empty(self::$_generalManager)) {
75: self::$_generalManager = new CakeEventManager();
76: }
77:
78: self::$_generalManager->_isGlobal = true;
79: return self::$_generalManager;
80: }
81:
82: /**
83: * Adds a new listener to an event. Listeners
84: *
85: * @param callback|CakeEventListener $callable PHP valid callback type or instance of CakeEventListener to be called
86: * when the event named with $eventKey is triggered. If a CakeEventListener instance is passed, then the `implementedEvents`
87: * method will be called on the object to register the declared events individually as methods to be managed by this class.
88: * It is possible to define multiple event handlers per event name.
89: *
90: * @param string $eventKey The event unique identifier name with which the callback will be associated. If $callable
91: * is an instance of CakeEventListener this argument will be ignored
92: *
93: * @param array $options used to set the `priority` and `passParams` flags to the listener.
94: * Priorities are handled like queues, and multiple attachments added to the same priority queue will be treated in
95: * the order of insertion. `passParams` means that the event data property will be converted to function arguments
96: * when the listener is called. If $called is an instance of CakeEventListener, this parameter will be ignored
97: *
98: * @return void
99: * @throws InvalidArgumentException When event key is missing or callable is not an
100: * instance of CakeEventListener.
101: */
102: public function attach($callable, $eventKey = null, $options = array()) {
103: if (!$eventKey && !($callable instanceof CakeEventListener)) {
104: throw new InvalidArgumentException(__d('cake_dev', 'The eventKey variable is required'));
105: }
106: if ($callable instanceof CakeEventListener) {
107: $this->_attachSubscriber($callable);
108: return;
109: }
110: $options = $options + array('priority' => self::$defaultPriority, 'passParams' => false);
111: $this->_listeners[$eventKey][$options['priority']][] = array(
112: 'callable' => $callable,
113: 'passParams' => $options['passParams'],
114: );
115: }
116:
117: /**
118: * Auxiliary function to attach all implemented callbacks of a CakeEventListener class instance
119: * as individual methods on this manager
120: *
121: * @param CakeEventListener $subscriber
122: * @return void
123: */
124: protected function _attachSubscriber(CakeEventListener $subscriber) {
125: foreach ((array)$subscriber->implementedEvents() as $eventKey => $function) {
126: $options = array();
127: $method = $function;
128: if (is_array($function) && isset($function['callable'])) {
129: list($method, $options) = $this->_extractCallable($function, $subscriber);
130: } elseif (is_array($function) && is_numeric(key($function))) {
131: foreach ($function as $f) {
132: list($method, $options) = $this->_extractCallable($f, $subscriber);
133: $this->attach($method, $eventKey, $options);
134: }
135: continue;
136: }
137: if (is_string($method)) {
138: $method = array($subscriber, $function);
139: }
140: $this->attach($method, $eventKey, $options);
141: }
142: }
143:
144: /**
145: * Auxiliary function to extract and return a PHP callback type out of the callable definition
146: * from the return value of the `implementedEvents` method on a CakeEventListener
147: *
148: * @param array $function the array taken from a handler definition for an event
149: * @param CakeEventListener $object The handler object
150: * @return callback
151: */
152: protected function _extractCallable($function, $object) {
153: $method = $function['callable'];
154: $options = $function;
155: unset($options['callable']);
156: if (is_string($method)) {
157: $method = array($object, $method);
158: }
159: return array($method, $options);
160: }
161:
162: /**
163: * Removes a listener from the active listeners.
164: *
165: * @param callback|CakeEventListener $callable any valid PHP callback type or an instance of CakeEventListener
166: * @param string $eventKey The event unique identifier name with which the callback has been associated
167: * @return void
168: */
169: public function detach($callable, $eventKey = null) {
170: if ($callable instanceof CakeEventListener) {
171: return $this->_detachSubscriber($callable, $eventKey);
172: }
173: if (empty($eventKey)) {
174: foreach (array_keys($this->_listeners) as $eventKey) {
175: $this->detach($callable, $eventKey);
176: }
177: return;
178: }
179: if (empty($this->_listeners[$eventKey])) {
180: return;
181: }
182: foreach ($this->_listeners[$eventKey] as $priority => $callables) {
183: foreach ($callables as $k => $callback) {
184: if ($callback['callable'] === $callable) {
185: unset($this->_listeners[$eventKey][$priority][$k]);
186: break;
187: }
188: }
189: }
190: }
191:
192: /**
193: * Auxiliary function to help detach all listeners provided by an object implementing CakeEventListener
194: *
195: * @param CakeEventListener $subscriber the subscriber to be detached
196: * @param string $eventKey optional event key name to unsubscribe the listener from
197: * @return void
198: */
199: protected function _detachSubscriber(CakeEventListener $subscriber, $eventKey = null) {
200: $events = (array)$subscriber->implementedEvents();
201: if (!empty($eventKey) && empty($events[$eventKey])) {
202: return;
203: } elseif (!empty($eventKey)) {
204: $events = array($eventKey => $events[$eventKey]);
205: }
206: foreach ($events as $key => $function) {
207: if (is_array($function)) {
208: if (is_numeric(key($function))) {
209: foreach ($function as $handler) {
210: $handler = isset($handler['callable']) ? $handler['callable'] : $handler;
211: $this->detach(array($subscriber, $handler), $key);
212: }
213: continue;
214: }
215: $function = $function['callable'];
216: }
217: $this->detach(array($subscriber, $function), $key);
218: }
219: }
220:
221: /**
222: * Dispatches a new event to all configured listeners
223: *
224: * @param string|CakeEvent $event the event key name or instance of CakeEvent
225: * @return void
226: */
227: public function dispatch($event) {
228: if (is_string($event)) {
229: $event = new CakeEvent($event);
230: }
231:
232: if (!$this->_isGlobal) {
233: self::instance()->dispatch($event);
234: }
235:
236: if (empty($this->_listeners[$event->name()])) {
237: return;
238: }
239:
240: foreach ($this->listeners($event->name()) as $listener) {
241: if ($event->isStopped()) {
242: break;
243: }
244: if ($listener['passParams'] === true) {
245: $result = call_user_func_array($listener['callable'], $event->data);
246: } else {
247: $result = call_user_func($listener['callable'], $event);
248: }
249: if ($result === false) {
250: $event->stopPropagation();
251: }
252: if ($result !== null) {
253: $event->result = $result;
254: }
255: }
256: }
257:
258: /**
259: * Returns a list of all listeners for an eventKey in the order they should be called
260: *
261: * @param string $eventKey
262: * @return array
263: */
264: public function listeners($eventKey) {
265: if (empty($this->_listeners[$eventKey])) {
266: return array();
267: }
268: ksort($this->_listeners[$eventKey]);
269: $result = array();
270: foreach ($this->_listeners[$eventKey] as $priorityQ) {
271: $result = array_merge($result, $priorityQ);
272: }
273: return $result;
274: }
275:
276: }
277: