Merge pull request #17217 from civicrm/5.25
[civicrm-core.git] / Civi / Core / CiviEventDispatcher.php
1 <?php
2
3 namespace Civi\Core;
4
5 use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher;
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 ContainerAwareEventDispatcher {
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 * @var array|null
33 * Array(string $eventName => string $action)
34 */
35 private $dispatchPolicyExact = NULL;
36
37 /**
38 * @var array|null
39 * Array(string $eventRegex => string $action)
40 */
41 private $dispatchPolicyRegex = NULL;
42
43 /**
44 * Determine whether $eventName should delegate to the CMS hook system.
45 *
46 * @param string $eventName
47 * Ex: 'civi.token.eval', 'hook_civicrm_post`.
48 * @return bool
49 */
50 protected function isHookEvent($eventName) {
51 return (substr($eventName, 0, 5) === 'hook_') && (strpos($eventName, '::') === FALSE);
52 }
53
54 /**
55 * @inheritDoc
56 */
57 public function dispatch($eventName, Event $event = NULL) {
58 // Dispatch policies add systemic overhead and (normally) should not be evaluated. JNZ.
59 if ($this->dispatchPolicyRegex !== NULL) {
60 switch ($mode = $this->checkDispatchPolicy($eventName)) {
61 case 'run':
62 // Continue on the normal execution.
63 break;
64
65 case 'drop':
66 // Quietly ignore the event.
67 return $event;
68
69 case 'warn':
70 // Run the event, but complain about it.
71 error_log("Unexpectedly dispatching event \"$eventName\".");
72 break;
73
74 case 'warn-drop':
75 // Ignore the event, but complaint about it.
76 error_log("Unexpectedly dispatching event \"$eventName\".");
77 return $event;
78
79 case 'fail':
80 throw new \RuntimeException("The dispatch policy prohibits event \"$eventName\".");
81
82 default:
83 throw new \RuntimeException("The dispatch policy for \"$eventName\" is unrecognized ($mode).");
84
85 }
86 }
87 $this->bindPatterns($eventName);
88 return parent::dispatch($eventName, $event);
89 }
90
91 /**
92 * @inheritDoc
93 */
94 public function getListeners($eventName = NULL) {
95 $this->bindPatterns($eventName);
96 return parent::getListeners($eventName);
97 }
98
99 /**
100 * @inheritDoc
101 */
102 public function hasListeners($eventName = NULL) {
103 // All hook_* events have default listeners, so hasListeners(NULL) is a truism.
104 return ($eventName === NULL || $this->isHookEvent($eventName))
105 ? TRUE : parent::hasListeners($eventName);
106 }
107
108 /**
109 * Invoke hooks using an event object.
110 *
111 * @param \Civi\Core\Event\GenericHookEvent $event
112 * @param string $eventName
113 * Ex: 'hook_civicrm_dashboard'.
114 */
115 public static function delegateToUF($event, $eventName) {
116 $hookName = substr($eventName, 5);
117 $hooks = \CRM_Utils_Hook::singleton();
118 $params = $event->getHookValues();
119 $count = count($params);
120
121 switch ($count) {
122 case 0:
123 $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);
124 break;
125
126 case 1:
127 $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);
128 break;
129
130 case 2:
131 $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);
132 break;
133
134 case 3:
135 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
136 break;
137
138 case 4:
139 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
140 break;
141
142 case 5:
143 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], $params[4], \CRM_Utils_Hook::$_nullObject, $hookName);
144 break;
145
146 case 6:
147 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $hookName);
148 break;
149
150 default:
151 throw new \RuntimeException("hook_{$hookName} cannot support more than 6 parameters");
152 }
153
154 $event->addReturnValues($fResult);
155 }
156
157 /**
158 * @param string $eventName
159 * Ex: 'civi.api.resolve' or 'hook_civicrm_dashboard'.
160 */
161 protected function bindPatterns($eventName) {
162 if ($eventName !== NULL && !isset($this->autoListeners[$eventName])) {
163 $this->autoListeners[$eventName] = 1;
164 if ($this->isHookEvent($eventName)) {
165 // WISHLIST: For native extensions (and possibly D6/D7/D8/BD), enumerate
166 // the listeners and list them one-by-one. This would make it easier to
167 // inspect via "cv debug:event-dispatcher".
168 $this->addListener($eventName, [
169 '\Civi\Core\CiviEventDispatcher',
170 'delegateToUF',
171 ], self::DEFAULT_HOOK_PRIORITY);
172 }
173 }
174 }
175
176 /**
177 * The dispatch policy allows you to filter certain events.
178 * This can be useful during upgrades or debugging.
179 *
180 * Enforcement will add systemic overhead, so this should normally be NULL.
181 *
182 * @param array|null $dispatchPolicy
183 * Each key is either the string-literal name of an event, or a regex delimited by '/'.
184 * Each value is one of: 'run', 'drop', 'warn', 'fail'.
185 * Exact name matches take precedence over regexes. Regexes are evaluated in order.
186 *
187 * Ex: ['hook_civicrm_pre' => 'fail']
188 * Ex: ['/^hook_/' => 'warn']
189 *
190 * @return static
191 */
192 public function setDispatchPolicy($dispatchPolicy) {
193 if (is_array($dispatchPolicy)) {
194 // Split $dispatchPolicy in two (exact rules vs regex rules).
195 $this->dispatchPolicyExact = [];
196 $this->dispatchPolicyRegex = [];
197 foreach ($dispatchPolicy as $pattern => $action) {
198 if ($pattern[0] === '/') {
199 $this->dispatchPolicyRegex[$pattern] = $action;
200 }
201 else {
202 $this->dispatchPolicyExact[$pattern] = $action;
203 }
204 }
205 }
206 else {
207 $this->dispatchPolicyExact = NULL;
208 $this->dispatchPolicyRegex = NULL;
209 }
210
211 return $this;
212 }
213
214 // /**
215 // * @return array|NULL
216 // */
217 // public function getDispatchPolicy() {
218 // return $this->dispatchPolicyRegex === NULL ? NULL : array_merge($this->dispatchPolicyExact, $this->dispatchPolicyRegex);
219 // }
220
221 /**
222 * @param string $eventName
223 * @return string
224 * Ex: 'run', 'drop', 'fail'
225 */
226 protected function checkDispatchPolicy($eventName) {
227 if (isset($this->dispatchPolicyExact[$eventName])) {
228 return $this->dispatchPolicyExact[$eventName];
229 }
230 foreach ($this->dispatchPolicyRegex as $eventPat => $action) {
231 if ($eventPat[0] === '/' && preg_match($eventPat, $eventName)) {
232 return $action;
233 }
234 }
235 return 'fail';
236 }
237
238 }