diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php new file mode 100644 index 0000000000000..bf828224435f6 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Messenger; + +use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Messenger\MiddlewareInterface; + +/** + * Wraps all handlers in a single doctrine transaction. + * + * @author Tobias Nyholm + */ +class DoctrineTransactionMiddleware implements MiddlewareInterface +{ + private $managerRegistry; + private $entityManagerName; + + public function __construct(ManagerRegistry $managerRegistry, ?string $entityManagerName) + { + $this->managerRegistry = $managerRegistry; + $this->entityManagerName = $entityManagerName; + } + + public function handle($message, callable $next) + { + $entityManager = $this->managerRegistry->getManager($this->entityManagerName); + + if (!$entityManager instanceof EntityManagerInterface) { + throw new \InvalidArgumentException(sprintf('The ObjectManager with name "%s" must be an instance of EntityManagerInterface', $this->entityManagerName)); + } + + $entityManager->getConnection()->beginTransaction(); + try { + $result = $next($message); + $entityManager->flush(); + $entityManager->getConnection()->commit(); + } catch (\Throwable $exception) { + $entityManager->getConnection()->rollBack(); + + throw $exception; + } + + return $result; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 15df976355909..a835b11428b6a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -969,13 +969,27 @@ private function addMessengerSection(ArrayNodeDefinition $rootNode) ->children() ->arrayNode('routing') ->useAttributeAsKey('message_class') + ->beforeNormalization() + ->always() + ->then(function ($config) { + $newConfig = array(); + foreach ($config as $k => $v) { + if (!is_int($k)) { + $newConfig[$k] = array('senders' => is_array($v) ? array_values($v) : array($v)); + } else { + $newConfig[$v['message-class']]['senders'] = array_map( + function ($a) { + return is_string($a) ? $a : $a['service']; + }, + array_values($v['sender']) + ); + } + } + + return $newConfig; + }) + ->end() ->prototype('array') - ->beforeNormalization() - ->ifString() - ->then(function ($v) { - return array('senders' => array($v)); - }) - ->end() ->children() ->arrayNode('senders') ->requiresAtLeastOneElement() @@ -984,6 +998,16 @@ private function addMessengerSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->end() + ->arrayNode('middlewares') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('doctrine_transaction') + ->canBeEnabled() + ->children() + ->scalarNode('entity_manager_name')->info('The name of the entity manager to use')->defaultNull()->end() + ->end() + ->end() + ->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 6f06557947368..8d6ddd7976fcd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -13,6 +13,7 @@ use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\AnnotationRegistry; +use Symfony\Bridge\Doctrine\Messenger\DoctrineTransactionMiddleware; use Symfony\Bridge\Monolog\Processor\DebugProcessor; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\Controller; @@ -1445,6 +1446,15 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $container->getDefinition('messenger.sender_locator')->replaceArgument(0, $senderLocatorMapping); $container->getDefinition('messenger.asynchronous.routing.sender_locator')->replaceArgument(1, $messageToSenderIdsMapping); + + if ($config['middlewares']['doctrine_transaction']['enabled']) { + if (!class_exists(DoctrineTransactionMiddleware::class)) { + throw new LogicException('The Doctrine transaction middleware is only available when the doctrine bridge is installed. Try running "composer require symfony/doctrine-bridge".'); + } + $container->getDefinition('messenger.middleware.doctrine_transaction')->replaceArgument(1, $config['middlewares']['doctrine_transaction']['entity_manager_name']); + } else { + $container->removeDefinition('messenger.middleware.doctrine_transaction'); + } } private function registerCacheConfiguration(array $config, ContainerBuilder $container) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml index 49bbcfff6804e..cbba149b2fb1f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml @@ -25,6 +25,13 @@ + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index d8ef61bbb456b..b3f4d27f4720a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -30,6 +30,7 @@ + @@ -342,4 +343,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 9f9e13317d7db..6fc0ebd1cf8c2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -253,6 +253,12 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'messenger' => array( 'enabled' => !class_exists(FullStack::class) && class_exists(MessageBusInterface::class), 'routing' => array(), + 'middlewares' => array( + 'doctrine_transaction' => array( + 'enabled' => false, + 'entity_manager_name' => null, + ), + ), ), ); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger.php new file mode 100644 index 0000000000000..03cf020f041dd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger.php @@ -0,0 +1,10 @@ +loadFromExtension('framework', array( + 'messenger' => array( + 'routing' => array( + 'App\Bar' => array('sender.bar', 'sender.biz'), + 'App\Foo' => 'sender.foo', + ), + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_doctrine.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_doctrine.php new file mode 100644 index 0000000000000..1295236717aef --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_doctrine.php @@ -0,0 +1,11 @@ +loadFromExtension('framework', array( + 'messenger' => array( + 'middlewares' => array( + 'doctrine_transaction' => array( + 'entity_manager_name' => 'foobar', + ), + ), + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger.xml new file mode 100644 index 0000000000000..1ecdc09bfbf10 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_doctrine.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_doctrine.xml new file mode 100644 index 0000000000000..8d9d8850f5c4c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_doctrine.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger.yml new file mode 100644 index 0000000000000..4f921514c4ec7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger.yml @@ -0,0 +1,5 @@ +framework: + messenger: + routing: + 'App\Bar': ['sender.bar', 'sender.biz'] + 'App\Foo': 'sender.foo' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_doctrine.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_doctrine.yml new file mode 100644 index 0000000000000..346fae558bd24 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_doctrine.yml @@ -0,0 +1,5 @@ +framework: + messenger: + middlewares: + doctrine_transaction: + entity_manager_name: 'foobar' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index d49111c65ced7..d130db0eec4ec 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; use Doctrine\Common\Annotations\Annotation; +use Symfony\Bridge\Doctrine\ContainerAwareEventManager; use Symfony\Bundle\FullStack; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddAnnotationsCachedReaderPass; @@ -489,6 +490,24 @@ public function testWebLink() $this->assertTrue($container->hasDefinition('web_link.add_link_header_listener')); } + public function testMessenger() + { + $container = $this->createContainerFromFile('messenger'); + $this->assertFalse($container->hasDefinition('messenger.middleware.doctrine_transaction')); + } + + public function testMessengerDoctrine() + { + if (!class_exists(ContainerAwareEventManager::class)) { + self::markTestSkipped('Skipping tests since Doctrine bridge is not installed'); + } + + $container = $this->createContainerFromFile('messenger_doctrine'); + $this->assertTrue($container->hasDefinition('messenger.middleware.doctrine_transaction')); + $def = $container->getDefinition('messenger.middleware.doctrine_transaction'); + $this->assertEquals('foobar', $def->getArgument(1)); + } + public function testTranslator() { $container = $this->createContainerFromFile('full');