EventManager.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 2.1.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Event;
  16. use InvalidArgumentException;
  17. /**
  18. * The event manager is responsible for keeping track of event listeners, passing the correct
  19. * data to them, and firing them in the correct order, when associated events are triggered. You
  20. * can create multiple instances of this object to manage local events or keep a single instance
  21. * and pass it around to manage all events in your app.
  22. */
  23. class EventManager
  24. {
  25. /**
  26. * The default priority queue value for new, attached listeners
  27. *
  28. * @var int
  29. */
  30. public static $defaultPriority = 10;
  31. /**
  32. * The globally available instance, used for dispatching events attached from any scope
  33. *
  34. * @var \Cake\Event\EventManager
  35. */
  36. protected static $_generalManager;
  37. /**
  38. * List of listener callbacks associated to
  39. *
  40. * @var array
  41. */
  42. protected $_listeners = [];
  43. /**
  44. * Internal flag to distinguish a common manager from the singleton
  45. *
  46. * @var bool
  47. */
  48. protected $_isGlobal = false;
  49. /**
  50. * The event list object.
  51. *
  52. * @var \Cake\Event\EventList|null
  53. */
  54. protected $_eventList;
  55. /**
  56. * Enables automatic adding of events to the event list object if it is present.
  57. *
  58. * @var bool
  59. */
  60. protected $_trackEvents = false;
  61. /**
  62. * Returns the globally available instance of a Cake\Event\EventManager
  63. * this is used for dispatching events attached from outside the scope
  64. * other managers were created. Usually for creating hook systems or inter-class
  65. * communication
  66. *
  67. * If called with the first parameter, it will be set as the globally available instance
  68. *
  69. * @param \Cake\Event\EventManager|null $manager Event manager instance.
  70. * @return static The global event manager
  71. */
  72. public static function instance($manager = null)
  73. {
  74. if ($manager instanceof EventManager) {
  75. static::$_generalManager = $manager;
  76. }
  77. if (empty(static::$_generalManager)) {
  78. static::$_generalManager = new static();
  79. }
  80. static::$_generalManager->_isGlobal = true;
  81. return static::$_generalManager;
  82. }
  83. /**
  84. * Adds a new listener to an event.
  85. *
  86. * @param callable|\Cake\Event\EventListenerInterface $callable PHP valid callback type or instance of Cake\Event\EventListenerInterface to be called
  87. * when the event named with $eventKey is triggered. If a Cake\Event\EventListenerInterface instance is passed, then the `implementedEvents`
  88. * method will be called on the object to register the declared events individually as methods to be managed by this class.
  89. * It is possible to define multiple event handlers per event name.
  90. *
  91. * @param string|null $eventKey The event unique identifier name with which the callback will be associated. If $callable
  92. * is an instance of Cake\Event\EventListenerInterface this argument will be ignored
  93. *
  94. * @param array $options used to set the `priority` flag to the listener. In the future more options may be added.
  95. * Priorities are treated as queues. Lower values are called before higher ones, and multiple attachments
  96. * added to the same priority queue will be treated in the order of insertion.
  97. *
  98. * @return void
  99. * @throws \InvalidArgumentException When event key is missing or callable is not an
  100. * instance of Cake\Event\EventListenerInterface.
  101. * @deprecated 3.0.0 Use on() instead.
  102. */
  103. public function attach($callable, $eventKey = null, array $options = [])
  104. {
  105. if ($eventKey === null) {
  106. $this->on($callable);
  107. return;
  108. }
  109. if ($options) {
  110. $this->on($eventKey, $options, $callable);
  111. return;
  112. }
  113. $this->on($eventKey, $callable);
  114. }
  115. /**
  116. * Adds a new listener to an event.
  117. *
  118. * A variadic interface to add listeners that emulates jQuery.on().
  119. *
  120. * Binding an EventListenerInterface:
  121. *
  122. * ```
  123. * $eventManager->on($listener);
  124. * ```
  125. *
  126. * Binding with no options:
  127. *
  128. * ```
  129. * $eventManager->on('Model.beforeSave', $callable);
  130. * ```
  131. *
  132. * Binding with options:
  133. *
  134. * ```
  135. * $eventManager->on('Model.beforeSave', ['priority' => 90], $callable);
  136. * ```
  137. *
  138. * @param string|\Cake\Event\EventListenerInterface|null $eventKey The event unique identifier name
  139. * with which the callback will be associated. If $eventKey is an instance of
  140. * Cake\Event\EventListenerInterface its events will be bound using the `implementedEvents` methods.
  141. *
  142. * @param array|callable $options Either an array of options or the callable you wish to
  143. * bind to $eventKey. If an array of options, the `priority` key can be used to define the order.
  144. * Priorities are treated as queues. Lower values are called before higher ones, and multiple attachments
  145. * added to the same priority queue will be treated in the order of insertion.
  146. *
  147. * @param callable|null $callable The callable function you want invoked.
  148. *
  149. * @return $this
  150. * @throws \InvalidArgumentException When event key is missing or callable is not an
  151. * instance of Cake\Event\EventListenerInterface.
  152. */
  153. public function on($eventKey = null, $options = [], $callable = null)
  154. {
  155. if ($eventKey instanceof EventListenerInterface) {
  156. $this->_attachSubscriber($eventKey);
  157. return $this;
  158. }
  159. $argCount = func_num_args();
  160. if ($argCount === 2) {
  161. $this->_listeners[$eventKey][static::$defaultPriority][] = [
  162. 'callable' => $options
  163. ];
  164. return $this;
  165. }
  166. if ($argCount === 3) {
  167. $priority = isset($options['priority']) ? $options['priority'] : static::$defaultPriority;
  168. $this->_listeners[$eventKey][$priority][] = [
  169. 'callable' => $callable
  170. ];
  171. return $this;
  172. }
  173. throw new InvalidArgumentException('Invalid arguments for EventManager::on().');
  174. }
  175. /**
  176. * Auxiliary function to attach all implemented callbacks of a Cake\Event\EventListenerInterface class instance
  177. * as individual methods on this manager
  178. *
  179. * @param \Cake\Event\EventListenerInterface $subscriber Event listener.
  180. * @return void
  181. */
  182. protected function _attachSubscriber(EventListenerInterface $subscriber)
  183. {
  184. foreach ((array)$subscriber->implementedEvents() as $eventKey => $function) {
  185. $options = [];
  186. $method = $function;
  187. if (is_array($function) && isset($function['callable'])) {
  188. list($method, $options) = $this->_extractCallable($function, $subscriber);
  189. } elseif (is_array($function) && is_numeric(key($function))) {
  190. foreach ($function as $f) {
  191. list($method, $options) = $this->_extractCallable($f, $subscriber);
  192. $this->on($eventKey, $options, $method);
  193. }
  194. continue;
  195. }
  196. if (is_string($method)) {
  197. $method = [$subscriber, $function];
  198. }
  199. $this->on($eventKey, $options, $method);
  200. }
  201. }
  202. /**
  203. * Auxiliary function to extract and return a PHP callback type out of the callable definition
  204. * from the return value of the `implementedEvents` method on a Cake\Event\EventListenerInterface
  205. *
  206. * @param array $function the array taken from a handler definition for an event
  207. * @param \Cake\Event\EventListenerInterface $object The handler object
  208. * @return callable
  209. */
  210. protected function _extractCallable($function, $object)
  211. {
  212. $method = $function['callable'];
  213. $options = $function;
  214. unset($options['callable']);
  215. if (is_string($method)) {
  216. $method = [$object, $method];
  217. }
  218. return [$method, $options];
  219. }
  220. /**
  221. * Removes a listener from the active listeners.
  222. *
  223. * @param callable|\Cake\Event\EventListenerInterface $callable any valid PHP callback type or an instance of EventListenerInterface
  224. * @param string|null $eventKey The event unique identifier name with which the callback has been associated
  225. * @return void
  226. * @deprecated 3.0.0 Use off() instead.
  227. */
  228. public function detach($callable, $eventKey = null)
  229. {
  230. if ($eventKey === null) {
  231. $this->off($callable);
  232. return;
  233. }
  234. $this->off($eventKey, $callable);
  235. }
  236. /**
  237. * Remove a listener from the active listeners.
  238. *
  239. * Remove a EventListenerInterface entirely:
  240. *
  241. * ```
  242. * $manager->off($listener);
  243. * ```
  244. *
  245. * Remove all listeners for a given event:
  246. *
  247. * ```
  248. * $manager->off('My.event');
  249. * ```
  250. *
  251. * Remove a specific listener:
  252. *
  253. * ```
  254. * $manager->off('My.event', $callback);
  255. * ```
  256. *
  257. * Remove a callback from all events:
  258. *
  259. * ```
  260. * $manager->off($callback);
  261. * ```
  262. *
  263. * @param string|\Cake\Event\EventListenerInterface $eventKey The event unique identifier name
  264. * with which the callback has been associated, or the $listener you want to remove.
  265. * @param callable|null $callable The callback you want to detach.
  266. * @return $this
  267. */
  268. public function off($eventKey, $callable = null)
  269. {
  270. if ($eventKey instanceof EventListenerInterface) {
  271. $this->_detachSubscriber($eventKey);
  272. return $this;
  273. }
  274. if ($callable instanceof EventListenerInterface) {
  275. $this->_detachSubscriber($callable, $eventKey);
  276. return $this;
  277. }
  278. if ($callable === null && is_string($eventKey)) {
  279. unset($this->_listeners[$eventKey]);
  280. return $this;
  281. }
  282. if ($callable === null) {
  283. foreach (array_keys($this->_listeners) as $name) {
  284. $this->off($name, $eventKey);
  285. }
  286. return $this;
  287. }
  288. if (empty($this->_listeners[$eventKey])) {
  289. return $this;
  290. }
  291. foreach ($this->_listeners[$eventKey] as $priority => $callables) {
  292. foreach ($callables as $k => $callback) {
  293. if ($callback['callable'] === $callable) {
  294. unset($this->_listeners[$eventKey][$priority][$k]);
  295. break;
  296. }
  297. }
  298. }
  299. return $this;
  300. }
  301. /**
  302. * Auxiliary function to help detach all listeners provided by an object implementing EventListenerInterface
  303. *
  304. * @param \Cake\Event\EventListenerInterface $subscriber the subscriber to be detached
  305. * @param string|null $eventKey optional event key name to unsubscribe the listener from
  306. * @return void
  307. */
  308. protected function _detachSubscriber(EventListenerInterface $subscriber, $eventKey = null)
  309. {
  310. $events = (array)$subscriber->implementedEvents();
  311. if (!empty($eventKey) && empty($events[$eventKey])) {
  312. return;
  313. }
  314. if (!empty($eventKey)) {
  315. $events = [$eventKey => $events[$eventKey]];
  316. }
  317. foreach ($events as $key => $function) {
  318. if (is_array($function)) {
  319. if (is_numeric(key($function))) {
  320. foreach ($function as $handler) {
  321. $handler = isset($handler['callable']) ? $handler['callable'] : $handler;
  322. $this->off($key, [$subscriber, $handler]);
  323. }
  324. continue;
  325. }
  326. $function = $function['callable'];
  327. }
  328. $this->off($key, [$subscriber, $function]);
  329. }
  330. }
  331. /**
  332. * Dispatches a new event to all configured listeners
  333. *
  334. * @param string|\Cake\Event\Event $event the event key name or instance of Event
  335. * @return \Cake\Event\Event
  336. * @triggers $event
  337. */
  338. public function dispatch($event)
  339. {
  340. if (is_string($event)) {
  341. $event = new Event($event);
  342. }
  343. $listeners = $this->listeners($event->getName());
  344. if ($this->_trackEvents) {
  345. $this->addEventToList($event);
  346. }
  347. if (!$this->_isGlobal && static::instance()->isTrackingEvents()) {
  348. static::instance()->addEventToList($event);
  349. }
  350. if (empty($listeners)) {
  351. return $event;
  352. }
  353. foreach ($listeners as $listener) {
  354. if ($event->isStopped()) {
  355. break;
  356. }
  357. $result = $this->_callListener($listener['callable'], $event);
  358. if ($result === false) {
  359. $event->stopPropagation();
  360. }
  361. if ($result !== null) {
  362. $event->setResult($result);
  363. }
  364. }
  365. return $event;
  366. }
  367. /**
  368. * Calls a listener.
  369. *
  370. * @param callable $listener The listener to trigger.
  371. * @param \Cake\Event\Event $event Event instance.
  372. * @return mixed The result of the $listener function.
  373. */
  374. protected function _callListener(callable $listener, Event $event)
  375. {
  376. $data = $event->getData();
  377. return $listener($event, ...array_values($data));
  378. }
  379. /**
  380. * Returns a list of all listeners for an eventKey in the order they should be called
  381. *
  382. * @param string $eventKey Event key.
  383. * @return array
  384. */
  385. public function listeners($eventKey)
  386. {
  387. $localListeners = [];
  388. if (!$this->_isGlobal) {
  389. $localListeners = $this->prioritisedListeners($eventKey);
  390. $localListeners = empty($localListeners) ? [] : $localListeners;
  391. }
  392. $globalListeners = static::instance()->prioritisedListeners($eventKey);
  393. $globalListeners = empty($globalListeners) ? [] : $globalListeners;
  394. $priorities = array_merge(array_keys($globalListeners), array_keys($localListeners));
  395. $priorities = array_unique($priorities);
  396. asort($priorities);
  397. $result = [];
  398. foreach ($priorities as $priority) {
  399. if (isset($globalListeners[$priority])) {
  400. $result = array_merge($result, $globalListeners[$priority]);
  401. }
  402. if (isset($localListeners[$priority])) {
  403. $result = array_merge($result, $localListeners[$priority]);
  404. }
  405. }
  406. return $result;
  407. }
  408. /**
  409. * Returns the listeners for the specified event key indexed by priority
  410. *
  411. * @param string $eventKey Event key.
  412. * @return array
  413. */
  414. public function prioritisedListeners($eventKey)
  415. {
  416. if (empty($this->_listeners[$eventKey])) {
  417. return [];
  418. }
  419. return $this->_listeners[$eventKey];
  420. }
  421. /**
  422. * Returns the listeners matching a specified pattern
  423. *
  424. * @param string $eventKeyPattern Pattern to match.
  425. * @return array
  426. */
  427. public function matchingListeners($eventKeyPattern)
  428. {
  429. $matchPattern = '/' . preg_quote($eventKeyPattern, '/') . '/';
  430. $matches = array_intersect_key(
  431. $this->_listeners,
  432. array_flip(
  433. preg_grep($matchPattern, array_keys($this->_listeners), 0)
  434. )
  435. );
  436. return $matches;
  437. }
  438. /**
  439. * Returns the event list.
  440. *
  441. * @return \Cake\Event\EventList
  442. */
  443. public function getEventList()
  444. {
  445. return $this->_eventList;
  446. }
  447. /**
  448. * Adds an event to the list if the event list object is present.
  449. *
  450. * @param \Cake\Event\Event $event An event to add to the list.
  451. * @return $this
  452. */
  453. public function addEventToList(Event $event)
  454. {
  455. if ($this->_eventList) {
  456. $this->_eventList->add($event);
  457. }
  458. return $this;
  459. }
  460. /**
  461. * Enables / disables event tracking at runtime.
  462. *
  463. * @param bool $enabled True or false to enable / disable it.
  464. * @return $this
  465. */
  466. public function trackEvents($enabled)
  467. {
  468. $this->_trackEvents = (bool)$enabled;
  469. return $this;
  470. }
  471. /**
  472. * Returns whether this manager is set up to track events
  473. *
  474. * @return bool
  475. */
  476. public function isTrackingEvents()
  477. {
  478. return $this->_trackEvents && $this->_eventList;
  479. }
  480. /**
  481. * Enables the listing of dispatched events.
  482. *
  483. * @param \Cake\Event\EventList $eventList The event list object to use.
  484. * @return $this
  485. */
  486. public function setEventList(EventList $eventList)
  487. {
  488. $this->_eventList = $eventList;
  489. $this->_trackEvents = true;
  490. return $this;
  491. }
  492. /**
  493. * Disables the listing of dispatched events.
  494. *
  495. * @return $this
  496. */
  497. public function unsetEventList()
  498. {
  499. $this->_eventList = null;
  500. $this->_trackEvents = false;
  501. return $this;
  502. }
  503. /**
  504. * Debug friendly object properties.
  505. *
  506. * @return array
  507. */
  508. public function __debugInfo()
  509. {
  510. $properties = get_object_vars($this);
  511. $properties['_generalManager'] = '(object) EventManager';
  512. $properties['_listeners'] = [];
  513. foreach ($this->_listeners as $key => $priorities) {
  514. $listenerCount = 0;
  515. foreach ($priorities as $listeners) {
  516. $listenerCount += count($listeners);
  517. }
  518. $properties['_listeners'][$key] = $listenerCount . ' listener(s)';
  519. }
  520. if ($this->_eventList) {
  521. foreach ($this->_eventList as $event) {
  522. $properties['_dispatchedEvents'][] = $event->getName() . ' with subject ' . get_class($event->getSubject());
  523. }
  524. }
  525. return $properties;
  526. }
  527. }