Merge pull request #20229 from pradpnayak/usercreate
[civicrm-core.git] / Civi / Core / CiviEventDispatcher.php
1 <?php
2
3 namespace Civi\Core;
4
5 use Symfony\Component\EventDispatcher\EventDispatcher;
6 use 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 */
18 class CiviEventDispatcher extends EventDispatcher {
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 */
29 private $autoListeners = [];
30
31 /**
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 *
37 * @var array|null
38 * Array(string $eventName => string $action)
39 */
40 private $dispatchPolicyExact = NULL;
41
42 /**
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 *
48 * @var array|null
49 * Array(string $eventRegex => string $action)
50 */
51 private $dispatchPolicyRegex = NULL;
52
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
64 /**
65 * Adds a series of event listeners from a subscriber object.
66 *
67 * This is particularly useful if you want to register the subscriber without
68 * materializing the subscriber object.
69 *
70 * @param string $subscriber
71 * Service ID of the subscriber.
72 * @param array $events
73 * List of events/methods/priorities.
74 * @see \Symfony\Component\EventDispatcher\EventSubscriberInterface::getSubscribedEvents()
75 */
76 public function addSubscriberServiceMap(string $subscriber, array $events) {
77 foreach ($events as $eventName => $params) {
78 if (\is_string($params)) {
79 $this->addListenerService($eventName, [$subscriber, $params]);
80 }
81 elseif (\is_string($params[0])) {
82 $this->addListenerService($eventName, [$subscriber, $params[0]], isset($params[1]) ? $params[1] : 0);
83 }
84 else {
85 foreach ($params as $listener) {
86 $this->addListenerService($eventName, [$subscriber, $listener[0]], isset($listener[1]) ? $listener[1] : 0);
87 }
88 }
89 }
90 }
91
92 /**
93 * Adds a service as event listener.
94 *
95 * This provides partial backwards compatibility with ContainerAwareEventDispatcher.
96 *
97 * @param string $eventName Event for which the listener is added
98 * @param array $callback The service ID of the listener service & the method
99 * name that has to be called
100 * @param int $priority The higher this value, the earlier an event listener
101 * will be triggered in the chain.
102 * Defaults to 0.
103 *
104 * @throws \InvalidArgumentException
105 */
106 public function addListenerService($eventName, $callback, $priority = 0) {
107 if (!\is_array($callback) || 2 !== \count($callback)) {
108 throw new \InvalidArgumentException('Expected an array("service", "method") argument');
109 }
110
111 $this->addListener($eventName, new \Civi\Core\Event\ServiceListener($callback), $priority);
112 }
113
114 /**
115 * @inheritDoc
116 */
117 public function dispatch($eventName, Event $event = NULL) {
118 // Dispatch policies add systemic overhead and (normally) should not be evaluated. JNZ.
119 if ($this->dispatchPolicyRegex !== NULL) {
120 switch ($mode = $this->checkDispatchPolicy($eventName)) {
121 case 'run':
122 // Continue on the normal execution.
123 break;
124
125 case 'drop':
126 // Quietly ignore the event.
127 return $event;
128
129 case 'warn':
130 // Run the event, but complain about it.
131 error_log("Unexpectedly dispatching event \"$eventName\".");
132 break;
133
134 case 'warn-drop':
135 // Ignore the event, but complaint about it.
136 error_log("Unexpectedly dispatching event \"$eventName\".");
137 return $event;
138
139 case 'fail':
140 throw new \RuntimeException("The dispatch policy prohibits event \"$eventName\".");
141
142 case 'not-ready':
143 throw new \RuntimeException("CiviCRM has not bootstrapped sufficiently to fire event \"$eventName\".");
144
145 default:
146 throw new \RuntimeException("The dispatch policy for \"$eventName\" is unrecognized ($mode).");
147
148 }
149 }
150 $this->bindPatterns($eventName);
151 return parent::dispatch($eventName, $event);
152 }
153
154 /**
155 * @inheritDoc
156 */
157 public function getListeners($eventName = NULL) {
158 $this->bindPatterns($eventName);
159 return parent::getListeners($eventName);
160 }
161
162 /**
163 * @inheritDoc
164 */
165 public function hasListeners($eventName = NULL) {
166 // All hook_* events have default listeners, so hasListeners(NULL) is a truism.
167 return ($eventName === NULL || $this->isHookEvent($eventName))
168 ? TRUE : parent::hasListeners($eventName);
169 }
170
171 /**
172 * Invoke hooks using an event object.
173 *
174 * @param \Civi\Core\Event\GenericHookEvent $event
175 * @param string $eventName
176 * Ex: 'hook_civicrm_dashboard'.
177 */
178 public static function delegateToUF($event, $eventName) {
179 $hookName = substr($eventName, 5);
180 $hooks = \CRM_Utils_Hook::singleton();
181 $params = $event->getHookValues();
182 $count = count($params);
183
184 switch ($count) {
185 case 0:
186 $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);
187 break;
188
189 case 1:
190 $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);
191 break;
192
193 case 2:
194 $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);
195 break;
196
197 case 3:
198 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
199 break;
200
201 case 4:
202 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
203 break;
204
205 case 5:
206 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], $params[4], \CRM_Utils_Hook::$_nullObject, $hookName);
207 break;
208
209 case 6:
210 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $hookName);
211 break;
212
213 default:
214 throw new \RuntimeException("hook_{$hookName} cannot support more than 6 parameters");
215 }
216
217 $event->addReturnValues($fResult);
218 }
219
220 /**
221 * Attach any pattern-based listeners which may be interested in $eventName.
222 *
223 * @param string $eventName
224 * Ex: 'civi.api.resolve' or 'hook_civicrm_dashboard'.
225 */
226 protected function bindPatterns($eventName) {
227 if ($eventName !== NULL && !isset($this->autoListeners[$eventName])) {
228 $this->autoListeners[$eventName] = 1;
229 if ($this->isHookEvent($eventName)) {
230 // WISHLIST: For native extensions (and possibly D6/D7/D8/BD), enumerate
231 // the listeners and list them one-by-one. This would make it easier to
232 // inspect via "cv debug:event-dispatcher".
233 $this->addListener($eventName, [
234 '\Civi\Core\CiviEventDispatcher',
235 'delegateToUF',
236 ], self::DEFAULT_HOOK_PRIORITY);
237 }
238 }
239 }
240
241 /**
242 * Set the dispatch policy. This allows you to filter certain events.
243 * This can be useful during upgrades or debugging.
244 *
245 * Enforcement will add systemic overhead, so this should normally be NULL.
246 *
247 * @param array|null $dispatchPolicy
248 * Each key is either the string-literal name of an event, or a regex delimited by '/'.
249 * Each value is one of: 'run', 'drop', 'warn', 'fail'.
250 * Exact name matches take precedence over regexes. Regexes are evaluated in order.
251 *
252 * Ex: ['hook_civicrm_pre' => 'fail']
253 * Ex: ['/^hook_/' => 'warn']
254 *
255 * @return static
256 */
257 public function setDispatchPolicy($dispatchPolicy) {
258 if (is_array($dispatchPolicy)) {
259 // Split $dispatchPolicy in two (exact rules vs regex rules).
260 $this->dispatchPolicyExact = [];
261 $this->dispatchPolicyRegex = [];
262 foreach ($dispatchPolicy as $pattern => $action) {
263 if ($pattern[0] === '/') {
264 $this->dispatchPolicyRegex[$pattern] = $action;
265 }
266 else {
267 $this->dispatchPolicyExact[$pattern] = $action;
268 }
269 }
270 }
271 else {
272 $this->dispatchPolicyExact = NULL;
273 $this->dispatchPolicyRegex = NULL;
274 }
275
276 return $this;
277 }
278
279 // /**
280 // * @return array|NULL
281 // */
282 // public function getDispatchPolicy() {
283 // return $this->dispatchPolicyRegex === NULL ? NULL : array_merge($this->dispatchPolicyExact, $this->dispatchPolicyRegex);
284 // }
285
286 /**
287 * Determine whether the dispatch policy applies to a given event.
288 *
289 * @param string $eventName
290 * Ex: 'civi.api.resolve' or 'hook_civicrm_dashboard'.
291 * @return string
292 * Ex: 'run', 'drop', 'fail'
293 */
294 protected function checkDispatchPolicy($eventName) {
295 if (isset($this->dispatchPolicyExact[$eventName])) {
296 return $this->dispatchPolicyExact[$eventName];
297 }
298 foreach ($this->dispatchPolicyRegex as $eventPat => $action) {
299 if ($eventPat[0] === '/' && preg_match($eventPat, $eventName)) {
300 return $action;
301 }
302 }
303 return 'fail';
304 }
305
306 }