[Doctrine Bridge] fix priority for doctrine event listeners
This commit is contained in:
parent
c18c93b51c
commit
9d9d4efb88
@ -11,27 +11,30 @@
|
|||||||
|
|
||||||
namespace Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass;
|
namespace Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass;
|
||||||
|
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
|
||||||
use Symfony\Component\DependencyInjection\Reference;
|
|
||||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
|
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||||
|
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
|
||||||
|
use Symfony\Component\DependencyInjection\Reference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers event listeners and subscribers to the available doctrine connections.
|
* Registers event listeners and subscribers to the available doctrine connections.
|
||||||
*
|
*
|
||||||
* @author Jeremy Mikola <jmikola@gmail.com>
|
* @author Jeremy Mikola <jmikola@gmail.com>
|
||||||
* @author Alexander <iam.asm89@gmail.com>
|
* @author Alexander <iam.asm89@gmail.com>
|
||||||
|
* @author David Maicher <mail@dmaicher.de>
|
||||||
*/
|
*/
|
||||||
class RegisterEventListenersAndSubscribersPass implements CompilerPassInterface
|
class RegisterEventListenersAndSubscribersPass implements CompilerPassInterface
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var string|string[]
|
||||||
|
*/
|
||||||
private $connections;
|
private $connections;
|
||||||
private $container;
|
|
||||||
private $eventManagers;
|
private $eventManagers;
|
||||||
private $managerTemplate;
|
private $managerTemplate;
|
||||||
private $tagPrefix;
|
private $tagPrefix;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
|
||||||
*
|
|
||||||
* @param string $connections Parameter ID for connections
|
* @param string $connections Parameter ID for connections
|
||||||
* @param string $managerTemplate sprintf() template for generating the event
|
* @param string $managerTemplate sprintf() template for generating the event
|
||||||
* manager's service ID for a connection name
|
* manager's service ID for a connection name
|
||||||
@ -53,105 +56,112 @@ class RegisterEventListenersAndSubscribersPass implements CompilerPassInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$taggedSubscribers = $container->findTaggedServiceIds($this->tagPrefix.'.event_subscriber');
|
|
||||||
$taggedListeners = $container->findTaggedServiceIds($this->tagPrefix.'.event_listener');
|
|
||||||
|
|
||||||
if (empty($taggedSubscribers) && empty($taggedListeners)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->container = $container;
|
|
||||||
$this->connections = $container->getParameter($this->connections);
|
$this->connections = $container->getParameter($this->connections);
|
||||||
$sortFunc = function ($a, $b) {
|
$this->addTaggedSubscribers($container);
|
||||||
$a = isset($a['priority']) ? $a['priority'] : 0;
|
$this->addTaggedListeners($container);
|
||||||
$b = isset($b['priority']) ? $b['priority'] : 0;
|
}
|
||||||
|
|
||||||
return $a > $b ? -1 : 1;
|
private function addTaggedSubscribers(ContainerBuilder $container)
|
||||||
};
|
{
|
||||||
|
$subscriberTag = $this->tagPrefix.'.event_subscriber';
|
||||||
|
$taggedSubscribers = $this->findAndSortTags($subscriberTag, $container);
|
||||||
|
|
||||||
if (!empty($taggedSubscribers)) {
|
foreach ($taggedSubscribers as $taggedSubscriber) {
|
||||||
$subscribersPerCon = $this->groupByConnection($taggedSubscribers);
|
$id = $taggedSubscriber[0];
|
||||||
foreach ($subscribersPerCon as $con => $subscribers) {
|
$taggedSubscriberDef = $container->getDefinition($id);
|
||||||
$em = $this->getEventManager($con);
|
|
||||||
|
|
||||||
uasort($subscribers, $sortFunc);
|
if ($taggedSubscriberDef->isAbstract()) {
|
||||||
foreach ($subscribers as $id => $instance) {
|
throw new InvalidArgumentException(sprintf('The abstract service "%s" cannot be tagged as a doctrine event subscriber.', $id));
|
||||||
if ($container->getDefinition($id)->isAbstract()) {
|
|
||||||
throw new \InvalidArgumentException(sprintf('The abstract service "%s" cannot be tagged as a doctrine event subscriber.', $id));
|
|
||||||
}
|
|
||||||
|
|
||||||
$em->addMethodCall('addEventSubscriber', array(new Reference($id)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($taggedListeners)) {
|
$tag = $taggedSubscriber[1];
|
||||||
$listenersPerCon = $this->groupByConnection($taggedListeners, true);
|
$connections = isset($tag['connection']) ? array($tag['connection']) : array_keys($this->connections);
|
||||||
foreach ($listenersPerCon as $con => $listeners) {
|
foreach ($connections as $con) {
|
||||||
$em = $this->getEventManager($con);
|
if (!isset($this->connections[$con])) {
|
||||||
|
throw new RuntimeException(sprintf('The Doctrine connection "%s" referenced in service "%s" does not exist. Available connections names: %s', $con, $taggedSubscriber, implode(', ', array_keys($this->connections))));
|
||||||
uasort($listeners, $sortFunc);
|
|
||||||
foreach ($listeners as $id => $instance) {
|
|
||||||
if ($container->getDefinition($id)->isAbstract()) {
|
|
||||||
throw new \InvalidArgumentException(sprintf('The abstract service "%s" cannot be tagged as a doctrine event listener.', $id));
|
|
||||||
}
|
|
||||||
|
|
||||||
$em->addMethodCall('addEventListener', array(
|
|
||||||
array_unique($instance['event']),
|
|
||||||
isset($instance['lazy']) && $instance['lazy'] ? $id : new Reference($id),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->getEventManagerDef($container, $con)->addMethodCall('addEventSubscriber', array(new Reference($id)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function groupByConnection(array $services, $isListener = false)
|
private function addTaggedListeners(ContainerBuilder $container)
|
||||||
{
|
{
|
||||||
$grouped = array();
|
$listenerTag = $this->tagPrefix.'.event_listener';
|
||||||
foreach ($allCons = array_keys($this->connections) as $con) {
|
$taggedListeners = $this->findAndSortTags($listenerTag, $container);
|
||||||
$grouped[$con] = array();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($services as $id => $instances) {
|
foreach ($taggedListeners as $taggedListener) {
|
||||||
foreach ($instances as $instance) {
|
$id = $taggedListener[0];
|
||||||
if ($isListener) {
|
$taggedListenerDef = $container->getDefinition($taggedListener[0]);
|
||||||
if (!isset($instance['event'])) {
|
if ($taggedListenerDef->isAbstract()) {
|
||||||
throw new \InvalidArgumentException(sprintf('Doctrine event listener "%s" must specify the "event" attribute.', $id));
|
throw new InvalidArgumentException(sprintf('The abstract service "%s" cannot be tagged as a doctrine event listener.', $id));
|
||||||
}
|
}
|
||||||
$instance['event'] = array($instance['event']);
|
|
||||||
|
|
||||||
if (isset($instance['lazy']) && $instance['lazy']) {
|
$tag = $taggedListener[1];
|
||||||
$this->container->getDefinition($id)->setPublic(true);
|
if (!isset($tag['event'])) {
|
||||||
}
|
throw new InvalidArgumentException(sprintf('Doctrine event listener "%s" must specify the "event" attribute.', $id));
|
||||||
|
}
|
||||||
|
|
||||||
|
$connections = isset($tag['connection']) ? array($tag['connection']) : array_keys($this->connections);
|
||||||
|
foreach ($connections as $con) {
|
||||||
|
if (!isset($this->connections[$con])) {
|
||||||
|
throw new RuntimeException(sprintf('The Doctrine connection "%s" referenced in service "%s" does not exist. Available connections names: %s', $con, $id, implode(', ', array_keys($this->connections))));
|
||||||
}
|
}
|
||||||
|
|
||||||
$cons = isset($instance['connection']) ? array($instance['connection']) : $allCons;
|
if ($lazy = isset($tag['lazy']) && $tag['lazy']) {
|
||||||
foreach ($cons as $con) {
|
$taggedListenerDef->setPublic(true);
|
||||||
if (!isset($grouped[$con])) {
|
|
||||||
throw new \RuntimeException(sprintf('The Doctrine connection "%s" referenced in service "%s" does not exist. Available connections names: %s', $con, $id, implode(', ', array_keys($this->connections))));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($isListener && isset($grouped[$con][$id])) {
|
|
||||||
$grouped[$con][$id]['event'] = array_merge($grouped[$con][$id]['event'], $instance['event']);
|
|
||||||
} else {
|
|
||||||
$grouped[$con][$id] = $instance;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we add one call per event per service so we have the correct order
|
||||||
|
$this->getEventManagerDef($container, $con)->addMethodCall('addEventListener', array(
|
||||||
|
$tag['event'],
|
||||||
|
$lazy ? $id : new Reference($id),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $grouped;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getEventManager($name)
|
private function getEventManagerDef(ContainerBuilder $container, $name)
|
||||||
{
|
{
|
||||||
if (null === $this->eventManagers) {
|
if (!isset($this->eventManagers[$name])) {
|
||||||
$this->eventManagers = array();
|
$this->eventManagers[$name] = $container->getDefinition(sprintf($this->managerTemplate, $name));
|
||||||
foreach ($this->connections as $n => $id) {
|
|
||||||
$this->eventManagers[$n] = $this->container->getDefinition(sprintf($this->managerTemplate, $n));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->eventManagers[$name];
|
return $this->eventManagers[$name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds and orders all service tags with the given name by their priority.
|
||||||
|
*
|
||||||
|
* The order of additions must be respected for services having the same priority,
|
||||||
|
* and knowing that the \SplPriorityQueue class does not respect the FIFO method,
|
||||||
|
* we should not use this class.
|
||||||
|
*
|
||||||
|
* @see https://bugs.php.net/bug.php?id=53710
|
||||||
|
* @see https://bugs.php.net/bug.php?id=60926
|
||||||
|
*
|
||||||
|
* @param string $tagName
|
||||||
|
* @param ContainerBuilder $container
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function findAndSortTags($tagName, ContainerBuilder $container)
|
||||||
|
{
|
||||||
|
$sortedTags = array();
|
||||||
|
|
||||||
|
foreach ($container->findTaggedServiceIds($tagName) as $serviceId => $tags) {
|
||||||
|
foreach ($tags as $attributes) {
|
||||||
|
$priority = isset($attributes['priority']) ? $attributes['priority'] : 0;
|
||||||
|
$sortedTags[$priority][] = array($serviceId, $attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sortedTags) {
|
||||||
|
krsort($sortedTags);
|
||||||
|
$sortedTags = call_user_func_array('array_merge', $sortedTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sortedTags;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ namespace Symfony\Bridge\Doctrine\Tests\DependencyInjection\CompilerPass;
|
|||||||
use Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass\RegisterEventListenersAndSubscribersPass;
|
use Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass\RegisterEventListenersAndSubscribersPass;
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
use Symfony\Component\DependencyInjection\Definition;
|
use Symfony\Component\DependencyInjection\Definition;
|
||||||
|
use Symfony\Component\DependencyInjection\Reference;
|
||||||
|
|
||||||
class RegisterEventListenersAndSubscribersPassTest extends \PHPUnit_Framework_TestCase
|
class RegisterEventListenersAndSubscribersPassTest extends \PHPUnit_Framework_TestCase
|
||||||
{
|
{
|
||||||
@ -55,12 +56,18 @@ class RegisterEventListenersAndSubscribersPassTest extends \PHPUnit_Framework_Te
|
|||||||
|
|
||||||
$container
|
$container
|
||||||
->register('a', 'stdClass')
|
->register('a', 'stdClass')
|
||||||
|
->setPublic(false)
|
||||||
|
->addTag('doctrine.event_listener', array(
|
||||||
|
'event' => 'bar',
|
||||||
|
))
|
||||||
->addTag('doctrine.event_listener', array(
|
->addTag('doctrine.event_listener', array(
|
||||||
'event' => 'foo',
|
'event' => 'foo',
|
||||||
'priority' => -5,
|
'priority' => -5,
|
||||||
))
|
))
|
||||||
->addTag('doctrine.event_listener', array(
|
->addTag('doctrine.event_listener', array(
|
||||||
'event' => 'bar',
|
'event' => 'foo_bar',
|
||||||
|
'priority' => 3,
|
||||||
|
'lazy' => true,
|
||||||
))
|
))
|
||||||
;
|
;
|
||||||
$container
|
$container
|
||||||
@ -69,12 +76,34 @@ class RegisterEventListenersAndSubscribersPassTest extends \PHPUnit_Framework_Te
|
|||||||
'event' => 'foo',
|
'event' => 'foo',
|
||||||
))
|
))
|
||||||
;
|
;
|
||||||
|
$container
|
||||||
|
->register('c', 'stdClass')
|
||||||
|
->addTag('doctrine.event_listener', array(
|
||||||
|
'event' => 'foo_bar',
|
||||||
|
'priority' => 4,
|
||||||
|
))
|
||||||
|
;
|
||||||
|
|
||||||
$this->process($container);
|
$this->process($container);
|
||||||
$this->assertEquals(array('b', 'a'), $this->getServiceOrder($container, 'addEventListener'));
|
$methodCalls = $container->getDefinition('doctrine.dbal.default_connection.event_manager')->getMethodCalls();
|
||||||
|
|
||||||
$calls = $container->getDefinition('doctrine.dbal.default_connection.event_manager')->getMethodCalls();
|
$this->assertEquals(
|
||||||
$this->assertEquals(array('foo', 'bar'), $calls[1][1][0]);
|
array(
|
||||||
|
array('addEventListener', array('foo_bar', new Reference('c'))),
|
||||||
|
array('addEventListener', array('foo_bar', new Reference('a'))),
|
||||||
|
array('addEventListener', array('bar', new Reference('a'))),
|
||||||
|
array('addEventListener', array('foo', new Reference('b'))),
|
||||||
|
array('addEventListener', array('foo', new Reference('a'))),
|
||||||
|
),
|
||||||
|
$methodCalls
|
||||||
|
);
|
||||||
|
|
||||||
|
// not lazy so must be reference
|
||||||
|
$this->assertInstanceOf('Symfony\Component\DependencyInjection\Reference', $methodCalls[0][1][1]);
|
||||||
|
|
||||||
|
// lazy so id instead of reference and must mark service public
|
||||||
|
$this->assertSame('a', $methodCalls[1][1][1]);
|
||||||
|
$this->assertTrue($container->getDefinition('a')->isPublic());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testProcessEventListenersWithMultipleConnections()
|
public function testProcessEventListenersWithMultipleConnections()
|
||||||
@ -87,15 +116,86 @@ class RegisterEventListenersAndSubscribersPassTest extends \PHPUnit_Framework_Te
|
|||||||
'event' => 'onFlush',
|
'event' => 'onFlush',
|
||||||
))
|
))
|
||||||
;
|
;
|
||||||
|
|
||||||
|
$container
|
||||||
|
->register('b', 'stdClass')
|
||||||
|
->addTag('doctrine.event_listener', array(
|
||||||
|
'event' => 'onFlush',
|
||||||
|
'connection' => 'default',
|
||||||
|
))
|
||||||
|
;
|
||||||
|
|
||||||
|
$container
|
||||||
|
->register('c', 'stdClass')
|
||||||
|
->addTag('doctrine.event_listener', array(
|
||||||
|
'event' => 'onFlush',
|
||||||
|
'connection' => 'second',
|
||||||
|
))
|
||||||
|
;
|
||||||
|
|
||||||
$this->process($container);
|
$this->process($container);
|
||||||
|
|
||||||
$callsDefault = $container->getDefinition('doctrine.dbal.default_connection.event_manager')->getMethodCalls();
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
array('addEventListener', array('onFlush', new Reference('a'))),
|
||||||
|
array('addEventListener', array('onFlush', new Reference('b'))),
|
||||||
|
),
|
||||||
|
$container->getDefinition('doctrine.dbal.default_connection.event_manager')->getMethodCalls()
|
||||||
|
);
|
||||||
|
|
||||||
$this->assertEquals('addEventListener', $callsDefault[0][0]);
|
$this->assertEquals(
|
||||||
$this->assertEquals(array('onFlush'), $callsDefault[0][1][0]);
|
array(
|
||||||
|
array('addEventListener', array('onFlush', new Reference('a'))),
|
||||||
|
array('addEventListener', array('onFlush', new Reference('c'))),
|
||||||
|
),
|
||||||
|
$container->getDefinition('doctrine.dbal.second_connection.event_manager')->getMethodCalls()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$callsSecond = $container->getDefinition('doctrine.dbal.second_connection.event_manager')->getMethodCalls();
|
public function testProcessEventSubscribersWithMultipleConnections()
|
||||||
$this->assertEquals($callsDefault, $callsSecond);
|
{
|
||||||
|
$container = $this->createBuilder(true);
|
||||||
|
|
||||||
|
$container
|
||||||
|
->register('a', 'stdClass')
|
||||||
|
->addTag('doctrine.event_subscriber', array(
|
||||||
|
'event' => 'onFlush',
|
||||||
|
))
|
||||||
|
;
|
||||||
|
|
||||||
|
$container
|
||||||
|
->register('b', 'stdClass')
|
||||||
|
->addTag('doctrine.event_subscriber', array(
|
||||||
|
'event' => 'onFlush',
|
||||||
|
'connection' => 'default',
|
||||||
|
))
|
||||||
|
;
|
||||||
|
|
||||||
|
$container
|
||||||
|
->register('c', 'stdClass')
|
||||||
|
->addTag('doctrine.event_subscriber', array(
|
||||||
|
'event' => 'onFlush',
|
||||||
|
'connection' => 'second',
|
||||||
|
))
|
||||||
|
;
|
||||||
|
|
||||||
|
$this->process($container);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
array('addEventSubscriber', array(new Reference('a'))),
|
||||||
|
array('addEventSubscriber', array(new Reference('b'))),
|
||||||
|
),
|
||||||
|
$container->getDefinition('doctrine.dbal.default_connection.event_manager')->getMethodCalls()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
array('addEventSubscriber', array(new Reference('a'))),
|
||||||
|
array('addEventSubscriber', array(new Reference('c'))),
|
||||||
|
),
|
||||||
|
$container->getDefinition('doctrine.dbal.second_connection.event_manager')->getMethodCalls()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testProcessEventSubscribersWithPriorities()
|
public function testProcessEventSubscribersWithPriorities()
|
||||||
@ -132,11 +232,17 @@ class RegisterEventListenersAndSubscribersPassTest extends \PHPUnit_Framework_Te
|
|||||||
;
|
;
|
||||||
|
|
||||||
$this->process($container);
|
$this->process($container);
|
||||||
$serviceOrder = $this->getServiceOrder($container, 'addEventSubscriber');
|
|
||||||
$unordered = array_splice($serviceOrder, 0, 3);
|
$this->assertEquals(
|
||||||
sort($unordered);
|
array(
|
||||||
$this->assertEquals(array('c', 'd', 'e'), $unordered);
|
array('addEventSubscriber', array(new Reference('c'))),
|
||||||
$this->assertEquals(array('b', 'a'), $serviceOrder);
|
array('addEventSubscriber', array(new Reference('d'))),
|
||||||
|
array('addEventSubscriber', array(new Reference('e'))),
|
||||||
|
array('addEventSubscriber', array(new Reference('b'))),
|
||||||
|
array('addEventSubscriber', array(new Reference('a'))),
|
||||||
|
),
|
||||||
|
$container->getDefinition('doctrine.dbal.default_connection.event_manager')->getMethodCalls()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testProcessNoTaggedServices()
|
public function testProcessNoTaggedServices()
|
||||||
@ -156,26 +262,6 @@ class RegisterEventListenersAndSubscribersPassTest extends \PHPUnit_Framework_Te
|
|||||||
$pass->process($container);
|
$pass->process($container);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getServiceOrder(ContainerBuilder $container, $method)
|
|
||||||
{
|
|
||||||
$order = array();
|
|
||||||
foreach ($container->getDefinition('doctrine.dbal.default_connection.event_manager')->getMethodCalls() as $call) {
|
|
||||||
list($name, $arguments) = $call;
|
|
||||||
if ($method !== $name) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('addEventListener' === $name) {
|
|
||||||
$order[] = (string) $arguments[1];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$order[] = (string) $arguments[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $order;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function createBuilder($multipleConnections = false)
|
private function createBuilder($multipleConnections = false)
|
||||||
{
|
{
|
||||||
$container = new ContainerBuilder();
|
$container = new ContainerBuilder();
|
||||||
|
Reference in New Issue
Block a user