[EventDispatcher] Handle laziness internally instead of relying on ClosureProxyArgument

This commit is contained in:
Nicolas Grekas 2017-06-01 11:24:09 +02:00
parent 7a9875c3cd
commit c17a009d66
5 changed files with 134 additions and 17 deletions

View File

@ -116,7 +116,7 @@ class ContainerAwareEventDispatcher extends EventDispatcher
public function hasListeners($eventName = null) public function hasListeners($eventName = null)
{ {
if (null === $eventName) { if (null === $eventName) {
return (bool) count($this->listenerIds) || (bool) count($this->listeners); return count($this->listenerIds) || count($this->listeners) || parent::hasListeners();
} }
if (isset($this->listenerIds[$eventName])) { if (isset($this->listenerIds[$eventName])) {

View File

@ -11,10 +11,11 @@
namespace Symfony\Component\EventDispatcher\DependencyInjection; namespace Symfony\Component\EventDispatcher\DependencyInjection;
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@ -78,7 +79,7 @@ class RegisterListenersPass implements CompilerPassInterface
$event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']); $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']);
} }
$definition->addMethodCall('addListener', array($event['event'], new ClosureProxyArgument($id, $event['method']), $priority)); $definition->addMethodCall('addListener', array($event['event'], array(new ServiceClosureArgument(new Reference($id)), $event['method']), $priority));
} }
} }
@ -103,7 +104,7 @@ class RegisterListenersPass implements CompilerPassInterface
ExtractingEventDispatcher::$subscriber = $class; ExtractingEventDispatcher::$subscriber = $class;
$extractingDispatcher->addSubscriber($extractingDispatcher); $extractingDispatcher->addSubscriber($extractingDispatcher);
foreach ($extractingDispatcher->listeners as $args) { foreach ($extractingDispatcher->listeners as $args) {
$args[1] = new ClosureProxyArgument($id, $args[1]); $args[1] = array(new ServiceClosureArgument(new Reference($id)), $args[1]);
$definition->addMethodCall('addListener', $args); $definition->addMethodCall('addListener', $args);
} }
$extractingDispatcher->listeners = array(); $extractingDispatcher->listeners = array();

View File

