(REF) CiviEventDispatcher - Move service-listener stub to standalone class
authorTim Otten <totten@civicrm.org>
Tue, 4 May 2021 20:31:51 +0000 (13:31 -0700)
committerTim Otten <totten@civicrm.org>
Wed, 5 May 2021 01:24:48 +0000 (18:24 -0700)
This change makes it easier for reflective tools (e.g.  `cv
debug:event-dispatcher`) to recognize service-based listeners.

Before: `addListenerServce()` creates a stub for the target service+method.
The stub is an anonymous `function`.

After: `addListenerService()` creates a staub for the target service+method.
The stub is based on invokable class.

Civi/Core/CiviEventDispatcher.php
Civi/Core/Event/ServiceListener.php [new file with mode: 0644]
tests/phpunit/Civi/Core/Event/ServiceListenerTest.php [new file with mode: 0644]

index ac104537d81bfe8968015ea37004a6447634aeb1..195fc1ccfed99d8409a72dc3a62bfb3053101d9f 100644 (file)
@@ -80,13 +80,7 @@ class CiviEventDispatcher extends EventDispatcher {
       throw new \InvalidArgumentException('Expected an array("service", "method") argument');
     }
 
-    $this->addListener($eventName, function($event) use ($callback) {
-      static $svc;
-      if ($svc === NULL) {
-        $svc = \Civi::container()->get($callback[0]);
-      }
-      return call_user_func([$svc, $callback[1]], $event);
-    }, $priority);
+    $this->addListener($eventName, new \Civi\Core\Event\ServiceListener($callback), $priority);
   }
 
   /**
diff --git a/Civi/Core/Event/ServiceListener.php b/Civi/Core/Event/ServiceListener.php
new file mode 100644 (file)
index 0000000..ae5b70c
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+
+namespace Civi\Core\Event;
+
+/**
+ * A ServiceListener is a `callable` (supporting "__invoke()") which references
+ * a method of a service-object.
+ *
+ * The following two callables are conceptually similar:
+ *
+ *   (A) addListener('some.event', [Civi::service('foo'), 'doBar']);
+ *   (B) addListener('some.event', new ServiceListener(['foo', 'doBar']));
+ *
+ * The difference is that (A) immediately instantiates the 'foo' service,
+ * whereas (B) instantiates `foo` lazily. (B) is more amenable to serialization,
+ * caching, etc. If you have a long-tail of many services/listeners/etc that
+ * are not required for every page-load, then (B) should perform better.
+ *
+ * @package Civi\Core\Event
+ */
+class ServiceListener {
+
+  /**
+   * @var array
+   *   Ex: ['service_name', 'someMethod']
+   */
+  private $inertCb = NULL;
+
+  /**
+   * @var array|null
+   *   Ex: [$svcObj, 'someMethod']
+   */
+  private $liveCb = NULL;
+
+  /**
+   * @var \Symfony\Component\DependencyInjection\ContainerInterface
+   */
+  private $container = NULL;
+
+  /**
+   * @param array $callback
+   *   Ex: ['service_name', 'someMethod']
+   */
+  public function __construct($callback) {
+    $this->inertCb = $callback;
+  }
+
+  public function __invoke(...$args) {
+    if ($this->liveCb === NULL) {
+      $c = $this->container ?: \Civi::container();
+      $this->liveCb = [$c->get($this->inertCb[0]), $this->inertCb[1]];
+    }
+    return call_user_func_array($this->liveCb, $args);
+  }
+
+  public function __toString() {
+    $class = NULL;
+    if (\Civi\Core\Container::isContainerBooted()) {
+      try {
+        $c = $this->container ?: \Civi::container();
+        $class = $c->findDefinition($this->inertCb[0])->getClass();
+      }
+      catch (Throwable $t) {
+      }
+    }
+    if ($class) {
+      return sprintf('$(%s)->%s() [%s]', $this->inertCb[0], $this->inertCb[1], $class);
+    }
+    else {
+      return sprintf('\$(%s)->%s()', $this->inertCb[0], $this->inertCb[1]);
+    }
+  }
+
+  public function __sleep() {
+    return ['inertCb'];
+  }
+
+  /**
+   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+   * @return static
+   */
+  public function setContainer(\Symfony\Component\DependencyInjection\ContainerInterface $container) {
+    $this->container = $container;
+    return $this;
+  }
+
+}
diff --git a/tests/phpunit/Civi/Core/Event/ServiceListenerTest.php b/tests/phpunit/Civi/Core/Event/ServiceListenerTest.php
new file mode 100644 (file)
index 0000000..0cf534b
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+namespace Civi\Core\Event;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+
+class ServiceListenerTest extends \CiviUnitTestCase {
+
+  public function tearDown(): void {
+    ServiceListenerTestExample::$notes = [];
+    parent::tearDown();
+  }
+
+  public function testDispatch() {
+    $changeMe = $rand = rand(0, 16384);
+
+    $container = new ContainerBuilder();
+    $container->setDefinition('test.svlt', new Definition(ServiceListenerTestExample::class, [$rand]))
+      ->setPublic(TRUE);
+
+    $d = \Civi::dispatcher();
+    $d->addListener('hook_civicrm_svlt', (new ServiceListener(['test.svlt', 'onSvlt']))->setContainer($container));
+
+    // Baseline
+    $this->assertEquals([], ServiceListenerTestExample::$notes);
+    $this->assertEquals($changeMe, $rand);
+
+    // First call - instantiate and run
+    $d->dispatch('hook_civicrm_svlt', GenericHookEvent::create(['foo' => &$changeMe]));
+    $this->assertEquals($changeMe, 1 + $rand);
+    $this->assertEquals(["construct($rand)", "onSvlt($rand)"],
+      ServiceListenerTestExample::$notes);
+
+    // Second call - reuse and run
+    $d->dispatch('hook_civicrm_svlt', GenericHookEvent::create(['foo' => &$changeMe]));
+    $this->assertEquals($changeMe, 2 + $rand);
+    $this->assertEquals(["construct($rand)", "onSvlt($rand)", "onSvlt(" . ($rand + 1) . ")"],
+      ServiceListenerTestExample::$notes);
+  }
+
+}
+
+class ServiceListenerTestExample {
+
+  /**
+   * Free-form list of strings.
+   *
+   * @var array
+   */
+  public static $notes = [];
+
+  public function __construct($rand) {
+    self::$notes[] = "construct($rand)";
+  }
+
+  public function onSvlt(GenericHookEvent $e) {
+    self::$notes[] = "onSvlt({$e->foo})";
+    $e->foo++;
+  }
+
+}