EventManager.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 2.1.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Event;
  17. use InvalidArgumentException;
  18. /**
  19. * The event manager is responsible for keeping track of event listeners, passing the correct
  20. * data to them, and firing them in the correct order, when associated events are triggered. You
  21. * can create multiple instances of this object to manage local events or keep a single instance
  22. * and pass it around to manage all events in your app.
  23. */
  24. class EventManager implements EventManagerInterface
  25. {
  26. /**
  27. * The default priority queue value for new, attached listeners
  28. *
  29. * @var int
  30. */
  31. public static $defaultPriority = 10;
  32. /**
  33. * The globally available instance, used for dispatching events attached from any scope
  34. *
  35. * @var \Cake\Event\EventManager
  36. */
  37. protected static $_generalManager;
  38. /**
  39. * List of listener callbacks associated to
  40. *
  41. * @var array
  42. */
  43. protected $_listeners = [];
  44. /**
  45. * Internal flag to distinguish a common manager from the singleton
  46. *
  47. * @var bool
  48. */
  49. protected $_isGlobal = false;
  50. /**
  51. * The event list object.
  52. *
  53. * @var \Cake\Event\EventList|null
  54. */
  55. protected $_eventList;
  56. /**
  57. * Enables automatic adding of events to the event list object if it is present.
  58. *
  59. * @var bool
  60. */
  61. protected $_trackEvents = false;
  62. /**
  63. * Returns the globally available instance of a Cake\Event\EventManager
  64. * this is used for dispatching events attached from outside the scope
  65. * other managers were created. Usually for creating hook systems or inter-class
  66. * communication
  67. *
  68. * If called with the first parameter, it will be set as the globally available instance
  69. *
  70. * @param \Cake\Event\EventManager|null $manager Event manager instance.
  71. * @return static The global event manager
  72. */
  73. public static function instance(?EventManagerInterface $manager = null)
  74. {
  75. if ($manager instanceof EventManagerInterface) {
  76. static::$_generalManager = $manager;
  77. }
  78. if (empty(static::$_generalManager)) {
  79. static::$_generalManager = new static();
  80. }
  81. static::$_generalManager->_isGlobal = true;
  82. return static::$_generalManager;
  83. }
  84. /**
  85. * @inheritDoc
  86. */
  87. public function on($eventKey = null, $options = [], $callable = null)
  88. {
  89. if ($eventKey instanceof EventListenerInterface) {
  90. $this->_attachSubscriber($eventKey);
  91. return $this;
  92. }
  93. $argCount = func_num_args();
  94. if ($eventKey && $argCount === 2) {
  95. $this->_listeners[$eventKey][static::$defaultPriority][] = [
  96. 'callable' => $options,
  97. ];
  98. return $this;
  99. }
  100. if ($eventKey && $argCount === 3) {
  101. $priority = $options['priority'] ?? static::$defaultPriority;
  102. $this->_listeners[$eventKey][$priority][] = [
  103. 'callable' => $callable,
  104. ];
  105. return $this;
  106. }
  107. throw new InvalidArgumentException(
  108. 'Invalid arguments for EventManager::on(). ' .
  109. "Expected 1, 2 or 3 arguments. Got {$argCount} arguments."
  110. );
  111. }
  112. /**
  113. * Auxiliary function to attach all implemented callbacks of a Cake\Event\EventListenerInterface class instance
  114. * as individual methods on this manager
  115. *
  116. * @param \Cake\Event\EventListenerInterface $subscriber Event listener.
  117. * @return void
  118. */
  119. protected function _attachSubscriber(EventListenerInterface $subscriber): void
  120. {
  121. foreach ((array)$subscriber->implementedEvents() as $eventKey => $function) {
  122. $options = [];
  123. $method = $function;
  124. if (is_array($function) && isset($function['callable'])) {
  125. list($method, $options) = $this->_extractCallable($function, $subscriber);
  126. } elseif (is_array($function) && is_numeric(key($function))) {
  127. foreach ($function as $f) {
  128. list($method, $options) = $this->_extractCallable($f, $subscriber);
  129. $this->on($eventKey, $options, $method);
  130. }
  131. continue;
  132. }
  133. if (is_string($method)) {
  134. $method = [$subscriber, $function];
  135. }
  136. $this->on($eventKey, $options, $method);
  137. }
  138. }
  139. /**
  140. * Auxiliary function to extract and return a PHP callback type out of the callable definition
  141. * from the return value of the `implementedEvents` method on a Cake\Event\EventListenerInterface
  142. *
  143. * @param array $function the array taken from a handler definition for an event
  144. * @param \Cake\Event\EventListenerInterface $object The handler object
  145. * @return callable
  146. */
  147. protected function _extractCallable(array $function, EventListenerInterface $object)
  148. {
  149. $method = $function['callable'];
  150. $options = $function;
  151. unset($options['callable']);
  152. if (is_string($method)) {
  153. $method = [$object, $method];
  154. }
  155. /** @var callable $callable */
  156. $callable = [$method, $options];
  157. return $callable;
  158. }
  159. /**
  160. * @inheritDoc
  161. */
  162. public function off($eventKey, $callable = null)
  163. {
  164. if ($eventKey instanceof EventListenerInterface) {
  165. $this->_detachSubscriber($eventKey);
  166. return $this;
  167. }
  168. if ($callable instanceof EventListenerInterface) {
  169. $this->_detachSubscriber($callable, $eventKey);
  170. return $this;
  171. }
  172. if ($callable === null && is_string($eventKey)) {
  173. unset($this->_listeners[$eventKey]);
  174. return $this;
  175. }
  176. if ($callable === null) {
  177. foreach (array_keys($this->_listeners) as $name) {
  178. $this->off($name, $eventKey);
  179. }
  180. return $this;
  181. }
  182. if (empty($this->_listeners[$eventKey])) {
  183. return $this;
  184. }
  185. foreach ($this->_listeners[$eventKey] as $priority => $callables) {
  186. foreach ($callables as $k => $callback) {
  187. if ($callback['callable'] === $callable) {
  188. unset($this->_listeners[$eventKey][$priority][$k]);
  189. break;
  190. }
  191. }
  192. }
  193. return $this;
  194. }
  195. /**
  196. * Auxiliary function to help detach all listeners provided by an object implementing EventListenerInterface
  197. *
  198. * @param \Cake\Event\EventListenerInterface $subscriber the subscriber to be detached
  199. * @param string|null $eventKey optional event key name to unsubscribe the listener from
  200. * @return void
  201. */
  202. protected function _detachSubscriber(EventListenerInterface $subscriber, ?string $eventKey = null): void
  203. {
  204. $events = (array)$subscriber->implementedEvents();
  205. if (!empty($eventKey) && empty($events[$eventKey])) {
  206. return;
  207. }
  208. if (!empty($eventKey)) {
  209. $events = [$eventKey => $events[$eventKey]];
  210. }
  211. foreach ($events as $key => $function) {
  212. if (is_array($function)) {
  213. if (is_numeric(key($function))) {
  214. foreach ($function as $handler) {
  215. $handler = $handler['callable'] ?? $handler;
  216. $this->off($key, [$subscriber, $handler]);
  217. }
  218. continue;
  219. }
  220. $function = $function['callable'];
  221. }
  222. $this->off($key, [$subscriber, $function]);
  223. }
  224. }
  225. /**
  226. * @inheritDoc
  227. */
  228. public function dispatch($event): EventInterface
  229. {
  230. if (is_string($event)) {
  231. $event = new Event($event);
  232. }
  233. $listeners = $this->listeners($event->getName());
  234. if ($this->_trackEvents) {
  235. $this->addEventToList($event);
  236. }
  237. if (!$this->_isGlobal && static::instance()->isTrackingEvents()) {
  238. static::instance()->addEventToList($event);
  239. }
  240. if (empty($listeners)) {
  241. return $event;
  242. }
  243. foreach ($listeners as $listener) {
  244. if ($event->isStopped()) {
  245. break;
  246. }
  247. $result = $this->_callListener($listener['callable'], $event);
  248. if ($result === false) {
  249. $event->stopPropagation();
  250. }
  251. if ($result !== null) {
  252. $event->setResult($result);
  253. }
  254. }
  255. return $event;
  256. }
  257. /**
  258. * Calls a listener.
  259. *
  260. * @param callable $listener The listener to trigger.
  261. * @param \Cake\Event\EventInterface $event Event instance.
  262. * @return mixed The result of the $listener function.
  263. */
  264. protected function _callListener(callable $listener, EventInterface $event)
  265. {
  266. $data = $event->getData();
  267. return $listener($event, ...array_values($data));
  268. }
  269. /**
  270. * @inheritDoc
  271. */
  272. public function listeners(string $eventKey): array
  273. {
  274. $localListeners = [];
  275. if (!$this->_isGlobal) {
  276. $localListeners = $this->prioritisedListeners($eventKey);
  277. $localListeners = empty($localListeners) ? [] : $localListeners;
  278. }
  279. $globalListeners = static::instance()->prioritisedListeners($eventKey);
  280. $globalListeners = empty($globalListeners) ? [] : $globalListeners;
  281. $priorities = array_merge(array_keys($globalListeners), array_keys($localListeners));
  282. $priorities = array_unique($priorities);
  283. asort($priorities);
  284. $result = [];
  285. foreach ($priorities as $priority) {
  286. if (isset($globalListeners[$priority])) {
  287. $result = array_merge($result, $globalListeners[$priority]);
  288. }
  289. if (isset($localListeners[$priority])) {
  290. $result = array_merge($result, $localListeners[$priority]);
  291. }
  292. }
  293. return $result;
  294. }
  295. /**
  296. * Returns the listeners for the specified event key indexed by priority
  297. *
  298. * @param string $eventKey Event key.
  299. * @return array
  300. */
  301. public function prioritisedListeners($eventKey)
  302. {
  303. if (empty($this->_listeners[$eventKey])) {
  304. return [];
  305. }
  306. return $this->_listeners[$eventKey];
  307. }
  308. /**
  309. * Returns the listeners matching a specified pattern
  310. *
  311. * @param string $eventKeyPattern Pattern to match.
  312. * @return array
  313. */
  314. public function matchingListeners($eventKeyPattern)
  315. {
  316. $matchPattern = '/' . preg_quote($eventKeyPattern, '/') . '/';
  317. $matches = array_intersect_key(
  318. $this->_listeners,
  319. array_flip(
  320. preg_grep($matchPattern, array_keys($this->_listeners), 0)
  321. )
  322. );
  323. return $matches;
  324. }
  325. /**
  326. * Returns the event list.
  327. *
  328. * @return \Cake\Event\EventList|null
  329. */
  330. public function getEventList(): ?EventList
  331. {
  332. return $this->_eventList;
  333. }
  334. /**
  335. * Adds an event to the list if the event list object is present.
  336. *
  337. * @param \Cake\Event\EventInterface $event An event to add to the list.
  338. * @return $this
  339. */
  340. public function addEventToList(EventInterface $event)
  341. {
  342. if ($this->_eventList) {
  343. $this->_eventList->add($event);
  344. }
  345. return $this;
  346. }
  347. /**
  348. * Enables / disables event tracking at runtime.
  349. *
  350. * @param bool $enabled True or false to enable / disable it.
  351. * @return $this
  352. */
  353. public function trackEvents($enabled)
  354. {
  355. $this->_trackEvents = (bool)$enabled;
  356. return $this;
  357. }
  358. /**
  359. * Returns whether this manager is set up to track events
  360. *
  361. * @return bool
  362. */
  363. public function isTrackingEvents()
  364. {
  365. return $this->_trackEvents && $this->_eventList;
  366. }
  367. /**
  368. * Enables the listing of dispatched events.
  369. *
  370. * @param \Cake\Event\EventList $eventList The event list object to use.
  371. * @return $this
  372. */
  373. public function setEventList(EventList $eventList)
  374. {
  375. $this->_eventList = $eventList;
  376. $this->_trackEvents = true;
  377. return $this;
  378. }
  379. /**
  380. * Disables the listing of dispatched events.
  381. *
  382. * @return $this
  383. */
  384. public function unsetEventList()
  385. {
  386. $this->_eventList = null;
  387. $this->_trackEvents = false;
  388. return $this;
  389. }
  390. /**
  391. * Debug friendly object properties.
  392. *
  393. * @return array
  394. */
  395. public function __debugInfo()
  396. {
  397. $properties = get_object_vars($this);
  398. $properties['_generalManager'] = '(object) EventManager';
  399. $properties['_listeners'] = [];
  400. foreach ($this->_listeners as $key => $priorities) {
  401. $listenerCount = 0;
  402. foreach ($priorities as $listeners) {
  403. $listenerCount += count($listeners);
  404. }
  405. $properties['_listeners'][$key] = $listenerCount . ' listener(s)';
  406. }
  407. if ($this->_eventList) {
  408. $count = count($this->_eventList);
  409. for ($i = 0; $i < $count; $i++) {
  410. $event = $this->_eventList[$i];
  411. $subject = $event->getSubject();
  412. $properties['_dispatchedEvents'][] = $event->getName() . ' with ' .
  413. (is_object($subject) ? 'subject ' . get_class($subject) : 'no subject');
  414. }
  415. } else {
  416. $properties['_dispatchedEvents'] = null;
  417. }
  418. unset($properties['_eventList']);
  419. return $properties;
  420. }
  421. }