Commit | Line | Data |
---|---|---|
762dc04d TO |
1 | <?php |
2 | ||
3 | namespace Civi\Core; | |
4 | ||
3c006cb8 | 5 | use Symfony\Component\EventDispatcher\EventDispatcher; |
762dc04d TO |
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 | */ | |
3c006cb8 | 18 | class 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 | ||
120 | default: | |
121 | throw new \RuntimeException("The dispatch policy for \"$eventName\" is unrecognized ($mode)."); | |
122 | ||
123 | } | |
124 | } | |
762dc04d TO |
125 | $this->bindPatterns($eventName); |
126 | return parent::dispatch($eventName, $event); | |
127 | } | |
128 | ||
129 | /** | |
130 | * @inheritDoc | |
131 | */ | |
132 | public function getListeners($eventName = NULL) { | |
133 | $this->bindPatterns($eventName); | |
134 | return parent::getListeners($eventName); | |
135 | } | |
136 | ||
137 | /** | |
138 | * @inheritDoc | |
139 | */ | |
140 | public function hasListeners($eventName = NULL) { | |
141 | // All hook_* events have default listeners, so hasListeners(NULL) is a truism. | |
142 | return ($eventName === NULL || $this->isHookEvent($eventName)) | |
143 | ? TRUE : parent::hasListeners($eventName); | |
144 | } | |
145 | ||
146 | /** | |
147 | * Invoke hooks using an event object. | |
148 | * | |
149 | * @param \Civi\Core\Event\GenericHookEvent $event | |
150 | * @param string $eventName | |
151 | * Ex: 'hook_civicrm_dashboard'. | |
152 | */ | |
153 | public static function delegateToUF($event, $eventName) { | |
154 | $hookName = substr($eventName, 5); | |
155 | $hooks = \CRM_Utils_Hook::singleton(); | |
156 | $params = $event->getHookValues(); | |
157 | $count = count($params); | |
158 | ||
159 | switch ($count) { | |
160 | case 0: | |
161 | $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); | |
162 | break; | |
163 | ||
164 | case 1: | |
165 | $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); | |
166 | break; | |
167 | ||
168 | case 2: | |
169 | $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); | |
170 | break; | |
171 | ||
172 | case 3: | |
173 | $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName); | |
174 | break; | |
175 | ||
176 | case 4: | |
177 | $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName); | |
178 | break; | |
179 | ||
180 | case 5: | |
181 | $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], $params[4], \CRM_Utils_Hook::$_nullObject, $hookName); | |
182 | break; | |
183 | ||
184 | case 6: | |
185 | $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $hookName); | |
186 | break; | |
187 | ||
188 | default: | |
189 | throw new \RuntimeException("hook_{$hookName} cannot support more than 6 parameters"); | |
190 | } | |
191 | ||
192 | $event->addReturnValues($fResult); | |
193 | } | |
194 | ||
195 | /** | |
7a4b686e TO |
196 | * Attach any pattern-based listeners which may be interested in $eventName. |
197 | * | |
762dc04d TO |
198 | * @param string $eventName |
199 | * Ex: 'civi.api.resolve' or 'hook_civicrm_dashboard'. | |
200 | */ | |
201 | protected function bindPatterns($eventName) { | |
202 | if ($eventName !== NULL && !isset($this->autoListeners[$eventName])) { | |
203 | $this->autoListeners[$eventName] = 1; | |
204 | if ($this->isHookEvent($eventName)) { | |
205 | // WISHLIST: For native extensions (and possibly D6/D7/D8/BD), enumerate | |
206 | // the listeners and list them one-by-one. This would make it easier to | |
207 | // inspect via "cv debug:event-dispatcher". | |
c64f69d9 | 208 | $this->addListener($eventName, [ |
762dc04d TO |
209 | '\Civi\Core\CiviEventDispatcher', |
210 | 'delegateToUF', | |
c64f69d9 | 211 | ], self::DEFAULT_HOOK_PRIORITY); |
762dc04d TO |
212 | } |
213 | } | |
214 | } | |
215 | ||
926d7afc | 216 | /** |
7a4b686e | 217 | * Set the dispatch policy. This allows you to filter certain events. |
926d7afc TO |
218 | * This can be useful during upgrades or debugging. |
219 | * | |
220 | * Enforcement will add systemic overhead, so this should normally be NULL. | |
221 | * | |
222 | * @param array|null $dispatchPolicy | |
223 | * Each key is either the string-literal name of an event, or a regex delimited by '/'. | |
224 | * Each value is one of: 'run', 'drop', 'warn', 'fail'. | |
225 | * Exact name matches take precedence over regexes. Regexes are evaluated in order. | |
226 | * | |
227 | * Ex: ['hook_civicrm_pre' => 'fail'] | |
228 | * Ex: ['/^hook_/' => 'warn'] | |
229 | * | |
230 | * @return static | |
231 | */ | |
232 | public function setDispatchPolicy($dispatchPolicy) { | |
233 | if (is_array($dispatchPolicy)) { | |
234 | // Split $dispatchPolicy in two (exact rules vs regex rules). | |
235 | $this->dispatchPolicyExact = []; | |
236 | $this->dispatchPolicyRegex = []; | |
237 | foreach ($dispatchPolicy as $pattern => $action) { | |
238 | if ($pattern[0] === '/') { | |
239 | $this->dispatchPolicyRegex[$pattern] = $action; | |
240 | } | |
241 | else { | |
242 | $this->dispatchPolicyExact[$pattern] = $action; | |
243 | } | |
244 | } | |
245 | } | |
246 | else { | |
247 | $this->dispatchPolicyExact = NULL; | |
248 | $this->dispatchPolicyRegex = NULL; | |
249 | } | |
250 | ||
251 | return $this; | |
252 | } | |
253 | ||
254 | // /** | |
255 | // * @return array|NULL | |
256 | // */ | |
257 | // public function getDispatchPolicy() { | |
258 | // return $this->dispatchPolicyRegex === NULL ? NULL : array_merge($this->dispatchPolicyExact, $this->dispatchPolicyRegex); | |
259 | // } | |
260 | ||
261 | /** | |
7a4b686e TO |
262 | * Determine whether the dispatch policy applies to a given event. |
263 | * | |
926d7afc | 264 | * @param string $eventName |
7a4b686e | 265 | * Ex: 'civi.api.resolve' or 'hook_civicrm_dashboard'. |
926d7afc TO |
266 | * @return string |
267 | * Ex: 'run', 'drop', 'fail' | |
268 | */ | |
269 | protected function checkDispatchPolicy($eventName) { | |
270 | if (isset($this->dispatchPolicyExact[$eventName])) { | |
271 | return $this->dispatchPolicyExact[$eventName]; | |
272 | } | |
273 | foreach ($this->dispatchPolicyRegex as $eventPat => $action) { | |
274 | if ($eventPat[0] === '/' && preg_match($eventPat, $eventName)) { | |
275 | return $action; | |
276 | } | |
277 | } | |
278 | return 'fail'; | |
279 | } | |
280 | ||
762dc04d | 281 | } |