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