Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[EventDispatcher] Delay instantiation of a service until the respective listener is triggered in ContainerAwareEventDispatcher #12019

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 32 additions & 104 deletions src/Symfony/Component/EventDispatcher/ContainerAwareEventDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\EventDispatcher;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\IntrospectableContainerInterface;

/**
* Lazily loads listeners and subscribers from the dependency injection
Expand All @@ -30,16 +31,10 @@ class ContainerAwareEventDispatcher extends EventDispatcher
private $container;

/**
* The service IDs of the event listeners and subscribers
* @var array
*/
private $listenerIds = array();

/**
* The services registered as listeners
* @var array
* A list of proxy closures indexed by eventName, serviceId, method.
* @var ContainerInterface
*/
private $listeners = array();
private $proxies;

/**
* Constructor.
Expand Down Expand Up @@ -69,66 +64,42 @@ public function addListenerService($eventName, $callback, $priority = 0)
throw new \InvalidArgumentException('Expected an array("service", "method") argument');
}

$this->listenerIds[$eventName][] = array($callback[0], $callback[1], $priority);
}

public function removeListener($eventName, $listener)
{
$this->lazyLoad($eventName);
$container = $this->container;
list($serviceId, $method) = $callback;

if (isset($this->listeners[$eventName])) {
foreach ($this->listeners[$eventName] as $key => $l) {
foreach ($this->listenerIds[$eventName] as $i => $args) {
list($serviceId, $method, $priority) = $args;
if ($key === $serviceId.'.'.$method) {
if ($listener === array($l, $method)) {
unset($this->listeners[$eventName][$key]);
if (empty($this->listeners[$eventName])) {
unset($this->listeners[$eventName]);
}
unset($this->listenerIds[$eventName][$i]);
if (empty($this->listenerIds[$eventName])) {
unset($this->listenerIds[$eventName]);
}
}
}
}
}
if (isset($this->proxies[$eventName][$serviceId][$method])) {
$proxy = $this->proxies[$eventName][$serviceId][$method];
unset($this->proxies[$eventName][$serviceId][$method]);
parent::removeListener($eventName, $proxy);
}

parent::removeListener($eventName, $listener);
$proxy = new LazyServiceListener($container, $serviceId, $method);
$this->proxies[$eventName][$serviceId][$method] = $proxy;
parent::addListener($eventName, $proxy, $priority);
}

/**
* @see EventDispatcherInterface::hasListeners
* @see EventDispatcherInterface::removeListener
*/
public function hasListeners($eventName = null)
{
if (null === $eventName) {
return (bool) count($this->listenerIds) || (bool) count($this->listeners);
}

if (isset($this->listenerIds[$eventName])) {
return true;
}

return parent::hasListeners($eventName);
}

