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);
}
/**
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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++;
+ }
+
+}