@ -24,6 +24,7 @@ namespace Symfony\Component\EventDispatcher;
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
* @author Jordan Alliot <jordan.alliot@gmail.com> * @author Jordan Alliot <jordan.alliot@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/ */
class EventDispatcher implements EventDispatcherInterface class EventDispatcher implements EventDispatcherInterface
{ {
@ -52,7 +53,7 @@ class EventDispatcher implements EventDispatcherInterface
public function getListeners($eventName = null) public function getListeners($eventName = null)
{ {
if (null !== $eventName) { if (null !== $eventName) {
if (!isset($this->listeners[$eventName])) { if (empty($this->listeners[$eventName])) {
return array(); return array();
} }
@ -77,13 +78,23 @@ class EventDispatcher implements EventDispatcherInterface
*/ */
public function getListenerPriority($eventName, $listener) public function getListenerPriority($eventName, $listener)
{ {
if (!isset($this->listeners[$eventName])) { if (empty($this->listeners[$eventName])) {
return; return;
} }
if (is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure) {
$listener[0] = $listener[0]();
}
foreach ($this->listeners[$eventName] as $priority => $listeners) { foreach ($this->listeners[$eventName] as $priority => $listeners) {
if (false !== in_array($listener, $listeners, true)) { foreach ($listeners as $k => $v) {
return $priority; if ($v !== $listener && is_array($v) && isset($v[0]) && $v[0] instanceof \Closure) {
$v[0] = $v[0]();
$this->listeners[$eventName][$priority][$k] = $v;
}
if ($v === $listener) {
return $priority;
}
} }
} }
} }
@ -93,7 +104,17 @@ class EventDispatcher implements EventDispatcherInterface
*/ */
public function hasListeners($eventName = null) public function hasListeners($eventName = null)
{ {
return (bool) $this->getListeners($eventName); if (null !== $eventName) {
return !empty($this->listeners[$eventName]);
}
foreach ($this->listeners as $eventListeners) {
if ($eventListeners) {
return true;
}
}
return false;
} }
/** /**
@ -110,13 +131,30 @@ class EventDispatcher implements EventDispatcherInterface
*/ */
public function removeListener($eventName, $listener) public function removeListener($eventName, $listener)
{ {
if (!isset($this->listeners[$eventName])) { if (empty($this->listeners[$eventName])) {
return; return;
} }
if (is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure) {
$listener[0] = $listener[0]();
}
foreach ($this->listeners[$eventName] as $priority => $listeners) { foreach ($this->listeners[$eventName] as $priority => $listeners) {
if (false !== ($key = array_search($listener, $listeners, true))) { foreach ($listeners as $k => $v) {
unset($this->listeners[$eventName][$priority][$key], $this->sorted[$eventName]); if ($v !== $listener && is_array($v) && isset($v[0]) && $v[0] instanceof \Closure) {
$v[0] = $v[0]();
}
if ($v === $listener) {
unset($listeners[$k], $this->sorted[$eventName]);
} else {
$listeners[$k] = $v;
}
}
if ($listeners) {
$this->listeners[$eventName][$priority] = $listeners;
} else {
unset($this->listeners[$eventName][$priority]);
} }
} }
} }
@ -183,6 +221,16 @@ class EventDispatcher implements EventDispatcherInterface
private function sortListeners($eventName) private function sortListeners($eventName)
{ {
krsort($this->listeners[$eventName]); krsort($this->listeners[$eventName]);
$this->sorted[$eventName] = call_user_func_array('array_merge', $this->listeners[$eventName]); $this->sorted[$eventName] = array();
foreach ($this->listeners[$eventName] as $priority => $listeners) {
foreach ($listeners as $k => $listener) {
if (is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure) {
$listener[0] = $listener[0]();
$this->listeners[$eventName][$priority][$k] = $listener;
}
$this->sorted[$eventName][] = $listener;
}
}
} }
} }

View File

@ -302,6 +302,73 @@ abstract class AbstractEventDispatcherTest extends TestCase
$this->assertFalse($this->dispatcher->hasListeners('foo')); $this->assertFalse($this->dispatcher->hasListeners('foo'));
$this->assertFalse($this->dispatcher->hasListeners()); $this->assertFalse($this->dispatcher->hasListeners());
} }
public function testHasListenersIsLazy()
{
$called = 0;
$listener = array(function () use (&$called) { ++$called; }, 'onFoo');
$this->dispatcher->addListener('foo', $listener);
$this->assertTrue($this->dispatcher->hasListeners());
$this->assertTrue($this->dispatcher->hasListeners('foo'));
$this->assertSame(0, $called);
}
public function testDispatchLazyListener()
{
$called = 0;
$factory = function () use (&$called) {
++$called;
return new TestWithDispatcher();
};
$this->dispatcher->addListener('foo', array($factory, 'foo'));
$this->assertSame(0, $called);
$this->dispatcher->dispatch('foo', new Event());
$this->dispatcher->dispatch('foo', new Event());
$this->assertSame(1, $called);
}
public function testRemoveFindsLazyListeners()
{
$test = new TestWithDispatcher();
$factory = function () use ($test) { return $test; };
$this->dispatcher->addListener('foo', array($factory, 'foo'));
$this->assertTrue($this->dispatcher->hasListeners('foo'));
$this->dispatcher->removeListener('foo', array($test, 'foo'));
$this->assertFalse($this->dispatcher->hasListeners('foo'));
$this->dispatcher->addListener('foo', array($test, 'foo'));
$this->assertTrue($this->dispatcher->hasListeners('foo'));
$this->dispatcher->removeListener('foo', array($factory, 'foo'));
$this->assertFalse($this->dispatcher->hasListeners('foo'));
}
public function testPriorityFindsLazyListeners()
{
$test = new TestWithDispatcher();
$factory = function () use ($test) { return $test; };
$this->dispatcher->addListener('foo', array($factory, 'foo'), 3);
$this->assertSame(3, $this->dispatcher->getListenerPriority('foo', array($test, 'foo')));
$this->dispatcher->removeListener('foo', array($factory, 'foo'));
$this->dispatcher->addListener('foo', array($test, 'foo'), 5);
$this->assertSame(5, $this->dispatcher->getListenerPriority('foo', array($factory, 'foo')));
}
public function testGetLazyListeners()
{
$test = new TestWithDispatcher();
$factory = function () use ($test) { return $test; };
$this->dispatcher->addListener('foo', array($factory, 'foo'), 3);
$this->assertSame(array(array($test, 'foo')), $this->dispatcher->getListeners('foo'));
$this->dispatcher->removeListener('foo', array($test, 'foo'));
$this->dispatcher->addListener('bar', array($factory, 'foo'), 3);
$this->assertSame(array('bar' => array(array($test, 'foo'))), $this->dispatcher->getListeners());
}
} }
class CallableClass class CallableClass

View File

@ -12,8 +12,9 @@
namespace Symfony\Component\EventDispatcher\Tests\DependencyInjection; namespace Symfony\Component\EventDispatcher\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
class RegisterListenersPassTest extends TestCase class RegisterListenersPassTest extends TestCase
@ -127,17 +128,17 @@ class RegisterListenersPassTest extends TestCase
$registerListenersPass->process($container); $registerListenersPass->process($container);
$definition = $container->getDefinition('event_dispatcher'); $definition = $container->getDefinition('event_dispatcher');
$expected_calls = array( $expectedCalls = array(
array( array(
'addListener', 'addListener',
array( array(
'event', 'event',
new ClosureProxyArgument('foo', 'onEvent'), array(new ServiceClosureArgument(new Reference('foo')), 'onEvent'),
0, 0,
), ),
), ),
); );
$this->assertEquals($expected_calls, $definition->getMethodCalls()); $this->assertEquals($expectedCalls, $definition->getMethodCalls());
} }
/** /**