From 9936dceb587497e1ad15e5d03453c5d536db39cb Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Tue, 21 Jun 2022 05:41:21 -0700 Subject: [PATCH] Container - Use ClassScanner to setup HookListeners. Add test. Before: While setting up the container-cache, it scans for these patterns: * BAOs implementing HookInterface * BAOs implementing EventDispatcherInterface After: While setting up the container-cache, it scans for these patterns: * BAOs implementing HookInterface (*unchanged*) * BAOs implementing EventDispatcherInterface (*unchanged*) * Extensions for classes (./Civi and ./CRM) implementing HookInterface (*new*) Comment: For the moment, the `EventDispatcherInterface` is not (on its own) sufficient to activate auto-registration. This is because (1) the interface is quite common in the world-at-large, so we'd prone to over-listening and (2) we don't control the versioning-cycle, so the dephell is likely worse. However, if you really want it, I think that it will pick up classes that implement both `HookInterface` and `EventDispatcherInterface`. --- Civi/Core/Container.php | 11 +++++++---- .../example/CRM/Shimmy/ShimmyHooks.php | 9 +++++++++ .../example/tests/mixin/InterfaceTest.php | 16 ++++++++++++++++ .../tests/phpunit/E2E/Shimmy/LifecycleTest.php | 9 +++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 mixin/interface-php@1/example/CRM/Shimmy/ShimmyHooks.php diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index dc48b4926357..a1573dafd9f8 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -375,12 +375,15 @@ public function createContainer() { ))->addTag('kernel.event_subscriber')->setPublic(TRUE); $dispatcherDefn = $container->getDefinition('dispatcher'); - foreach (\CRM_Core_DAO_AllCoreTables::getBaoClasses() as $baoEntity => $baoClass) { - $listenerMap = EventScanner::findListeners($baoClass, $baoEntity); + $baoClasses = array_flip(\CRM_Core_DAO_AllCoreTables::getBaoClasses()); + $hookClasses = ClassScanner::get(['interface' => HookInterface::class]); + $allEventClasses = array_merge(array_keys($baoClasses), $hookClasses); + foreach ($allEventClasses as $eventClass) { + $listenerMap = EventScanner::findListeners($eventClass, $baoClasses[$eventClass] ?? NULL); if ($listenerMap) { - $file = (new \ReflectionClass($baoClass))->getFileName(); + $file = (new \ReflectionClass($eventClass))->getFileName(); $container->addResource(new \Symfony\Component\Config\Resource\FileResource($file)); - $dispatcherDefn->addMethodCall('addListenerMap', [$baoClass, $listenerMap]); + $dispatcherDefn->addMethodCall('addListenerMap', [$eventClass, $listenerMap]); } } diff --git a/mixin/interface-php@1/example/CRM/Shimmy/ShimmyHooks.php b/mixin/interface-php@1/example/CRM/Shimmy/ShimmyHooks.php new file mode 100644 index 000000000000..db000cbcb338 --- /dev/null +++ b/mixin/interface-php@1/example/CRM/Shimmy/ShimmyHooks.php @@ -0,0 +1,9 @@ +api4('WorkflowMessage', 'get', ['where' => [['name', '=', 'shimmy_message_example']]]); $this->assertEquals('CRM_Shimmy_ShimmyMessage', $items[0]['class']); + + // Assert that HookInterface's are registered. + $hookData = $this->fireHookShimmyFooBar($cv); + $this->assertEquals(['hello world'], $hookData); } public function testDisabled($cv) { // Assert that WorkflowMessageInterface's are removed. $items = $cv->api4('WorkflowMessage', 'get', ['where' => [['name', '=', 'shimmy_message_example']]]); $this->assertEmpty($items); + + // Assert that HookInterface's are removed. + $hookData = $this->fireHookShimmyFooBar($cv); + $this->assertEquals([], $hookData); } public function testUninstalled($cv) { // Assert that WorkflowMessageInterface's are removed. $items = $cv->api4('WorkflowMessage', 'get', ['where' => [['name', '=', 'shimmy_message_example']]]); $this->assertEmpty($items); + + // Assert that HookInterface's are removed. + $hookData = $this->fireHookShimmyFooBar($cv); + $this->assertEquals([], $hookData); } protected static function getPath($suffix = ''): string { return dirname(__DIR__, 2) . $suffix; } + protected function fireHookShimmyFooBar($cv): array { + return $cv->phpEval('$d=[]; Civi::dispatcher()->dispatch("hook_civicrm_shimmyFooBar", \Civi\Core\Event\GenericHookEvent::create(["data"=>&$d,"for"=>"world"])); return $d;'); + } + } diff --git a/tests/extensions/shimmy/tests/phpunit/E2E/Shimmy/LifecycleTest.php b/tests/extensions/shimmy/tests/phpunit/E2E/Shimmy/LifecycleTest.php index 49c60c782f38..dc1a7b8bc91e 100644 --- a/tests/extensions/shimmy/tests/phpunit/E2E/Shimmy/LifecycleTest.php +++ b/tests/extensions/shimmy/tests/phpunit/E2E/Shimmy/LifecycleTest.php @@ -109,6 +109,11 @@ public function api4($entity, $action, $params): array { return (array) civicrm_api4($entity, $action, $params); } + public function phpEval(string $expr): array { + // phpcs:ignore + return eval($expr); + } + }; } @@ -124,6 +129,10 @@ public function api4($entity, $action, $params): array { return $this->cv('api4 --in=json ' . escapeshellarg("$entity.$action"), json_encode($params)); } + public function phpEval(string $expr): array { + return $this->cv('php:eval ' . escapeshellarg($expr)); + } + /** * Call the "cv" command. *