/**
* @see EventDispatcherInterface::getListeners
*/
public function getListeners($eventName = null)
public function removeListener($eventName, $listener)
{
if (null === $eventName) {
foreach (array_keys($this->listenerIds) as $serviceEventName) {
$this->lazyLoad($serviceEventName);
$introspect = $this->container instanceof IntrospectableContainerInterface;
if (isset($this->proxies[$eventName])) {
foreach ($this->proxies[$eventName] as $serviceId => $methods) {
if (!$introspect || $this->container->initialized($serviceId)) {
foreach ($methods as $method => $proxy) {
if ($listener === array($this->container->get($serviceId), $method)) {
unset($this->proxies[$eventName][$serviceId][$method]);
parent::removeListener($eventName, $proxy);

return;
}
}
}
}
} else {
$this->lazyLoad($eventName);
}

return parent::getListeners($eventName);
parent::removeListener($eventName, $listener);
}

/**
Expand All @@ -141,62 +112,19 @@ public function addSubscriberService($serviceId, $class)
{
foreach ($class::getSubscribedEvents() as $eventName => $params) {
if (is_string($params)) {
$this->listenerIds[$eventName][] = array($serviceId, $params, 0);
$this->addListenerService($eventName, array($serviceId, $params), 0);
} elseif (is_string($params[0])) {
$this->listenerIds[$eventName][] = array($serviceId, $params[0], isset($params[1]) ? $params[1] : 0);
$this->addListenerService($eventName, array($serviceId, $params[0]), isset($params[1]) ? $params[1] : 0);
} else {
foreach ($params as $listener) {
$this->listenerIds[$eventName][] = array($serviceId, $listener[0], isset($listener[1]) ? $listener[1] : 0);
$this->addListenerService($eventName, array($serviceId, $listener[0]), isset($listener[1]) ? $listener[1] : 0);
}
}
}
}

/**
* {@inheritdoc}
*
* Lazily loads listeners for this event from the dependency injection
* container.
*
* @throws \InvalidArgumentException if the service is not defined
*/
public function dispatch($eventName, Event $event = null)
{
$this->lazyLoad($eventName);

return parent::dispatch($eventName, $event);
}

public function getContainer()
{
return $this->container;
}

/**
* Lazily loads listeners for this event from the dependency injection
* container.
*
* @param string $eventName The name of the event to dispatch. The name of
* the event is the name of the method that is
* invoked on listeners.
*/
protected function lazyLoad($eventName)
{
if (isset($this->listenerIds[$eventName])) {
foreach ($this->listenerIds[$eventName] as $args) {
list($serviceId, $method, $priority) = $args;
$listener = $this->container->get($serviceId);

$key = $serviceId.'.'.$method;
if (!isset($this->listeners[$eventName][$key])) {
$this->addListener($eventName, array($listener, $method), $priority);
} elseif ($listener !== $this->listeners[$eventName][$key]) {
parent::removeListener($eventName, array($this->listeners[$eventName][$key], $method));
$this->addListener($eventName, array($listener, $method), $priority);
}

$this->listeners[$eventName][$key] = $listener;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\LazyServiceListener;
use Symfony\Component\Stopwatch\Stopwatch;
use Psr\Log\LoggerInterface;

Expand Down Expand Up @@ -268,6 +269,13 @@ private function getListenerInfo($listener, $eventName)
$info = array(
'event' => $eventName,
);
// Unpack lazy container service listener.
if ($listener instanceof LazyServiceListener) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a hack. What about implementing the IntrospectableCallable as suggested by @stof instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The very reason for this patch is to prevent preliminary service instantiation. I'm not comfortable with adding a public getInnerCallable which suffers exactly from this side-effect.

Then there is already a WrappedListener class. I initially was tempted to extract a WrappedListenerInterface from there but that would have made the code in TraceableEventDispatcher less obvious. Mainly because in that case getWrappedListener would have been used for two very different things. Introducing IntrospectableCallable without touching the WrappedListener also would result in a confusing coexistence of two similar concepts in the same component.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@znerol your implementation also triggers the lazy-loading of the service when tracing events. But instead of providing an interface usable by other implementations of a lazy-loading dispatcher, it is tied to the Symfony container

$container = $listener->getContainer();
$method = $listener->getMethod();
$serviceId = $listener->getServiceId();
$listener = array($container->get($serviceId), $method);
}
if ($listener instanceof \Closure) {
$info += array(
'type' => 'Closure',
Expand Down
85 changes: 85 additions & 0 deletions src/Symfony/Component/EventDispatcher/LazyServiceListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\EventDispatcher;

use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* A listener forwarding its invocation to a service.
*/
class LazyServiceListener
{
/**
* The container from where service is loaded
* @var ContainerInterface
*/
private $container;

/**
* The service id.
* @var string
*/
private $serviceId;

/**
* The name of a method on the service.
* @var string
*/
private $method;

/**
* Constructor.
*
* @param ContainerInterface $container The service container
* @param string $serviceId The service identifier
* @param string $method The method name
*/
public function __construct(ContainerInterface $container, $serviceId, $method)
{
$this->container = $container;
$this->serviceId = $serviceId;
$this->method = $method;
}

/**
* Retrieves the service from the container and forwards the method call.
*/
public function __invoke(Event $event, $eventName, EventDispatcherInterface $dispatcher)
{
$service = $this->container->get($this->serviceId);
$service->{$this->method}($event, $eventName, $dispatcher);
}

/**
* Returns the container.
*/
public function getContainer()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, you should not have these getters once you add the method returning the callable

{
return $this->container;
}

/**
* Returns the service id.
*/
public function getServiceId()
{
return $this->serviceId;
}

/**
* Returns the method name.
*/
public function getMethod()
{
return $this->method;
}
}
Loading