From 926d7afc5dd6bf4cf6c2d15b62d8bbaf82911899 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 17 Apr 2020 19:48:46 -0700 Subject: [PATCH] dev/core#1460 - CiviEventDispatcher - Add policy options --- Civi/Core/CiviEventDispatcher.php | 103 ++++++++++++++++++ .../Civi/Core/CiviEventDispatcherTest.php | 54 +++++++++ 2 files changed, 157 insertions(+) create mode 100644 tests/phpunit/Civi/Core/CiviEventDispatcherTest.php diff --git a/Civi/Core/CiviEventDispatcher.php b/Civi/Core/CiviEventDispatcher.php index 2334f1298c..b5e307dece 100644 --- a/Civi/Core/CiviEventDispatcher.php +++ b/Civi/Core/CiviEventDispatcher.php @@ -28,6 +28,18 @@ class CiviEventDispatcher extends ContainerAwareEventDispatcher { */ private $autoListeners = []; + /** + * @var array|null + * Array(string $eventName => string $action) + */ + private $dispatchPolicyExact = NULL; + + /** + * @var array|null + * Array(string $eventRegex => string $action) + */ + private $dispatchPolicyRegex = NULL; + /** * Determine whether $eventName should delegate to the CMS hook system. * @@ -43,6 +55,35 @@ class CiviEventDispatcher extends ContainerAwareEventDispatcher { * @inheritDoc */ public function dispatch($eventName, Event $event = NULL) { + // Dispatch policies add systemic overhead and (normally) should not be evaluated. JNZ. + if ($this->dispatchPolicyRegex !== NULL) { + switch ($mode = $this->checkDispatchPolicy($eventName)) { + case 'run': + // Continue on the normal execution. + break; + + case 'drop': + // Quietly ignore the event. + return $event; + + case 'warn': + // Run the event, but complain about it. + error_log("Unexpectedly dispatching event \"$eventName\"."); + break; + + case 'warn-drop': + // Ignore the event, but complaint about it. + error_log("Unexpectedly dispatching event \"$eventName\"."); + return $event; + + case 'fail': + throw new \RuntimeException("The dispatch policy prohibits event \"$eventName\"."); + + default: + throw new \RuntimeException("The dispatch policy for \"$eventName\" is unrecognized ($mode)."); + + } + } $this->bindPatterns($eventName); return parent::dispatch($eventName, $event); } @@ -132,4 +173,66 @@ class CiviEventDispatcher extends ContainerAwareEventDispatcher { } } + /** + * The dispatch policy allows you to filter certain events. + * This can be useful during upgrades or debugging. + * + * Enforcement will add systemic overhead, so this should normally be NULL. + * + * @param array|null $dispatchPolicy + * Each key is either the string-literal name of an event, or a regex delimited by '/'. + * Each value is one of: 'run', 'drop', 'warn', 'fail'. + * Exact name matches take precedence over regexes. Regexes are evaluated in order. + * + * Ex: ['hook_civicrm_pre' => 'fail'] + * Ex: ['/^hook_/' => 'warn'] + * + * @return static + */ + public function setDispatchPolicy($dispatchPolicy) { + if (is_array($dispatchPolicy)) { + // Split $dispatchPolicy in two (exact rules vs regex rules). + $this->dispatchPolicyExact = []; + $this->dispatchPolicyRegex = []; + foreach ($dispatchPolicy as $pattern => $action) { + if ($pattern[0] === '/') { + $this->dispatchPolicyRegex[$pattern] = $action; + } + else { + $this->dispatchPolicyExact[$pattern] = $action; + } + } + } + else { + $this->dispatchPolicyExact = NULL; + $this->dispatchPolicyRegex = NULL; + } + + return $this; + } + + // /** + // * @return array|NULL + // */ + // public function getDispatchPolicy() { + // return $this->dispatchPolicyRegex === NULL ? NULL : array_merge($this->dispatchPolicyExact, $this->dispatchPolicyRegex); + // } + + /** + * @param string $eventName + * @return string + * Ex: 'run', 'drop', 'fail' + */ + protected function checkDispatchPolicy($eventName) { + if (isset($this->dispatchPolicyExact[$eventName])) { + return $this->dispatchPolicyExact[$eventName]; + } + foreach ($this->dispatchPolicyRegex as $eventPat => $action) { + if ($eventPat[0] === '/' && preg_match($eventPat, $eventName)) { + return $action; + } + } + return 'fail'; + } + } diff --git a/tests/phpunit/Civi/Core/CiviEventDispatcherTest.php b/tests/phpunit/Civi/Core/CiviEventDispatcherTest.php new file mode 100644 index 0000000000..5b9c6d7687 --- /dev/null +++ b/tests/phpunit/Civi/Core/CiviEventDispatcherTest.php @@ -0,0 +1,54 @@ +setDispatchPolicy([ + 'hook_civicrm_fakeRunnable' => 'run', + ]); + $calls = []; + $d->addListener('hook_civicrm_fakeRunnable', function() use (&$calls) { + $calls['hook_civicrm_fakeRunnable'] = 1; + }); + $d->dispatch('hook_civicrm_fakeRunnable', new GenericHookEvent()); + $this->assertEquals(1, $calls['hook_civicrm_fakeRunnable']); + } + + public function testDispatchPolicy_drop() { + $d = new CiviEventDispatcher(\Civi::container()); + $d->setDispatchPolicy([ + '/^hook_civicrm_fakeDr/' => 'drop', + ]); + $calls = []; + $d->addListener('hook_civicrm_fakeDroppable', function() use (&$calls) { + $calls['hook_civicrm_fakeDroppable'] = 1; + }); + $d->dispatch('hook_civicrm_fakeDroppable', new GenericHookEvent()); + $this->assertTrue(!isset($calls['hook_civicrm_fakeDroppable'])); + } + + public function testDispatchPolicy_fail() { + $d = new CiviEventDispatcher(\Civi::container()); + $d->setDispatchPolicy([ + '/^hook_civicrm_fakeFa/' => 'fail', + ]); + try { + $d->dispatch('hook_civicrm_fakeFailure', new GenericHookEvent()); + $this->fail('Expected exception'); + } + catch (\Exception $e) { + $this->assertRegExp(';The dispatch policy prohibits event;', $e->getMessage()); + } + } + +} -- 2.25.1