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