Merge pull request #19116 from eileenmcnaughton/pay_edit
[civicrm-core.git] / Civi / Core / CiviEventDispatcher.php
CommitLineData
762dc04d
TO
1<?php
2
3namespace Civi\Core;
4
3c006cb8 5use Symfony\Component\EventDispatcher\EventDispatcher;
762dc04d
TO
6use Symfony\Component\EventDispatcher\Event;
7
8/**
9 * Class CiviEventDispatcher
10 * @package Civi\Core
11 *
12 * The CiviEventDispatcher is a Symfony dispatcher. Additionally, if an event
13 * follows the naming convention of "hook_*", then it will also be dispatched
14 * through CRM_Utils_Hook::invoke().
15 *
16 * @see \CRM_Utils_Hook
17 */
3c006cb8 18class CiviEventDispatcher extends EventDispatcher {
762dc04d
TO
19
20 const DEFAULT_HOOK_PRIORITY = -100;
21
22 /**
23 * Track the list of hook-events for which we have autoregistered
24 * the hook adapter.
25 *
26 * @var array
27 * Array(string $eventName => trueish).
28 */
c64f69d9 29 private $autoListeners = [];
762dc04d 30
926d7afc 31 /**
7a4b686e
TO
32 * A list of dispatch-policies (based on an exact-match to the event name).
33 *
34 * Note: $dispatchPolicyExact and $dispatchPolicyRegex should coexist; e.g.
35 * if one is NULL, then both are NULL. If one is an array, then both are arrays.
36 *
926d7afc
TO
37 * @var array|null
38 * Array(string $eventName => string $action)
39 */
40 private $dispatchPolicyExact = NULL;
41
42 /**
7a4b686e
TO
43 * A list of dispatch-policies (based on an regex-match to the event name).
44 *
45 * Note: $dispatchPolicyExact and $dispatchPolicyRegex should coexist; e.g.
46 * if one is NULL, then both are NULL. If one is an array, then both are arrays.
47 *
926d7afc
TO
48 * @var array|null
49 * Array(string $eventRegex => string $action)
50 */
51 private $dispatchPolicyRegex = NULL;
52
762dc04d
TO
53 /**
54 * Determine whether $eventName should delegate to the CMS hook system.
55 *
56 * @param string $eventName
57 * Ex: 'civi.token.eval', 'hook_civicrm_post`.
58 * @return bool
59 */
60 protected function isHookEvent($eventName) {
61 return (substr($eventName, 0, 5) === 'hook_') && (strpos($eventName, '::') === FALSE);
62 }
63
b4483703
TO
64 /**
65 * Adds a service as event listener.
66 *
67 * This provides partial backwards compatibility with ContainerAwareEventDispatcher.
68 *
69 * @param string $eventName Event for which the listener is added
70 * @param array $callback The service ID of the listener service & the method
71 * name that has to be called
72 * @param int $priority The higher this value, the earlier an event listener
73 * will be triggered in the chain.
74 * Defaults to 0.
75 *
76 * @throws \InvalidArgumentException
77 */
78 public function addListenerService($eventName, $callback, $priority = 0) {
79 if (!\is_array($callback) || 2 !== \count($callback)) {
80 throw new \InvalidArgumentException('Expected an array("service", "method") argument');
81 }
82
83 $this->addListener($eventName, function($event) use ($callback) {
84 static $svc;
85 if ($svc === NULL) {
86 $svc = \Civi::container()->get($callback[0]);
87 }
88 return call_user_func([$svc, $callback[1]], $event);
89 }, $priority);
90 }
91
762dc04d
TO
92 /**
93 * @inheritDoc
94 */
95 public function dispatch($eventName, Event $event = NULL) {
926d7afc
TO
96 // Dispatch policies add systemic overhead and (normally) should not be evaluated. JNZ.
97 if ($this->dispatchPolicyRegex !== NULL) {
98 switch ($mode = $this->checkDispatchPolicy($eventName)) {
99 case 'run':
100 // Continue on the normal execution.
101 break;
102
103 case 'drop':
104 // Quietly ignore the event.
105 return $event;
106
107 case 'warn':
108 // Run the event, but complain about it.
109 error_log("Unexpectedly dispatching event \"$eventName\".");
110 break;
111
112 case 'warn-drop':
113 // Ignore the event, but complaint about it.
114 error_log("Unexpectedly dispatching event \"$eventName\".");
115 return $event;
116
117 case 'fail':
118 throw new \RuntimeException("The dispatch policy prohibits event \"$eventName\".");
119
42ccedc7
TO
120 case 'not-ready':
121 throw new \RuntimeException("CiviCRM has not bootstrapped sufficiently to fire event \"$eventName\".");
122
926d7afc
TO
123 default:
124 throw new \RuntimeException("The dispatch policy for \"$eventName\" is unrecognized ($mode).");
125
126 }
127 }
762dc04d
TO
128 $this->bindPatterns($eventName);
129 return parent::dispatch($eventName, $event);
130 }
131
132 /**
133 * @inheritDoc
134 */
135 public function getListeners($eventName = NULL) {
136 $this->bindPatterns($eventName);
137 return parent::getListeners($eventName);
138 }
139
140 /**
141 * @inheritDoc
142 */
143 public function hasListeners($eventName = NULL) {
144 // All hook_* events have default listeners, so hasListeners(NULL) is a truism.
145 return ($eventName === NULL || $this->isHookEvent($eventName))
146 ? TRUE : parent::hasListeners($eventName);
147 }
148
149 /**
150 * Invoke hooks using an event object.
151 *
152 * @param \Civi\Core\Event\GenericHookEvent $event
153 * @param string $eventName
154 * Ex: 'hook_civicrm_dashboard'.
155 */
156 public static function delegateToUF($event, $eventName) {
157 $hookName = substr($eventName, 5);
158 $hooks = \CRM_Utils_Hook::singleton();
159 $params = $event->getHookValues();
160 $count = count($params);
161
162 switch ($count) {
163 case 0:
164 $fResult = $hooks->invokeViaUF($count, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
165 break;
166
167 case 1:
168 $fResult = $hooks->invokeViaUF($count, $params[0], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
169 break;
170
171 case 2:
172 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
173 break;
174
175 case 3:
176 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
177 break;
178
179 case 4:
180 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
181 break;
182
183 case 5:
184 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], $params[4], \CRM_Utils_Hook::$_nullObject, $hookName);
185 break;
186
187 case 6:
188 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $hookName);
189 break;
190
191 default:
192 throw new \RuntimeException("hook_{$hookName} cannot support more than 6 parameters");
193 }
194
195 $event->addReturnValues($fResult);
196 }
197
198 /**
7a4b686e
TO
199 * Attach any pattern-based listeners which may be interested in $eventName.
200 *
762dc04d
TO
201 * @param string $eventName
202 * Ex: 'civi.api.resolve' or 'hook_civicrm_dashboard'.
203 */
204 protected function bindPatterns($eventName) {
205 if ($eventName !== NULL && !isset($this->autoListeners[$eventName])) {
206 $this->autoListeners[$eventName] = 1;
207 if ($this->isHookEvent($eventName)) {
208 // WISHLIST: For native extensions (and possibly D6/D7/D8/BD), enumerate
209 // the listeners and list them one-by-one. This would make it easier to
210 // inspect via "cv debug:event-dispatcher".
c64f69d9 211 $this->addListener($eventName, [
762dc04d
TO
212 '\Civi\Core\CiviEventDispatcher',
213 'delegateToUF',
c64f69d9 214 ], self::DEFAULT_HOOK_PRIORITY);
762dc04d
TO
215 }
216 }
217 }
218
926d7afc 219 /**
7a4b686e 220 * Set the dispatch policy. This allows you to filter certain events.
926d7afc
TO
221 * This can be useful during upgrades or debugging.
222 *
223 * Enforcement will add systemic overhead, so this should normally be NULL.
224 *
225 * @param array|null $dispatchPolicy
226 * Each key is either the string-literal name of an event, or a regex delimited by '/'.
227 * Each value is one of: 'run', 'drop', 'warn', 'fail'.
228 * Exact name matches take precedence over regexes. Regexes are evaluated in order.
229 *
230 * Ex: ['hook_civicrm_pre' => 'fail']
231 * Ex: ['/^hook_/' => 'warn']
232 *
233 * @return static
234 */
235 public function setDispatchPolicy($dispatchPolicy) {
236 if (is_array($dispatchPolicy)) {
237 // Split $dispatchPolicy in two (exact rules vs regex rules).
238 $this->dispatchPolicyExact = [];
239 $this->dispatchPolicyRegex = [];
240 foreach ($dispatchPolicy as $pattern => $action) {
241 if ($pattern[0] === '/') {
242 $this->dispatchPolicyRegex[$pattern] = $action;
243 }
244 else {
245 $this->dispatchPolicyExact[$pattern] = $action;
246 }
247 }
248 }
249 else {
250 $this->dispatchPolicyExact = NULL;
251 $this->dispatchPolicyRegex = NULL;
252 }
253
254 return $this;
255 }
256
257 // /**
258 // * @return array|NULL
259 // */
260 // public function getDispatchPolicy() {
261 // return $this->dispatchPolicyRegex === NULL ? NULL : array_merge($this->dispatchPolicyExact, $this->dispatchPolicyRegex);
262 // }
263
264 /**
7a4b686e
TO
265 * Determine whether the dispatch policy applies to a given event.
266 *
926d7afc 267 * @param string $eventName
7a4b686e 268 * Ex: 'civi.api.resolve' or 'hook_civicrm_dashboard'.
926d7afc
TO
269 * @return string
270 * Ex: 'run', 'drop', 'fail'
271 */
272 protected function checkDispatchPolicy($eventName) {
273 if (isset($this->dispatchPolicyExact[$eventName])) {
274 return $this->dispatchPolicyExact[$eventName];
275 }
276 foreach ($this->dispatchPolicyRegex as $eventPat => $action) {
277 if ($eventPat[0] === '/' && preg_match($eventPat, $eventName)) {
278 return $action;
279 }
280 }
281 return 'fail';
282 }
283
762dc04d 284}