5 use Symfony\Component\EventDispatcher\EventDispatcher
;
6 use Symfony\Component\EventDispatcher\Event
;
9 * Class CiviEventDispatcher
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().
16 * @see \CRM_Utils_Hook
18 class CiviEventDispatcher
extends EventDispatcher
{
20 const DEFAULT_HOOK_PRIORITY
= -100;
23 * Track the list of hook-events for which we have autoregistered
27 * Array(string $eventName => trueish).
29 private $autoListeners = [];
32 * A list of dispatch-policies (based on an exact-match to the event name).
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.
38 * Array(string $eventName => string $action)
40 private $dispatchPolicyExact = NULL;
43 * A list of dispatch-policies (based on an regex-match to the event name).
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.
49 * Array(string $eventRegex => string $action)
51 private $dispatchPolicyRegex = NULL;
54 * Determine whether $eventName should delegate to the CMS hook system.
56 * @param string $eventName
57 * Ex: 'civi.token.eval', 'hook_civicrm_post`.
60 protected function isHookEvent($eventName) {
61 return (substr($eventName, 0, 5) === 'hook_') && (strpos($eventName, '::') === FALSE);
65 * Adds a service as event listener.
67 * This provides partial backwards compatibility with ContainerAwareEventDispatcher.
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.
76 * @throws \InvalidArgumentException
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');
83 $this->addListener($eventName, function($event) use ($callback) {
86 $svc = \Civi
::container()->get($callback[0]);
88 return call_user_func([$svc, $callback[1]], $event);
95 public function dispatch($eventName, Event
$event = NULL) {
96 // Dispatch policies add systemic overhead and (normally) should not be evaluated. JNZ.
97 if ($this->dispatchPolicyRegex
!== NULL) {
98 switch ($mode = $this->checkDispatchPolicy($eventName)) {
100 // Continue on the normal execution.
104 // Quietly ignore the event.
108 // Run the event, but complain about it.
109 error_log("Unexpectedly dispatching event \"$eventName\".");
113 // Ignore the event, but complaint about it.
114 error_log("Unexpectedly dispatching event \"$eventName\".");
118 throw new \
RuntimeException("The dispatch policy prohibits event \"$eventName\".");
121 throw new \
RuntimeException("The dispatch policy for \"$eventName\" is unrecognized ($mode).");
125 $this->bindPatterns($eventName);
126 return parent
::dispatch($eventName, $event);
132 public function getListeners($eventName = NULL) {
133 $this->bindPatterns($eventName);
134 return parent
::getListeners($eventName);
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);
147 * Invoke hooks using an event object.
149 * @param \Civi\Core\Event\GenericHookEvent $event
150 * @param string $eventName
151 * Ex: 'hook_civicrm_dashboard'.
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);
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);
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);
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);
173 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], \CRM_Utils_Hook
::$_nullObject, \CRM_Utils_Hook
::$_nullObject, \CRM_Utils_Hook
::$_nullObject, $hookName);
177 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], \CRM_Utils_Hook
::$_nullObject, \CRM_Utils_Hook
::$_nullObject, $hookName);
181 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], $params[4], \CRM_Utils_Hook
::$_nullObject, $hookName);
185 $fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $hookName);
189 throw new \
RuntimeException("hook_{$hookName} cannot support more than 6 parameters");
192 $event->addReturnValues($fResult);
196 * Attach any pattern-based listeners which may be interested in $eventName.
198 * @param string $eventName
199 * Ex: 'civi.api.resolve' or 'hook_civicrm_dashboard'.
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".
208 $this->addListener($eventName, [
209 '\Civi\Core\CiviEventDispatcher',
211 ], self
::DEFAULT_HOOK_PRIORITY
);
217 * Set the dispatch policy. This allows you to filter certain events.
218 * This can be useful during upgrades or debugging.
220 * Enforcement will add systemic overhead, so this should normally be NULL.
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.
227 * Ex: ['hook_civicrm_pre' => 'fail']
228 * Ex: ['/^hook_/' => 'warn']
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;
242 $this->dispatchPolicyExact
[$pattern] = $action;
247 $this->dispatchPolicyExact
= NULL;
248 $this->dispatchPolicyRegex
= NULL;
255 // * @return array|NULL
257 // public function getDispatchPolicy() {
258 // return $this->dispatchPolicyRegex === NULL ? NULL : array_merge($this->dispatchPolicyExact, $this->dispatchPolicyRegex);
262 * Determine whether the dispatch policy applies to a given event.
264 * @param string $eventName
265 * Ex: 'civi.api.resolve' or 'hook_civicrm_dashboard'.
267 * Ex: 'run', 'drop', 'fail'
269 protected function checkDispatchPolicy($eventName) {
270 if (isset($this->dispatchPolicyExact
[$eventName])) {
271 return $this->dispatchPolicyExact
[$eventName];
273 foreach ($this->dispatchPolicyRegex
as $eventPat => $action) {
274 if ($eventPat[0] === '/' && preg_match($eventPat, $eventName)) {