diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index a7421573e0f24..8d0a2dd604c77 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -1313,6 +1313,10 @@ function ($a) {
->prototype('variable')
->end()
->end()
+ ->scalarNode('failure_transport')
+ ->defaultNull()
+ ->info('Transport name to send failed messages to (after all retries have failed).')
+ ->end()
->arrayNode('retry_strategy')
->addDefaultsIfNotSet()
->beforeNormalization()
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 4b3e4427a0cba..375c698f1af1e 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -1915,15 +1915,38 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder
$container->setAlias('messenger.default_serializer', $config['serializer']['default_serializer']);
}
+ $failureTransports = [];
+ if ($config['failure_transport']) {
+ if (!isset($config['transports'][$config['failure_transport']])) {
+ throw new LogicException(sprintf('Invalid Messenger configuration: the failure transport "%s" is not a valid transport or service id.', $config['failure_transport']));
+ }
+
+ $container->setAlias('messenger.failure_transports.default', 'messenger.transport.'.$config['failure_transport']);
+ $failureTransports[] = $config['failure_transport'];
+ }
+
+ $failureTransportsByName = [];
+ foreach ($config['transports'] as $name => $transport) {
+ if ($transport['failure_transport']) {
+ $failureTransports[] = $transport['failure_transport'];
+ $failureTransportsByName[$name] = $transport['failure_transport'];
+ } elseif ($config['failure_transport']) {
+ $failureTransportsByName[$name] = $config['failure_transport'];
+ }
+ }
+
$senderAliases = [];
$transportRetryReferences = [];
foreach ($config['transports'] as $name => $transport) {
$serializerId = $transport['serializer'] ?? 'messenger.default_serializer';
-
$transportDefinition = (new Definition(TransportInterface::class))
->setFactory([new Reference('messenger.transport_factory'), 'createTransport'])
->setArguments([$transport['dsn'], $transport['options'] + ['transport_name' => $name], new Reference($serializerId)])
- ->addTag('messenger.receiver', ['alias' => $name])
+ ->addTag('messenger.receiver', [
+ 'alias' => $name,
+ 'is_failure_transport' => \in_array($name, $failureTransports),
+ ]
+ )
;
$container->setDefinition($transportId = 'messenger.transport.'.$name, $transportDefinition);
$senderAliases[$name] = $transportId;
@@ -1954,6 +1977,18 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder
$senderReferences[$serviceId] = new Reference($serviceId);
}
+ foreach ($config['transports'] as $name => $transport) {
+ if ($transport['failure_transport']) {
+ if (!isset($senderReferences[$transport['failure_transport']])) {
+ throw new LogicException(sprintf('Invalid Messenger configuration: the failure transport "%s" is not a valid transport or service id.', $transport['failure_transport']));
+ }
+ }
+ }
+
+ $failureTransportReferencesByTransportName = array_map(function ($failureTransportName) use ($senderReferences) {
+ return $senderReferences[$failureTransportName];
+ }, $failureTransportsByName);
+
$messageToSendersMapping = [];
foreach ($config['routing'] as $message => $messageConfiguration) {
if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) {
@@ -1984,19 +2019,17 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder
$container->getDefinition('messenger.retry_strategy_locator')
->replaceArgument(0, $transportRetryReferences);
- if ($config['failure_transport']) {
- if (!isset($senderReferences[$config['failure_transport']])) {
- throw new LogicException(sprintf('Invalid Messenger configuration: the failure transport "%s" is not a valid transport or service id.', $config['failure_transport']));
- }
-
- $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')
- ->replaceArgument(0, $senderReferences[$config['failure_transport']]);
+ if (\count($failureTransports) > 0) {
$container->getDefinition('console.command.messenger_failed_messages_retry')
->replaceArgument(0, $config['failure_transport']);
$container->getDefinition('console.command.messenger_failed_messages_show')
->replaceArgument(0, $config['failure_transport']);
$container->getDefinition('console.command.messenger_failed_messages_remove')
->replaceArgument(0, $config['failure_transport']);
+
+ $failureTransportsByTransportNameServiceLocator = ServiceLocatorTagPass::register($container, $failureTransportReferencesByTransportName);
+ $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')
+ ->replaceArgument(0, $failureTransportsByTransportNameServiceLocator);
} else {
$container->removeDefinition('messenger.failure.send_failed_message_to_failure_transport_listener');
$container->removeDefinition('console.command.messenger_failed_messages_retry');
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php
index 5aea86ba7a06b..1c05d8760e614 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php
@@ -165,8 +165,8 @@
->set('console.command.messenger_failed_messages_retry', FailedMessagesRetryCommand::class)
->args([
- abstract_arg('Receiver name'),
- abstract_arg('Receiver'),
+ abstract_arg('Default failure receiver name'),
+ abstract_arg('Receivers'),
service('messenger.routable_message_bus'),
service('event_dispatcher'),
service('logger'),
@@ -175,15 +175,15 @@
->set('console.command.messenger_failed_messages_show', FailedMessagesShowCommand::class)
->args([
- abstract_arg('Receiver name'),
- abstract_arg('Receiver'),
+ abstract_arg('Default failure receiver name'),
+ abstract_arg('Receivers'),
])
->tag('console.command')
->set('console.command.messenger_failed_messages_remove', FailedMessagesRemoveCommand::class)
->args([
- abstract_arg('Receiver name'),
- abstract_arg('Receiver'),
+ abstract_arg('Default failure receiver name'),
+ abstract_arg('Receivers'),
])
->tag('console.command')
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php
index a7d993d47e316..5d563c92e7152 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php
@@ -172,7 +172,7 @@
->set('messenger.failure.send_failed_message_to_failure_transport_listener', SendFailedMessageToFailureTransportListener::class)
->args([
- abstract_arg('failure transport'),
+ abstract_arg('failure transports'),
service('logger')->ignoreOnInvalid(),
])
->tag('kernel.event_subscriber')
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 1027c3c6f9878..2d33aae50fe25 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
@@ -491,6 +491,7 @@
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports.php
new file mode 100644
index 0000000000000..8f85259aa6908
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports.php
@@ -0,0 +1,19 @@
+loadFromExtension('framework', [
+ 'messenger' => [
+ 'transports' => [
+ 'transport_1' => [
+ 'dsn' => 'null://',
+ 'failure_transport' => 'failure_transport_1'
+ ],
+ 'transport_2' => 'null://',
+ 'transport_3' => [
+ 'dsn' => 'null://',
+ 'failure_transport' => 'failure_transport_3'
+ ],
+ 'failure_transport_1' => 'null://',
+ 'failure_transport_3' => 'null://'
+ ],
+ ],
+]);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports_global.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports_global.php
new file mode 100644
index 0000000000000..0cff76887b152
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports_global.php
@@ -0,0 +1,21 @@
+loadFromExtension('framework', [
+ 'messenger' => [
+ 'failure_transport' => 'failure_transport_global',
+ 'transports' => [
+ 'transport_1' => [
+ 'dsn' => 'null://',
+ 'failure_transport' => 'failure_transport_1'
+ ],
+ 'transport_2' => 'null://',
+ 'transport_3' => [
+ 'dsn' => 'null://',
+ 'failure_transport' => 'failure_transport_3'
+ ],
+ 'failure_transport_global' => 'null://',
+ 'failure_transport_1' => 'null://',
+ 'failure_transport_3' => 'null://',
+ ],
+ ],
+]);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports.xml
new file mode 100644
index 0000000000000..b8e9f19759429
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports_global.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports_global.xml
new file mode 100644
index 0000000000000..c6e5c530fda1b
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports_global.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports.yml
new file mode 100644
index 0000000000000..863f18a7d1a1f
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports.yml
@@ -0,0 +1,12 @@
+framework:
+ messenger:
+ transports:
+ transport_1:
+ dsn: 'null://'
+ failure_transport: failure_transport_1
+ transport_2: 'null://'
+ transport_3:
+ dsn: 'null://'
+ failure_transport: failure_transport_3
+ failure_transport_1: 'null://'
+ failure_transport_3: 'null://'
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports_global.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports_global.yml
new file mode 100644
index 0000000000000..10023edb0b9fd
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports_global.yml
@@ -0,0 +1,14 @@
+framework:
+ messenger:
+ failure_transport: failure_transport_global
+ transports:
+ transport_1:
+ dsn: 'null://'
+ failure_transport: failure_transport_1
+ transport_2: 'null://'
+ transport_3:
+ dsn: 'null://'
+ failure_transport: failure_transport_3
+ failure_transport_global: 'null://'
+ failure_transport_1: 'null://'
+ failure_transport_3: 'null://'
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
index 9d5d1aa9fb544..531d368b27baa 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
@@ -32,6 +32,7 @@
use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
use Symfony\Component\Cache\DependencyInjection\CachePoolPass;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
+use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass;
@@ -710,12 +711,92 @@ public function testMessenger()
$this->assertSame(TransportFactory::class, $container->getDefinition('messenger.transport_factory')->getClass());
}
+ public function testMessengerMultipleFailureTransports()
+ {
+ $container = $this->createContainerFromFile('messenger_multiple_failure_transports');
+
+ $failureTransport1Definition = $container->getDefinition('messenger.transport.failure_transport_1');
+ $failureTransport1Tags = $failureTransport1Definition->getTag('messenger.receiver')[0];
+
+ $this->assertEquals([
+ 'alias' => 'failure_transport_1',
+ 'is_failure_transport' => true,
+ ], $failureTransport1Tags);
+
+ $failureTransport3Definition = $container->getDefinition('messenger.transport.failure_transport_3');
+ $failureTransport3Tags = $failureTransport3Definition->getTag('messenger.receiver')[0];
+
+ $this->assertEquals([
+ 'alias' => 'failure_transport_3',
+ 'is_failure_transport' => true,
+ ], $failureTransport3Tags);
+
+ // transport 2 exists but does not appear in the mapping
+ $this->assertFalse($container->hasDefinition('messenger.transport.failure_transport_2'));
+
+ $failureTransportsByTransportNameServiceLocator = $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')->getArgument(0);
+ $failureTransports = $container->getDefinition((string) $failureTransportsByTransportNameServiceLocator)->getArgument(0);
+ $expectedTransportsByFailureTransports = [
+ 'transport_1' => new Reference('messenger.transport.failure_transport_1'),
+ 'transport_3' => new Reference('messenger.transport.failure_transport_3'),
+ ];
+
+ $failureTransportsReferences = array_map(function (ServiceClosureArgument $serviceClosureArgument) {
+ $values = $serviceClosureArgument->getValues();
+
+ return array_shift($values);
+ }, $failureTransports);
+ $this->assertEquals($expectedTransportsByFailureTransports, $failureTransportsReferences);
+ }
+
+ public function testMessengerMultipleFailureTransportsWithGlobalFailureTransport()
+ {
+ $container = $this->createContainerFromFile('messenger_multiple_failure_transports_global');
+
+ $this->assertEquals('messenger.transport.failure_transport_global', (string) $container->getAlias('messenger.failure_transports.default'));
+
+ $failureTransport1Definition = $container->getDefinition('messenger.transport.failure_transport_1');
+ $failureTransport1Tags = $failureTransport1Definition->getTag('messenger.receiver')[0];
+
+ $this->assertEquals([
+ 'alias' => 'failure_transport_1',
+ 'is_failure_transport' => true,
+ ], $failureTransport1Tags);
+
+ $failureTransport3Definition = $container->getDefinition('messenger.transport.failure_transport_3');
+ $failureTransport3Tags = $failureTransport3Definition->getTag('messenger.receiver')[0];
+
+ $this->assertEquals([
+ 'alias' => 'failure_transport_3',
+ 'is_failure_transport' => true,
+ ], $failureTransport3Tags);
+
+ $failureTransportsByTransportNameServiceLocator = $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')->getArgument(0);
+ $failureTransports = $container->getDefinition((string) $failureTransportsByTransportNameServiceLocator)->getArgument(0);
+ $expectedTransportsByFailureTransports = [
+ 'failure_transport_1' => new Reference('messenger.transport.failure_transport_global'),
+ 'failure_transport_3' => new Reference('messenger.transport.failure_transport_global'),
+ 'failure_transport_global' => new Reference('messenger.transport.failure_transport_global'),
+ 'transport_1' => new Reference('messenger.transport.failure_transport_1'),
+ 'transport_2' => new Reference('messenger.transport.failure_transport_global'),
+ 'transport_3' => new Reference('messenger.transport.failure_transport_3'),
+ ];
+
+ $failureTransportsReferences = array_map(function (ServiceClosureArgument $serviceClosureArgument) {
+ $values = $serviceClosureArgument->getValues();
+
+ return array_shift($values);
+ }, $failureTransports);
+ $this->assertEquals($expectedTransportsByFailureTransports, $failureTransportsReferences);
+ }
+
public function testMessengerTransports()
{
$container = $this->createContainerFromFile('messenger_transports');
$this->assertTrue($container->hasDefinition('messenger.transport.default'));
$this->assertTrue($container->getDefinition('messenger.transport.default')->hasTag('messenger.receiver'));
- $this->assertEquals([['alias' => 'default']], $container->getDefinition('messenger.transport.default')->getTag('messenger.receiver'));
+ $this->assertEquals([
+ ['alias' => 'default', 'is_failure_transport' => false], ], $container->getDefinition('messenger.transport.default')->getTag('messenger.receiver'));
$transportArguments = $container->getDefinition('messenger.transport.default')->getArguments();
$this->assertEquals(new Reference('messenger.default_serializer'), $transportArguments[2]);
@@ -756,7 +837,22 @@ public function testMessengerTransports()
$this->assertSame(3, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(2));
$this->assertSame(100, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(3));
- $this->assertEquals(new Reference('messenger.transport.failed'), $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')->getArgument(0));
+ $failureTransportsByTransportNameServiceLocator = $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')->getArgument(0);
+ $failureTransports = $container->getDefinition((string) $failureTransportsByTransportNameServiceLocator)->getArgument(0);
+ $expectedTransportsByFailureTransports = [
+ 'beanstalkd' => new Reference('messenger.transport.failed'),
+ 'customised' => new Reference('messenger.transport.failed'),
+ 'default' => new Reference('messenger.transport.failed'),
+ 'failed' => new Reference('messenger.transport.failed'),
+ 'redis' => new Reference('messenger.transport.failed'),
+ ];
+
+ $failureTransportsReferences = array_map(function (ServiceClosureArgument $serviceClosureArgument) {
+ $values = $serviceClosureArgument->getValues();
+
+ return array_shift($values);
+ }, $failureTransports);
+ $this->assertEquals($expectedTransportsByFailureTransports, $failureTransportsReferences);
}
public function testMessengerRouting()
diff --git a/src/Symfony/Component/Messenger/Command/AbstractFailedMessagesCommand.php b/src/Symfony/Component/Messenger/Command/AbstractFailedMessagesCommand.php
index 77a6700500eed..0591ecbda990f 100644
--- a/src/Symfony/Component/Messenger/Command/AbstractFailedMessagesCommand.php
+++ b/src/Symfony/Component/Messenger/Command/AbstractFailedMessagesCommand.php
@@ -13,9 +13,12 @@
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Dumper;
+use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Messenger\Envelope;
+use Symfony\Component\Messenger\Exception\InvalidArgumentException;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
@@ -35,20 +38,36 @@
*/
abstract class AbstractFailedMessagesCommand extends Command
{
- private $receiverName;
- private $receiver;
+ private $globalFailureReceiverName;
+ protected $failureTransports;
- public function __construct(string $receiverName, ReceiverInterface $receiver)
+ public function __construct(?string $globalFailureReceiverName, $failureTransports)
{
- $this->receiverName = $receiverName;
- $this->receiver = $receiver;
+ $this->failureTransports = $failureTransports;
+ if (!$failureTransports instanceof ServiceLocator) {
+ trigger_deprecation('symfony/messenger', '5.3', 'Passing a non-scalar value as 2nd argument to "%s()" is deprecated, pass a ServiceLocator instead.', __METHOD__);
+
+ if (null === $globalFailureReceiverName) {
+ throw new InvalidArgumentException(sprintf('The argument "globalFailureReceiver" from method "%s()" must be not null if 2nd argument is not a ServiceLocator.', __METHOD__));
+ }
+
+ $this->failureTransports = new ServiceLocator([$globalFailureReceiverName => function () use ($failureTransports) { return $failureTransports; },]);
+ }
+ $this->globalFailureReceiverName = $globalFailureReceiverName;
parent::__construct();
}
protected function getReceiverName(): string
{
- return $this->receiverName;
+ trigger_deprecation('symfony/messenger', '5.3', 'The method "%s()" is deprecated, use getGlobalFailureReceiverName() instead.', __METHOD__);
+
+ return $this->globalFailureReceiverName;
+ }
+
+ protected function getGlobalFailureReceiverName(): ?string
+ {
+ return $this->globalFailureReceiverName;
}
/**
@@ -153,9 +172,18 @@ protected function printPendingMessagesMessage(ReceiverInterface $receiver, Symf
}
}
- protected function getReceiver(): ReceiverInterface
+ protected function getReceiver(/* string $name */): ReceiverInterface
{
- return $this->receiver;
+ if (1 > \func_num_args() && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) {
+ @trigger_error(sprintf('The "%s()" method will have a new "string $name" argument in version 5.3, not defining it is deprecated since Symfony 5.2.', __METHOD__), \E_USER_DEPRECATED);
+ }
+
+ $name = \func_num_args() > 0 ? func_get_arg(0) : $this->globalFailureReceiverName;
+ if (!$this->failureTransports->has($name)) {
+ throw new InvalidArgumentException(sprintf('The failure transport with name "%s" was not found. Available transports are: "%s".', $name, implode(', ', array_keys($this->failureTransports->getProvidedServices()))));
+ }
+
+ return $this->failureTransports->get($name);
}
protected function getLastRedeliveryStampWithException(Envelope $envelope): ?RedeliveryStamp
@@ -200,4 +228,27 @@ private function createCloner(): ?ClonerInterface
return $cloner;
}
+
+ protected function printWarningAvailableFailureTransports(SymfonyStyle $io, ?string $failureTransportName): void
+ {
+ $failureTransports = array_keys($this->failureTransports->getProvidedServices());
+ $failureTransportsCount = \count($failureTransports);
+ if ($failureTransportsCount > 1) {
+ $io->writeln([
+ sprintf('> Loading messages from the global failure transport %s.', $failureTransportName),
+ '> To use a different failure transport, pass --transport=.',
+ sprintf('> Available failure transports are: %s', implode(', ', $failureTransports)),
+ "\n",
+ ]);
+ }
+ }
+
+ protected function interactiveChooseFailureTransport(SymfonyStyle $io)
+ {
+ $failedTransports = array_keys($this->failureTransports->getProvidedServices());
+ $question = new ChoiceQuestion('Select failed transport:', $failedTransports, 0);
+ $question->setMultiselect(false);
+
+ return $io->askQuestion($question);
+ }
}
diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php
index df21b9bcd4f5a..45bec7eb89267 100644
--- a/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php
+++ b/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php
@@ -38,6 +38,7 @@ protected function configure(): void
->setDefinition([
new InputArgument('id', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'Specific message id(s) to remove'),
new InputOption('force', null, InputOption::VALUE_NONE, 'Force the operation without confirmation'),
+ new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport'),
new InputOption('show-messages', null, InputOption::VALUE_NONE, 'Display messages before removing it (if multiple ids are given)'),
])
->setDescription(self::$defaultDescription)
@@ -59,20 +60,25 @@ protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
- $receiver = $this->getReceiver();
+ $failureTransportName = $input->getOption('transport');
+ if (null === $failureTransportName) {
+ $failureTransportName = $this->getGlobalFailureReceiverName();
+ }
+
+ $receiver = $this->getReceiver($failureTransportName);
$shouldForce = $input->getOption('force');
$ids = (array) $input->getArgument('id');
$shouldDisplayMessages = $input->getOption('show-messages') || 1 === \count($ids);
- $this->removeMessages($ids, $receiver, $io, $shouldForce, $shouldDisplayMessages);
+ $this->removeMessages($failureTransportName, $ids, $receiver, $io, $shouldForce, $shouldDisplayMessages);
return 0;
}
- private function removeMessages(array $ids, ReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce, bool $shouldDisplayMessages): void
+ private function removeMessages(string $failureTransportName, array $ids, ReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce, bool $shouldDisplayMessages): void
{
if (!$receiver instanceof ListableReceiverInterface) {
- throw new RuntimeException(sprintf('The "%s" receiver does not support removing specific messages.', $this->getReceiverName()));
+ throw new RuntimeException(sprintf('The "%s" receiver does not support removing specific messages.', $failureTransportName));
}
foreach ($ids as $id) {
diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php
index 0e75c8f7c5a86..740b5dbe6638c 100644
--- a/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php
+++ b/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php
@@ -34,6 +34,8 @@
*/
class FailedMessagesRetryCommand extends AbstractFailedMessagesCommand
{
+ private const DEFAULT_TRANSPORT_OPTION = 'choose';
+
protected static $defaultName = 'messenger:failed:retry';
protected static $defaultDescription = 'Retry one or more messages from the failure transport';
@@ -41,13 +43,13 @@ class FailedMessagesRetryCommand extends AbstractFailedMessagesCommand
private $messageBus;
private $logger;
- public function __construct(string $receiverName, ReceiverInterface $receiver, MessageBusInterface $messageBus, EventDispatcherInterface $eventDispatcher, LoggerInterface $logger = null)
+ public function __construct(?string $globalReceiverName, $failureTransports, MessageBusInterface $messageBus, EventDispatcherInterface $eventDispatcher, LoggerInterface $logger = null)
{
$this->eventDispatcher = $eventDispatcher;
$this->messageBus = $messageBus;
$this->logger = $logger;
- parent::__construct($receiverName, $receiver);
+ parent::__construct($globalReceiverName, $failureTransports);
}
/**
@@ -59,6 +61,7 @@ protected function configure(): void
->setDefinition([
new InputArgument('id', InputArgument::IS_ARRAY, 'Specific message id(s) to retry'),
new InputOption('force', null, InputOption::VALUE_NONE, 'Force action without confirmation'),
+ new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
])
->setDescription(self::$defaultDescription)
->setHelp(<<<'EOF'
@@ -96,10 +99,19 @@ protected function execute(InputInterface $input, OutputInterface $output)
$io->comment('Re-run the command with a -vv option to see logs about consumed messages.');
}
- $receiver = $this->getReceiver();
+ $failureTransportName = $input->getOption('transport');
+ if (self::DEFAULT_TRANSPORT_OPTION === $failureTransportName) {
+ $this->printWarningAvailableFailureTransports($io, $this->getGlobalFailureReceiverName());
+ }
+ if ('' === $failureTransportName || null === $failureTransportName) {
+ $failureTransportName = $this->interactiveChooseFailureTransport($io);
+ }
+ $failureTransportName = self::DEFAULT_TRANSPORT_OPTION === $failureTransportName ? $this->getGlobalFailureReceiverName() : $failureTransportName;
+
+ $receiver = $this->getReceiver($failureTransportName);
$this->printPendingMessagesMessage($receiver, $io);
- $io->writeln(sprintf('To retry all the messages, run messenger:consume %s', $this->getReceiverName()));
+ $io->writeln(sprintf('To retry all the messages, run messenger:consume %s', $failureTransportName));
$shouldForce = $input->getOption('force');
$ids = $input->getArgument('id');
@@ -108,20 +120,20 @@ protected function execute(InputInterface $input, OutputInterface $output)
throw new RuntimeException('Message id must be passed when in non-interactive mode.');
}
- $this->runInteractive($io, $shouldForce);
+ $this->runInteractive($failureTransportName, $io, $shouldForce);
return 0;
}
- $this->retrySpecificIds($ids, $io, $shouldForce);
+ $this->retrySpecificIds($failureTransportName, $ids, $io, $shouldForce);
$io->success('All done!');
return 0;
}
- private function runInteractive(SymfonyStyle $io, bool $shouldForce)
+ private function runInteractive(string $failureTransportName, SymfonyStyle $io, bool $shouldForce)
{
- $receiver = $this->getReceiver();
+ $receiver = $this->failureTransports->get($failureTransportName);
$count = 0;
if ($receiver instanceof ListableReceiverInterface) {
// for listable receivers, find the messages one-by-one
@@ -136,7 +148,7 @@ private function runInteractive(SymfonyStyle $io, bool $shouldForce)
$id = $this->getMessageId($envelope);
if (null === $id) {
- throw new LogicException(sprintf('The "%s" receiver is able to list messages by id but the envelope is missing the TransportMessageIdStamp stamp.', $this->getReceiverName()));
+ throw new LogicException(sprintf('The "%s" receiver is able to list messages by id but the envelope is missing the TransportMessageIdStamp stamp.', $failureTransportName));
}
$ids[] = $id;
}
@@ -146,11 +158,11 @@ private function runInteractive(SymfonyStyle $io, bool $shouldForce)
break;
}
- $this->retrySpecificIds($ids, $io, $shouldForce);
+ $this->retrySpecificIds($failureTransportName, $ids, $io, $shouldForce);
}
} else {
// get() and ask messages one-by-one
- $count = $this->runWorker($this->getReceiver(), $io, $shouldForce);
+ $count = $this->runWorker($failureTransportName, $receiver, $io, $shouldForce);
}
// avoid success message if nothing was processed
@@ -159,7 +171,7 @@ private function runInteractive(SymfonyStyle $io, bool $shouldForce)
}
}
- private function runWorker(ReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce): int
+ private function runWorker(string $failureTransportName, ReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce): int
{
$count = 0;
$listener = function (WorkerMessageReceivedEvent $messageReceivedEvent) use ($io, $receiver, $shouldForce, &$count) {
@@ -180,7 +192,7 @@ private function runWorker(ReceiverInterface $receiver, SymfonyStyle $io, bool $
$this->eventDispatcher->addListener(WorkerMessageReceivedEvent::class, $listener);
$worker = new Worker(
- [$this->getReceiverName() => $receiver],
+ [$failureTransportName => $receiver],
$this->messageBus,
$this->eventDispatcher,
$this->logger
@@ -195,12 +207,12 @@ private function runWorker(ReceiverInterface $receiver, SymfonyStyle $io, bool $
return $count;
}
- private function retrySpecificIds(array $ids, SymfonyStyle $io, bool $shouldForce)
+ private function retrySpecificIds(string $failureTransportName, array $ids, SymfonyStyle $io, bool $shouldForce)
{
- $receiver = $this->getReceiver();
+ $receiver = $this->getReceiver($failureTransportName);
if (!$receiver instanceof ListableReceiverInterface) {
- throw new RuntimeException(sprintf('The "%s" receiver does not support retrying messages by id.', $this->getReceiverName()));
+ throw new RuntimeException(sprintf('The "%s" receiver does not support retrying messages by id.', $failureTransportName));
}
foreach ($ids as $id) {
@@ -210,7 +222,7 @@ private function retrySpecificIds(array $ids, SymfonyStyle $io, bool $shouldForc
}
$singleReceiver = new SingleMessageReceiver($receiver, $envelope);
- $this->runWorker($singleReceiver, $io, $shouldForce);
+ $this->runWorker($failureTransportName, $singleReceiver, $io, $shouldForce);
}
}
}
diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php
index 9e8f23d759208..c292549e3a9c4 100644
--- a/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php
+++ b/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php
@@ -27,6 +27,8 @@
*/
class FailedMessagesShowCommand extends AbstractFailedMessagesCommand
{
+ private const DEFAULT_TRANSPORT_OPTION = 'choose';
+
protected static $defaultName = 'messenger:failed:show';
protected static $defaultDescription = 'Show one or more messages from the failure transport';
@@ -39,6 +41,7 @@ protected function configure(): void
->setDefinition([
new InputArgument('id', InputArgument::OPTIONAL, 'Specific message id to show'),
new InputOption('max', null, InputOption::VALUE_REQUIRED, 'Maximum number of messages to list', 50),
+ new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
])
->setDescription(self::$defaultDescription)
->setHelp(<<<'EOF'
@@ -61,26 +64,36 @@ protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
- $receiver = $this->getReceiver();
+ $failureTransportName = $input->getOption('transport');
+ if (self::DEFAULT_TRANSPORT_OPTION === $failureTransportName) {
+ $this->printWarningAvailableFailureTransports($io, $this->getGlobalFailureReceiverName());
+ }
+ if ('' === $failureTransportName || null === $failureTransportName) {
+ $failureTransportName = $this->interactiveChooseFailureTransport($io);
+ }
+ $failureTransportName = self::DEFAULT_TRANSPORT_OPTION === $failureTransportName ? $this->getGlobalFailureReceiverName() : $failureTransportName;
+
+ $receiver = $this->getReceiver($failureTransportName);
+
$this->printPendingMessagesMessage($receiver, $io);
if (!$receiver instanceof ListableReceiverInterface) {
- throw new RuntimeException(sprintf('The "%s" receiver does not support listing or showing specific messages.', $this->getReceiverName()));
+ throw new RuntimeException(sprintf('The "%s" receiver does not support listing or showing specific messages.', $failureTransportName));
}
if (null === $id = $input->getArgument('id')) {
- $this->listMessages($io, $input->getOption('max'));
+ $this->listMessages($failureTransportName, $io, $input->getOption('max'));
} else {
- $this->showMessage($id, $io);
+ $this->showMessage($failureTransportName, $id, $io);
}
return 0;
}
- private function listMessages(SymfonyStyle $io, int $max)
+ private function listMessages(?string $failedTransportName, SymfonyStyle $io, int $max)
{
/** @var ListableReceiverInterface $receiver */
- $receiver = $this->getReceiver();
+ $receiver = $this->getReceiver($failedTransportName);
$envelopes = $receiver->all($max);
$rows = [];
@@ -119,13 +132,13 @@ private function listMessages(SymfonyStyle $io, int $max)
$io->comment(sprintf('Showing first %d messages.', $max));
}
- $io->comment('Run messenger:failed:show {id} -vv to see message details.');
+ $io->comment(sprintf('Run messenger:failed:show {id} --transport=%s -vv to see message details.', $failedTransportName));
}
- private function showMessage(string $id, SymfonyStyle $io)
+ private function showMessage(?string $failedTransportName, string $id, SymfonyStyle $io)
{
/** @var ListableReceiverInterface $receiver */
- $receiver = $this->getReceiver();
+ $receiver = $this->getReceiver($failedTransportName);
$envelope = $receiver->find($id);
if (null === $envelope) {
throw new RuntimeException(sprintf('The message "%s" was not found.', $id));
@@ -135,8 +148,8 @@ private function showMessage(string $id, SymfonyStyle $io)
$io->writeln([
'',
- sprintf(' Run messenger:failed:retry %s to retry this message.', $id),
- sprintf(' Run messenger:failed:remove %s to delete it.', $id),
+ sprintf(' Run messenger:failed:retry %s --transport=%s to retry this message.', $id, $failedTransportName),
+ sprintf(' Run messenger:failed:remove %s --transport=%s to delete it.', $id, $failedTransportName),
]);
}
}
diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php
index 5b77326270371..1a64dac475b02 100644
--- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php
+++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php
@@ -125,7 +125,7 @@ private function registerHandlers(ContainerBuilder $container, array $busIds)
if (!\in_array($options['bus'], $busIds)) {
$messageLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : ($r->implementsInterface(MessageSubscriberInterface::class) ? sprintf('returned by method "%s::getHandledMessages()"', $r->getName()) : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method));
- throw new RuntimeException(sprintf('Invalid configuration %s for message "%s": bus "%s" does not exist.', $messageLocation, $message, $options['bus']));
+ throw new RuntimeException(sprintf('Invalid configuration "%s" for message "%s": bus "%s" does not exist.', $messageLocation, $message, $options['bus']));
}
$buses = [$options['bus']];
@@ -134,7 +134,7 @@ private function registerHandlers(ContainerBuilder $container, array $busIds)
if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) {
$messageLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : ($r->implementsInterface(MessageSubscriberInterface::class) ? sprintf('returned by method "%s::getHandledMessages()"', $r->getName()) : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method));
- throw new RuntimeException(sprintf('Invalid handler service "%s": class or interface "%s" %s not found.', $serviceId, $message, $messageLocation));
+ throw new RuntimeException(sprintf('Invalid handler service "%s": class or interface "%s" "%s" not found.', $serviceId, $message, $messageLocation));
}
if (!$r->hasMethod($method)) {
@@ -255,6 +255,14 @@ private function guessHandledClasses(\ReflectionClass $handlerClass, string $ser
private function registerReceivers(ContainerBuilder $container, array $busIds)
{
$receiverMapping = [];
+ $failureTransportsMap = [];
+ if ($container->hasDefinition('console.command.messenger_failed_messages_retry')) {
+ $commandDefinition = $container->getDefinition('console.command.messenger_failed_messages_retry');
+ $globalReceiverName = $commandDefinition->getArgument(0);
+ if (null !== $globalReceiverName) {
+ $failureTransportsMap[$commandDefinition->getArgument(0)] = new Reference('messenger.failure_transports.default');
+ }
+ }
foreach ($container->findTaggedServiceIds($this->receiverTag) as $id => $tags) {
$receiverClass = $this->getServiceClass($container, $id);
@@ -267,6 +275,9 @@ private function registerReceivers(ContainerBuilder $container, array $busIds)
foreach ($tags as $tag) {
if (isset($tag['alias'])) {
$receiverMapping[$tag['alias']] = $receiverMapping[$id];
+ if ($tag['is_failure_transport'] ?? false) {
+ $failureTransportsMap[$tag['alias']] = $receiverMapping[$id];
+ }
}
}
}
@@ -303,6 +314,8 @@ private function registerReceivers(ContainerBuilder $container, array $busIds)
$container->getDefinition('messenger.receiver_locator')->replaceArgument(0, $receiverMapping);
+ $failureTransportsLocator = ServiceLocatorTagPass::register($container, $failureTransportsMap);
+
$failedCommandIds = [
'console.command.messenger_failed_messages_retry',
'console.command.messenger_failed_messages_show',
@@ -311,7 +324,7 @@ private function registerReceivers(ContainerBuilder $container, array $busIds)
foreach ($failedCommandIds as $failedCommandId) {
if ($container->hasDefinition($failedCommandId)) {
$definition = $container->getDefinition($failedCommandId);
- $definition->replaceArgument(1, $receiverMapping[$definition->getArgument(0)]);
+ $definition->replaceArgument(1, $failureTransportsLocator);
}
}
}
diff --git a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageToFailureTransportListener.php b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageToFailureTransportListener.php
index 729dd5fec4409..afbc3c4b373d3 100644
--- a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageToFailureTransportListener.php
+++ b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageToFailureTransportListener.php
@@ -10,6 +10,7 @@
namespace Symfony\Component\Messenger\EventListener;
+use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
@@ -25,12 +26,19 @@
*/
class SendFailedMessageToFailureTransportListener implements EventSubscriberInterface
{
- private $failureSender;
+ private $failureSenders;
private $logger;
- public function __construct(SenderInterface $failureSender, LoggerInterface $logger = null)
+ /**
+ * @param ContainerInterface $failureSenders
+ */
+ public function __construct($failureSenders, LoggerInterface $logger = null)
{
- $this->failureSender = $failureSender;
+ if (!$failureSenders instanceof ContainerInterface) {
+ trigger_deprecation('symfony/messenger', '5.3', 'Passing a SenderInterface value as 1st argument to "%s()" is deprecated, pass a ServiceLocator instead.', __METHOD__);
+ }
+
+ $this->failureSenders = $failureSenders;
$this->logger = $logger;
}
@@ -40,6 +48,15 @@ public function onMessageFailed(WorkerMessageFailedEvent $event)
return;
}
+ if (!$this->hasFailureTransports($event)) {
+ return;
+ }
+
+ $failureSender = $this->getFailureSender($event->getReceiverName());
+ if (null === $failureSender) {
+ return;
+ }
+
$envelope = $event->getEnvelope();
// avoid re-sending to the failed sender
@@ -56,11 +73,11 @@ public function onMessageFailed(WorkerMessageFailedEvent $event)
if (null !== $this->logger) {
$this->logger->info('Rejected message {class} will be sent to the failure transport {transport}.', [
'class' => \get_class($envelope->getMessage()),
- 'transport' => \get_class($this->failureSender),
+ 'transport' => \get_class($failureSender),
]);
}
- $this->failureSender->send($envelope);
+ $failureSender->send($envelope);
}
public static function getSubscribedEvents()
@@ -69,4 +86,18 @@ public static function getSubscribedEvents()
WorkerMessageFailedEvent::class => ['onMessageFailed', -100],
];
}
+
+ private function getFailureSender(string $receiverName): SenderInterface
+ {
+ if ($this->failureSenders instanceof SenderInterface) {
+ return $this->failureSenders;
+ }
+
+ return $this->failureSenders->get($receiverName);
+ }
+
+ private function hasFailureTransports(WorkerMessageFailedEvent $event): bool
+ {
+ return ($this->failureSenders instanceof ContainerInterface && $this->failureSenders->has($event->getReceiverName())) || $this->failureSenders instanceof SenderInterface;
+ }
}
diff --git a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRemoveCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRemoveCommandTest.php
index ba5e9cff00879..08dc2a0e1d920 100644
--- a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRemoveCommandTest.php
+++ b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRemoveCommandTest.php
@@ -13,12 +13,17 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Tester\CommandTester;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand;
use Symfony\Component\Messenger\Envelope;
+use Symfony\Component\Messenger\Exception\InvalidArgumentException;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
class FailedMessagesRemoveCommandTest extends TestCase
{
+ /**
+ * @group legacy
+ */
public function testRemoveSingleMessage()
{
$receiver = $this->createMock(ListableReceiverInterface::class);
@@ -36,6 +41,30 @@ public function testRemoveSingleMessage()
$this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay());
}
+ public function testRemoveSingleMessageWithServiceLocator()
+ {
+ $globalFailureReceiverName = 'failure_receiver';
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->once())->method('find')->with(20)->willReturn(new Envelope(new \stdClass()));
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->expects($this->once())->method('has')->with($globalFailureReceiverName)->willReturn(true);
+ $serviceLocator->expects($this->any())->method('get')->with($globalFailureReceiverName)->willReturn($receiver);
+
+ $command = new FailedMessagesRemoveCommand(
+ $globalFailureReceiverName,
+ $serviceLocator
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => 20, '--force' => true]);
+
+ $this->assertStringContainsString('Failed Message Details', $tester->getDisplay());
+ $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay());
+ }
+
+ /**
+ * @group legacy
+ */
public function testRemoveUniqueMessage()
{
$receiver = $this->createMock(ListableReceiverInterface::class);
@@ -53,6 +82,92 @@ public function testRemoveUniqueMessage()
$this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay());
}
+ public function testRemoveUniqueMessageWithServiceLocator()
+ {
+ $globalFailureReceiverName = 'failure_receiver';
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->once())->method('find')->with(20)->willReturn(new Envelope(new \stdClass()));
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->expects($this->once())->method('has')->with($globalFailureReceiverName)->willReturn(true);
+ $serviceLocator->expects($this->any())->method('get')->with($globalFailureReceiverName)->willReturn($receiver);
+
+ $command = new FailedMessagesRemoveCommand(
+ $globalFailureReceiverName,
+ $serviceLocator
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => [20], '--force' => true]);
+
+ $this->assertStringContainsString('Failed Message Details', $tester->getDisplay());
+ $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay());
+ }
+
+ public function testRemoveUniqueMessageWithServiceLocatorFromSpecificFailureTransport()
+ {
+ $failureReveiverName = 'specific_failure_receiver';
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->once())->method('find')->with(20)->willReturn(new Envelope(new \stdClass()));
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->expects($this->once())->method('has')->with($failureReveiverName)->willReturn(true);
+ $serviceLocator->expects($this->any())->method('get')->with($failureReveiverName)->willReturn($receiver);
+
+ $command = new FailedMessagesRemoveCommand(
+ $failureReveiverName,
+ $serviceLocator
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => [20], '--transport' => $failureReveiverName, '--force' => true]);
+
+ $this->assertStringContainsString('Failed Message Details', $tester->getDisplay());
+ $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay());
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testThrowExceptionIfFailureTransportNotDefined()
+ {
+ $failureReceiverName = 'failure_receiver';
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+
+ $this->expectException(InvalidArgumentException::class);
+ $command = new FailedMessagesRemoveCommand(
+ null,
+ $receiver
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => [20], '--transport' => $failureReceiverName, '--force' => true]);
+
+ $this->assertStringContainsString('Failed Message Details', $tester->getDisplay());
+ $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay());
+ }
+
+ public function testThrowExceptionIfFailureTransportNotDefinedWithServiceLocator()
+ {
+ $failureReceiverName = 'failure_receiver';
+
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->expects($this->once())->method('has')->with($failureReceiverName)->willReturn(false);
+
+ $this->expectException(InvalidArgumentException::class);
+ $command = new FailedMessagesRemoveCommand(
+ $failureReceiverName,
+ $serviceLocator
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => [20], '--transport' => $failureReceiverName, '--force' => true]);
+
+ $this->assertStringContainsString('Failed Message Details', $tester->getDisplay());
+ $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay());
+ }
+
+ /**
+ * @group legacy
+ */
public function testRemoveMultipleMessages()
{
$receiver = $this->createMock(ListableReceiverInterface::class);
@@ -76,6 +191,37 @@ public function testRemoveMultipleMessages()
$this->assertStringContainsString('Message with id 40 removed.', $tester->getDisplay());
}
+ public function testRemoveMultipleMessagesWithServiceLocator()
+ {
+ $globalFailureReceiverName = 'failure_receiver';
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->exactly(3))->method('find')->withConsecutive([20], [30], [40])->willReturnOnConsecutiveCalls(
+ new Envelope(new \stdClass()),
+ null,
+ new Envelope(new \stdClass())
+ );
+
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->expects($this->once())->method('has')->with($globalFailureReceiverName)->willReturn(true);
+ $serviceLocator->expects($this->any())->method('get')->with($globalFailureReceiverName)->willReturn($receiver);
+
+ $command = new FailedMessagesRemoveCommand(
+ $globalFailureReceiverName,
+ $serviceLocator
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => [20, 30, 40], '--force' => true]);
+
+ $this->assertStringNotContainsString('Failed Message Details', $tester->getDisplay());
+ $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay());
+ $this->assertStringContainsString('The message with id "30" was not found.', $tester->getDisplay());
+ $this->assertStringContainsString('Message with id 40 removed.', $tester->getDisplay());
+ }
+
+ /**
+ * @group legacy
+ */
public function testRemoveMultipleMessagesAndDisplayMessages()
{
$receiver = $this->createMock(ListableReceiverInterface::class);
@@ -96,4 +242,30 @@ public function testRemoveMultipleMessagesAndDisplayMessages()
$this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay());
$this->assertStringContainsString('Message with id 30 removed.', $tester->getDisplay());
}
+
+ public function testRemoveMultipleMessagesAndDisplayMessagesWithServiceLocator()
+ {
+ $globalFailureReceiverName = 'failure_receiver';
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->exactly(2))->method('find')->withConsecutive([20], [30])->willReturnOnConsecutiveCalls(
+ new Envelope(new \stdClass()),
+ new Envelope(new \stdClass())
+ );
+
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->expects($this->once())->method('has')->with($globalFailureReceiverName)->willReturn(true);
+ $serviceLocator->expects($this->any())->method('get')->with($globalFailureReceiverName)->willReturn($receiver);
+
+ $command = new FailedMessagesRemoveCommand(
+ $globalFailureReceiverName,
+ $serviceLocator
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => [20, 30], '--force' => true, '--show-messages' => true]);
+
+ $this->assertStringContainsString('Failed Message Details', $tester->getDisplay());
+ $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay());
+ $this->assertStringContainsString('Message with id 30 removed.', $tester->getDisplay());
+ }
}
diff --git a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRetryCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRetryCommandTest.php
index 901f70e1e1d69..e815707f80fc7 100644
--- a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRetryCommandTest.php
+++ b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRetryCommandTest.php
@@ -13,6 +13,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Tester\CommandTester;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Messenger\Command\FailedMessagesRetryCommand;
use Symfony\Component\Messenger\Envelope;
@@ -21,6 +22,9 @@
class FailedMessagesRetryCommandTest extends TestCase
{
+ /**
+ * @group legacy
+ */
public function testBasicRun()
{
$receiver = $this->createMock(ListableReceiverInterface::class);
@@ -45,4 +49,99 @@ public function testBasicRun()
$this->assertStringContainsString('[OK]', $tester->getDisplay());
}
+
+ public function testBasicRunWithServiceLocator()
+ {
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->exactly(2))->method('find')->withConsecutive([10], [12])->willReturn(new Envelope(new \stdClass()));
+ // message will eventually be ack'ed in Worker
+ $receiver->expects($this->exactly(2))->method('ack');
+
+ $dispatcher = new EventDispatcher();
+ $bus = $this->createMock(MessageBusInterface::class);
+ // the bus should be called in the worker
+ $bus->expects($this->exactly(2))->method('dispatch')->willReturn(new Envelope(new \stdClass()));
+
+ $failureTransportName = 'failure_receiver';
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
+ $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
+
+ $command = new FailedMessagesRetryCommand(
+ $failureTransportName,
+ $serviceLocator,
+ $bus,
+ $dispatcher
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => [10, 12], '--force' => true]);
+
+ $this->assertStringContainsString('[OK]', $tester->getDisplay());
+ $this->assertStringNotContainsString('Available failure transports are:', $tester->getDisplay());
+ }
+
+ public function testBasicRunWithServiceLocatorMultipleFailedTransportsDefined()
+ {
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->method('all')->willReturn([]);
+
+ $dispatcher = new EventDispatcher();
+ $bus = $this->createMock(MessageBusInterface::class);
+
+ $failureTransportName = 'failure_receiver';
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
+ $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
+ $serviceLocator->method('getProvidedServices')->willReturn([
+ 'failure_receiver' => [],
+ 'failure_receiver_2' => [],
+ 'failure_receiver_3' => [],
+ ]);
+
+ $command = new FailedMessagesRetryCommand(
+ $failureTransportName,
+ $serviceLocator,
+ $bus,
+ $dispatcher
+ );
+ $tester = new CommandTester($command);
+ $tester->setInputs([0]);
+ $tester->execute(['--force' => true]);
+
+ $expectedLadingMessage = << Available failure transports are: failure_receiver, failure_receiver_2, failure_receiver_3
+EOF;
+ $this->assertStringContainsString($expectedLadingMessage, $tester->getDisplay());
+ }
+
+ public function testBasicRunWithServiceLocatorWithSpecificFailureTransport()
+ {
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->exactly(2))->method('find')->withConsecutive([10], [12])->willReturn(new Envelope(new \stdClass()));
+ // message will eventually be ack'ed in Worker
+ $receiver->expects($this->exactly(2))->method('ack');
+
+ $dispatcher = new EventDispatcher();
+ $bus = $this->createMock(MessageBusInterface::class);
+ // the bus should be called in the worker
+ $bus->expects($this->exactly(2))->method('dispatch')->willReturn(new Envelope(new \stdClass()));
+
+ $failureTransportName = 'failure_receiver';
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
+ $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
+
+ $command = new FailedMessagesRetryCommand(
+ $failureTransportName,
+ $serviceLocator,
+ $bus,
+ $dispatcher
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => [10, 12], '--transport' => $failureTransportName, '--force' => true]);
+
+ $this->assertStringContainsString('[OK]', $tester->getDisplay());
+ }
}
diff --git a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php
index 6c6f110f31b88..5afd452eba82f 100644
--- a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php
+++ b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php
@@ -14,6 +14,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Messenger\Command\FailedMessagesShowCommand;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
@@ -28,6 +29,9 @@
*/
class FailedMessagesShowCommandTest extends TestCase
{
+ /**
+ * @group legacy
+ */
public function testBasicRun()
{
$sentToFailureStamp = new SentToFailureTransportStamp('async');
@@ -51,7 +55,7 @@ public function testBasicRun()
$tester->execute(['id' => 15]);
$this->assertStringContainsString(sprintf(<<getDisplay(true));
}
+ public function testBasicRunWithServiceLocator()
+ {
+ $sentToFailureStamp = new SentToFailureTransportStamp('async');
+ $redeliveryStamp = new RedeliveryStamp(0);
+ $errorStamp = ErrorDetailsStamp::create(new \Exception('Things are bad!', 123));
+ $envelope = new Envelope(new \stdClass(), [
+ new TransportMessageIdStamp(15),
+ $sentToFailureStamp,
+ $redeliveryStamp,
+ $errorStamp,
+ ]);
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->once())->method('find')->with(15)->willReturn($envelope);
+
+ $failureTransportName = 'failure_receiver';
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
+ $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
+
+ $command = new FailedMessagesShowCommand(
+ $failureTransportName,
+ $serviceLocator
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => 15]);
+
+ $this->assertStringContainsString(sprintf(<<getRedeliveredAt()->format('Y-m-d H:i:s')),
+ $tester->getDisplay(true));
+ }
+
+ /**
+ * @group legacy
+ */
public function testMultipleRedeliveryFails()
{
$sentToFailureStamp = new SentToFailureTransportStamp('async');
@@ -101,6 +150,48 @@ public function testMultipleRedeliveryFails()
$tester->getDisplay(true));
}
+ public function testMultipleRedeliveryFailsWithServiceLocator()
+ {
+ $sentToFailureStamp = new SentToFailureTransportStamp('async');
+ $redeliveryStamp1 = new RedeliveryStamp(0);
+ $errorStamp = ErrorDetailsStamp::create(new \Exception('Things are bad!', 123));
+ $redeliveryStamp2 = new RedeliveryStamp(0);
+ $envelope = new Envelope(new \stdClass(), [
+ new TransportMessageIdStamp(15),
+ $sentToFailureStamp,
+ $redeliveryStamp1,
+ $errorStamp,
+ $redeliveryStamp2,
+ ]);
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->once())->method('find')->with(15)->willReturn($envelope);
+
+ $failureTransportName = 'failure_receiver';
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
+ $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
+
+ $command = new FailedMessagesShowCommand(
+ $failureTransportName,
+ $serviceLocator
+ );
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => 15]);
+ $this->assertStringContainsString(sprintf(<<getRedeliveredAt()->format('Y-m-d H:i:s')),
+ $tester->getDisplay(true));
+ }
+
/**
* @group legacy
*/
@@ -136,6 +227,9 @@ public function testLegacyFallback()
$tester->getDisplay(true));
}
+ /**
+ * @group legacy
+ */
public function testReceiverShouldBeListable()
{
$receiver = $this->createMock(ReceiverInterface::class);
@@ -150,6 +244,28 @@ public function testReceiverShouldBeListable()
$tester->execute(['id' => 15]);
}
+ public function testReceiverShouldBeListableWithServiceLocator()
+ {
+ $receiver = $this->createMock(ReceiverInterface::class);
+ $failureTransportName = 'failure_receiver';
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
+ $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
+
+ $command = new FailedMessagesShowCommand(
+ $failureTransportName,
+ $serviceLocator
+ );
+
+ $this->expectExceptionMessage('The "failure_receiver" receiver does not support listing or showing specific messages.');
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => 15]);
+ }
+
+ /**
+ * @group legacy
+ */
public function testListMessages()
{
$sentToFailureStamp = new SentToFailureTransportStamp('async');
@@ -179,6 +295,56 @@ public function testListMessages()
$tester->getDisplay(true));
}
+ public function testListMessagesWithServiceLocator()
+ {
+ $sentToFailureStamp = new SentToFailureTransportStamp('async');
+ $redeliveryStamp = new RedeliveryStamp(0);
+ $errorStamp = ErrorDetailsStamp::create(new \RuntimeException('Things are bad!'));
+ $envelope = new Envelope(new \stdClass(), [
+ new TransportMessageIdStamp(15),
+ $sentToFailureStamp,
+ $redeliveryStamp,
+ $errorStamp,
+ ]);
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->once())->method('all')->with()->willReturn([$envelope]);
+
+ $failureTransportName = 'failure_receiver';
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
+ $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
+ $serviceLocator->method('getProvidedServices')->willReturn([
+ $failureTransportName => [],
+ 'failure_receiver_2' => [],
+ 'failure_receiver_3' => [],
+ ]);
+ $command = new FailedMessagesShowCommand(
+ $failureTransportName,
+ $serviceLocator
+ );
+ $tester = new CommandTester($command);
+ $tester->setInputs([0]);
+ $tester->execute([]);
+
+ $this->assertStringContainsString(sprintf(<<getRedeliveredAt()->format('Y-m-d H:i:s')),
+ $tester->getDisplay(true));
+
+ $expectedLoadingMessage = << Available failure transports are: failure_receiver, failure_receiver_2, failure_receiver_3
+EOF;
+
+ $this->assertStringContainsString($expectedLoadingMessage, $tester->getDisplay());
+ $this->assertStringContainsString('Run messenger:failed:show {id} --transport=failure_receiver -vv to see message details.', $tester->getDisplay());
+
+ }
+
+ /**
+ * @group legacy
+ */
public function testListMessagesReturnsNoMessagesFound()
{
$receiver = $this->createMock(ListableReceiverInterface::class);
@@ -194,6 +360,28 @@ public function testListMessagesReturnsNoMessagesFound()
$this->assertStringContainsString('[OK] No failed messages were found.', $tester->getDisplay(true));
}
+ public function testListMessagesReturnsNoMessagesFoundWithServiceLocator()
+ {
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->once())->method('all')->with()->willReturn([]);
+ $failureTransportName = 'failure_receiver';
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
+ $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
+
+ $command = new FailedMessagesShowCommand(
+ $failureTransportName,
+ $serviceLocator
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute([]);
+ $this->assertStringContainsString('[OK] No failed messages were found.', $tester->getDisplay(true));
+ }
+
+ /**
+ * @group legacy
+ */
public function testListMessagesReturnsPaginatedMessages()
{
$sentToFailureStamp = new SentToFailureTransportStamp('async');
@@ -216,6 +404,36 @@ public function testListMessagesReturnsPaginatedMessages()
$this->assertStringContainsString('Showing first 1 messages.', $tester->getDisplay(true));
}
+ public function testListMessagesReturnsPaginatedMessagesWithServiceLocator()
+ {
+ $sentToFailureStamp = new SentToFailureTransportStamp('async');
+ $envelope = new Envelope(new \stdClass(), [
+ new TransportMessageIdStamp(15),
+ $sentToFailureStamp,
+ new RedeliveryStamp(0),
+ ErrorDetailsStamp::create(new \RuntimeException('Things are bad!')),
+ ]);
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->once())->method('all')->with()->willReturn([$envelope]);
+
+ $failureTransportName = 'failure_receiver';
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
+ $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
+
+ $command = new FailedMessagesShowCommand(
+ $failureTransportName,
+ $serviceLocator
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute(['--max' => 1]);
+ $this->assertStringContainsString('Showing first 1 messages.', $tester->getDisplay(true));
+ }
+
+ /**
+ * @group legacy
+ */
public function testInvalidMessagesThrowsException()
{
$sentToFailureStamp = new SentToFailureTransportStamp('async');
@@ -236,6 +454,29 @@ public function testInvalidMessagesThrowsException()
$tester->execute(['id' => 15]);
}
+ public function testInvalidMessagesThrowsExceptionWithServiceLocator()
+ {
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+
+ $failureTransportName = 'failure_receiver';
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
+ $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
+
+ $command = new FailedMessagesShowCommand(
+ $failureTransportName,
+ $serviceLocator
+ );
+
+ $this->expectExceptionMessage('The message "15" was not found.');
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => 15]);
+ }
+
+ /**
+ * @group legacy
+ */
public function testVeryVerboseOutputForSingleMessageContainsExceptionWithTrace()
{
$exception = new \RuntimeException('Things are bad!');
@@ -275,4 +516,83 @@ public function testVeryVerboseOutputForSingleMessageContainsExceptionWithTrace(
__FILE__, $exceptionLine, $exceptionLine),
$tester->getDisplay(true));
}
+
+ public function testVeryVerboseOutputForSingleMessageContainsExceptionWithTraceWithServiceLocator()
+ {
+ $exception = new \RuntimeException('Things are bad!');
+ $exceptionLine = __LINE__ - 1;
+ $envelope = new Envelope(new \stdClass(), [
+ new TransportMessageIdStamp(15),
+ new SentToFailureTransportStamp('async'),
+ new RedeliveryStamp(0),
+ ErrorDetailsStamp::create($exception),
+ ]);
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->once())->method('find')->with(42)->willReturn($envelope);
+
+ $failureTransportName = 'failure_receiver';
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
+ $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
+
+ $command = new FailedMessagesShowCommand($failureTransportName, $serviceLocator);
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => 42], ['verbosity' => OutputInterface::VERBOSITY_VERY_VERBOSE]);
+ $this->assertStringMatchesFormat(sprintf(<<<'EOF'
+%%A
+Exception:
+==========
+
+RuntimeException {
+ message: "Things are bad!"
+ code: 0
+ file: "%s"
+ line: %d
+ trace: {
+ %%s%%eTests%%eCommand%%eFailedMessagesShowCommandTest.php:%d {
+ Symfony\Component\Messenger\Tests\Command\FailedMessagesShowCommandTest->testVeryVerboseOutputForSingleMessageContainsExceptionWithTraceWithServiceLocator()
+ › {
+ › $exception = new \RuntimeException('Things are bad!');
+ › $exceptionLine = __LINE__ - 1;
+ }
+%%A
+EOF
+ ,
+ __FILE__, $exceptionLine, $exceptionLine),
+ $tester->getDisplay(true));
+ }
+
+ public function testListMessagesWithServiceLocatorFromSpecificTransport()
+ {
+ $sentToFailureStamp = new SentToFailureTransportStamp('async');
+ $redeliveryStamp = new RedeliveryStamp(0);
+ $errorStamp = ErrorDetailsStamp::create(new \RuntimeException('Things are bad!'));
+ $envelope = new Envelope(new \stdClass(), [
+ new TransportMessageIdStamp(15),
+ $sentToFailureStamp,
+ $redeliveryStamp,
+ $errorStamp,
+ ]);
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->once())->method('all')->with()->willReturn([$envelope]);
+
+ $failureTransportName = 'failure_receiver_another';
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
+ $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
+
+ $command = new FailedMessagesShowCommand(
+ 'global_but_not_used',
+ $serviceLocator
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute(['--transport' => $failureTransportName]);
+ $this->assertStringContainsString(sprintf(<<getRedeliveredAt()->format('Y-m-d H:i:s')),
+ $tester->getDisplay(true));
+ }
}
diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php
index 580b588d755c4..1235bbbdcbd1d 100644
--- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php
+++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php
@@ -22,6 +22,8 @@
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceiver;
use Symfony\Component\Messenger\Command\ConsumeMessagesCommand;
use Symfony\Component\Messenger\Command\DebugCommand;
+use Symfony\Component\Messenger\Command\FailedMessagesRetryCommand;
+use Symfony\Component\Messenger\Command\FailedMessagesShowCommand;
use Symfony\Component\Messenger\Command\SetupTransportsCommand;
use Symfony\Component\Messenger\DataCollector\MessengerDataCollector;
use Symfony\Component\Messenger\DependencyInjection\MessengerPass;
@@ -402,7 +404,7 @@ public function testItRegistersHandlersOnDifferentBuses()
public function testItThrowsAnExceptionOnUnknownBus()
{
$this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Invalid configuration returned by method "Symfony\Component\Messenger\Tests\DependencyInjection\HandlerOnUndefinedBus::getHandledMessages()" for message "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage": bus "some_undefined_bus" does not exist.');
+ $this->expectExceptionMessage('Invalid configuration "returned by method "Symfony\Component\Messenger\Tests\DependencyInjection\HandlerOnUndefinedBus::getHandledMessages()"" for message "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage": bus "some_undefined_bus" does not exist.');
$container = $this->getContainerBuilder();
$container
->register(HandlerOnUndefinedBus::class, HandlerOnUndefinedBus::class)
@@ -415,7 +417,7 @@ public function testItThrowsAnExceptionOnUnknownBus()
public function testUndefinedMessageClassForHandler()
{
$this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandler": class or interface "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" used as argument type in method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandler::__invoke()" not found.');
+ $this->expectExceptionMessage('Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandler": class or interface "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" "used as argument type in method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandler::__invoke()"" not found.');
$container = $this->getContainerBuilder();
$container
->register(UndefinedMessageHandler::class, UndefinedMessageHandler::class)
@@ -428,7 +430,7 @@ public function testUndefinedMessageClassForHandler()
public function testUndefinedMessageClassForHandlerImplementingMessageHandlerInterface()
{
$this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaHandlerInterface": class or interface "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" used as argument type in method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaHandlerInterface::__invoke()" not found.');
+ $this->expectExceptionMessage('Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaHandlerInterface": class or interface "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" "used as argument type in method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaHandlerInterface::__invoke()"" not found.');
$container = $this->getContainerBuilder();
$container
->register(UndefinedMessageHandlerViaHandlerInterface::class, UndefinedMessageHandlerViaHandlerInterface::class)
@@ -441,7 +443,7 @@ public function testUndefinedMessageClassForHandlerImplementingMessageHandlerInt
public function testUndefinedMessageClassForHandlerImplementingMessageSubscriberInterface()
{
$this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaSubscriberInterface": class or interface "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" returned by method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaSubscriberInterface::getHandledMessages()" not found.');
+ $this->expectExceptionMessage('Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaSubscriberInterface": class or interface "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" "returned by method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaSubscriberInterface::getHandledMessages()"" not found.');
$container = $this->getContainerBuilder();
$container
->register(UndefinedMessageHandlerViaSubscriberInterface::class, UndefinedMessageHandlerViaSubscriberInterface::class)
@@ -693,6 +695,34 @@ private function assertHandlerDescriptor(ContainerBuilder $container, array $map
$this->assertEquals($options[$index] ?? [], $definitionArguments[1]);
}
}
+
+ public function testFailedCommandsRegisteredWithServiceLocatorArgumentReplaced()
+ {
+ $globalReceiverName = 'global_failure_transport';
+ $container = $this->getContainerBuilder($messageBusId = 'message_bus');
+
+ $container->register('console.command.messenger_failed_messages_retry', FailedMessagesRetryCommand::class)
+ ->setArgument(0, $globalReceiverName)
+ ->setArgument(1, null)
+ ->setArgument(2, new Reference($messageBusId));
+ $container->register('console.command.messenger_failed_messages_show', FailedMessagesShowCommand::class)
+ ->setArgument(0, $globalReceiverName)
+ ->setArgument(1, null);
+ $container->register('console.command.messenger_failed_messages_remove', FailedMessagesRetryCommand::class)
+ ->setArgument(0, $globalReceiverName)
+ ->setArgument(1, null);
+
+ (new MessengerPass())->process($container);
+
+ $retryDefinition = $container->getDefinition('console.command.messenger_failed_messages_retry');
+ $this->assertNotNull($retryDefinition->getArgument(1));
+
+ $showDefinition = $container->getDefinition('console.command.messenger_failed_messages_show');
+ $this->assertNotNull($showDefinition->getArgument(1));
+
+ $removeDefinition = $container->getDefinition('console.command.messenger_failed_messages_remove');
+ $this->assertNotNull($removeDefinition->getArgument(1));
+ }
}
class DummyHandler
diff --git a/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageToFailureTransportListenerTest.php b/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageToFailureTransportListenerTest.php
index c489ac45e33c4..6ef74fef5cdb3 100644
--- a/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageToFailureTransportListenerTest.php
+++ b/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageToFailureTransportListenerTest.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Messenger\Tests\EventListener;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\EventListener\SendFailedMessageToFailureTransportListener;
@@ -20,6 +21,9 @@
class SendFailedMessageToFailureTransportListenerTest extends TestCase
{
+ /**
+ * @group legacy
+ */
public function testItSendsToTheFailureTransport()
{
$sender = $this->createMock(SenderInterface::class);
@@ -43,11 +47,56 @@ public function testItSendsToTheFailureTransport()
$listener->onMessageFailed($event);
}
+ public function testItSendsToTheFailureTransportWithSenderLocator()
+ {
+ $receiverName = 'my_receiver';
+ $sender = $this->createMock(SenderInterface::class);
+ $sender->expects($this->once())->method('send')->with($this->callback(function ($envelope) use ($receiverName) {
+ /* @var Envelope $envelope */
+ $this->assertInstanceOf(Envelope::class, $envelope);
+
+ /** @var SentToFailureTransportStamp $sentToFailureTransportStamp */
+ $sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class);
+ $this->assertNotNull($sentToFailureTransportStamp);
+ $this->assertSame($receiverName, $sentToFailureTransportStamp->getOriginalReceiverName());
+
+ return true;
+ }))->willReturnArgument(0);
+
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->expects($this->once())->method('has')->willReturn(true);
+ $serviceLocator->expects($this->once())->method('get')->with($receiverName)->willReturn($sender);
+ $listener = new SendFailedMessageToFailureTransportListener($serviceLocator);
+
+ $exception = new \Exception('no!');
+ $envelope = new Envelope(new \stdClass());
+ $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception);
+
+ $listener->onMessageFailed($event);
+ }
+
+ /**
+ * @group legacy
+ */
public function testDoNothingOnRetry()
{
$sender = $this->createMock(SenderInterface::class);
$sender->expects($this->never())->method('send');
$listener = new SendFailedMessageToFailureTransportListener($sender);
+ $envelope = new Envelope(new \stdClass());
+ $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', new \Exception());
+ $event->setForRetry();
+
+ $listener->onMessageFailed($event);
+ }
+
+ public function testDoNothingOnRetryWithServiceLocator()
+ {
+ $sender = $this->createMock(SenderInterface::class);
+ $sender->expects($this->never())->method('send');
+
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $listener = new SendFailedMessageToFailureTransportListener($serviceLocator);
$envelope = new Envelope(new \stdClass());
$event = new WorkerMessageFailedEvent($envelope, 'my_receiver', new \Exception());
@@ -56,6 +105,9 @@ public function testDoNothingOnRetry()
$listener->onMessageFailed($event);
}
+ /**
+ * @group legacy
+ */
public function testDoNotRedeliverToFailed()
{
$sender = $this->createMock(SenderInterface::class);
@@ -69,4 +121,65 @@ public function testDoNotRedeliverToFailed()
$listener->onMessageFailed($event);
}
+
+ public function testDoNotRedeliverToFailedWithServiceLocator()
+ {
+ $receiverName = 'my_receiver';
+
+ $sender = $this->createMock(SenderInterface::class);
+ $sender->expects($this->never())->method('send');
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+
+ $listener = new SendFailedMessageToFailureTransportListener($serviceLocator);
+ $envelope = new Envelope(new \stdClass(), [
+ new SentToFailureTransportStamp($receiverName),
+ ]);
+ $event = new WorkerMessageFailedEvent($envelope, $receiverName, new \Exception());
+
+ $listener->onMessageFailed($event);
+ }
+
+ public function testDoNothingIfFailureTransportIsNotDefined()
+ {
+ $sender = $this->createMock(SenderInterface::class);
+ $sender->expects($this->never())->method('send');
+
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $listener = new SendFailedMessageToFailureTransportListener($serviceLocator, null);
+
+ $exception = new \Exception('no!');
+ $envelope = new Envelope(new \stdClass());
+ $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception);
+
+ $listener->onMessageFailed($event);
+ }
+
+ public function testItSendsToTheFailureTransportWithMultipleFailedTransports()
+ {
+ $receiverName = 'my_receiver';
+ $sender = $this->createMock(SenderInterface::class);
+ $sender->expects($this->once())->method('send')->with($this->callback(function ($envelope) use ($receiverName) {
+ /* @var Envelope $envelope */
+ $this->assertInstanceOf(Envelope::class, $envelope);
+
+ /** @var SentToFailureTransportStamp $sentToFailureTransportStamp */
+ $sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class);
+ $this->assertNotNull($sentToFailureTransportStamp);
+ $this->assertSame($receiverName, $sentToFailureTransportStamp->getOriginalReceiverName());
+
+ return true;
+ }))->willReturnArgument(0);
+
+ $serviceLocator = $this->createMock(ServiceLocator::class);
+ $serviceLocator->method('has')->with($receiverName)->willReturn(true);
+ $serviceLocator->method('get')->with($receiverName)->willReturn($sender);
+
+ $listener = new SendFailedMessageToFailureTransportListener($serviceLocator);
+
+ $exception = new \Exception('no!');
+ $envelope = new Envelope(new \stdClass());
+ $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception);
+
+ $listener->onMessageFailed($event);
+ }
}
diff --git a/src/Symfony/Component/Messenger/Tests/FailureIntegrationTest.php b/src/Symfony/Component/Messenger/Tests/FailureIntegrationTest.php
index 88e32dd845ea2..00c3571d678f6 100644
--- a/src/Symfony/Component/Messenger/Tests/FailureIntegrationTest.php
+++ b/src/Symfony/Component/Messenger/Tests/FailureIntegrationTest.php
@@ -13,6 +13,8 @@
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
+use Psr\Log\NullLogger;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
@@ -38,6 +40,9 @@
class FailureIntegrationTest extends TestCase
{
+ /**
+ * @group legacy
+ */
public function testRequeueMechanism()
{
$transport1 = new DummyFailureTestSenderAndReceiver();
@@ -216,6 +221,333 @@ public function testRequeueMechanism()
// the failure transport is empty because it worked
$this->assertEmpty($failureTransport->getMessagesWaitingToBeReceived());
}
+
+ public function testRequeueMechanismWithServiceLocator()
+ {
+ $transport1 = new DummyFailureTestSenderAndReceiver();
+ $transport2 = new DummyFailureTestSenderAndReceiver();
+ $failureTransport = new DummyFailureTestSenderAndReceiver();
+ $sendersLocatorFailureTransport = new ServiceLocator([
+ 'transport1' => function () use ($failureTransport) {
+ return $failureTransport;
+ },
+ 'transport2' => function () use ($failureTransport) {
+ return $failureTransport;
+ },
+ ]);
+
+ $transports = [
+ 'transport1' => $transport1,
+ 'transport2' => $transport2,
+ 'the_failure_transport' => $failureTransport,
+ ];
+
+ $locator = $this->createMock(ContainerInterface::class);
+ $locator->expects($this->any())
+ ->method('has')
+ ->willReturn(true);
+ $locator->expects($this->any())
+ ->method('get')
+ ->willReturnCallback(function ($transportName) use ($transports) {
+ return $transports[$transportName];
+ });
+ $senderLocator = new SendersLocator(
+ [DummyMessage::class => ['transport1', 'transport2']],
+ $locator
+ );
+
+ $retryStrategyLocator = $this->createMock(ContainerInterface::class);
+ $retryStrategyLocator->expects($this->any())
+ ->method('has')
+ ->willReturn(true);
+ $retryStrategyLocator->expects($this->any())
+ ->method('get')
+ ->willReturn(new MultiplierRetryStrategy(1));
+
+ // using to so we can lazily get the bus later and avoid circular problem
+ $transport1HandlerThatFails = new DummyTestHandler(true);
+ $allTransportHandlerThatWorks = new DummyTestHandler(false);
+ $transport2HandlerThatWorks = new DummyTestHandler(false);
+ $handlerLocator = new HandlersLocator([
+ DummyMessage::class => [
+ new HandlerDescriptor($transport1HandlerThatFails, [
+ 'from_transport' => 'transport1',
+ 'alias' => 'handler_that_fails',
+ ]),
+ new HandlerDescriptor($allTransportHandlerThatWorks, [
+ 'alias' => 'handler_that_works1',
+ ]),
+ new HandlerDescriptor($transport2HandlerThatWorks, [
+ 'from_transport' => 'transport2',
+ 'alias' => 'handler_that_works2',
+ ]),
+ ],
+ ]);
+
+ $dispatcher = new EventDispatcher();
+ $bus = new MessageBus([
+ new FailedMessageProcessingMiddleware(),
+ new SendMessageMiddleware($senderLocator),
+ new HandleMessageMiddleware($handlerLocator),
+ ]);
+ $dispatcher->addSubscriber(new AddErrorDetailsStampListener());
+ $dispatcher->addSubscriber(new SendFailedMessageForRetryListener($locator, $retryStrategyLocator));
+
+ $dispatcher->addSubscriber(new SendFailedMessageToFailureTransportListener($sendersLocatorFailureTransport));
+ $dispatcher->addSubscriber(new StopWorkerOnMessageLimitListener(1));
+
+ $runWorker = function (string $transportName) use ($transports, $bus, $dispatcher): ?\Throwable {
+ $throwable = null;
+ $failedListener = function (WorkerMessageFailedEvent $event) use (&$throwable) {
+ $throwable = $event->getThrowable();
+ };
+ $dispatcher->addListener(WorkerMessageFailedEvent::class, $failedListener);
+
+ $worker = new Worker([$transportName => $transports[$transportName]], $bus, $dispatcher);
+
+ $worker->run();
+
+ $dispatcher->removeListener(WorkerMessageFailedEvent::class, $failedListener);
+
+ return $throwable;
+ };
+
+ // send the message
+ $envelope = new Envelope(new DummyMessage('API'));
+ $bus->dispatch($envelope);
+
+ // message has been sent
+ $this->assertCount(1, $transport1->getMessagesWaitingToBeReceived());
+ $this->assertCount(1, $transport2->getMessagesWaitingToBeReceived());
+ $this->assertCount(0, $failureTransport->getMessagesWaitingToBeReceived());
+
+ // receive the message - one handler will fail and the message
+ // will be sent back to transport1 to be retried
+ /*
+ * Receive the message from "transport1"
+ */
+ $throwable = $runWorker('transport1');
+ // make sure this is failing for the reason we think
+ $this->assertInstanceOf(HandlerFailedException::class, $throwable);
+ // handler for transport1 and all transports were called
+ $this->assertSame(1, $transport1HandlerThatFails->getTimesCalled());
+ $this->assertSame(1, $allTransportHandlerThatWorks->getTimesCalled());
+ $this->assertSame(0, $transport2HandlerThatWorks->getTimesCalled());
+ // one handler failed and the message is retried (resent to transport1)
+ $this->assertCount(1, $transport1->getMessagesWaitingToBeReceived());
+ $this->assertEmpty($failureTransport->getMessagesWaitingToBeReceived());
+
+ /*
+ * Receive the message for a (final) retry
+ */
+ $runWorker('transport1');
+ // only the "failed" handler is called a 2nd time
+ $this->assertSame(2, $transport1HandlerThatFails->getTimesCalled());
+ $this->assertSame(1, $allTransportHandlerThatWorks->getTimesCalled());
+ // handling fails again, message is sent to failure transport
+ $this->assertCount(0, $transport1->getMessagesWaitingToBeReceived());
+ $this->assertCount(1, $failureTransport->getMessagesWaitingToBeReceived());
+ /** @var Envelope $failedEnvelope */
+ $failedEnvelope = $failureTransport->getMessagesWaitingToBeReceived()[0];
+ /** @var SentToFailureTransportStamp $sentToFailureStamp */
+ $sentToFailureStamp = $failedEnvelope->last(SentToFailureTransportStamp::class);
+ $this->assertNotNull($sentToFailureStamp);
+ /** @var ErrorDetailsStamp $errorDetailsStamp */
+ $errorDetailsStamp = $failedEnvelope->last(ErrorDetailsStamp::class);
+ $this->assertNotNull($errorDetailsStamp);
+ $this->assertSame('Failure from call 2', $errorDetailsStamp->getExceptionMessage());
+
+ /*
+ * Failed message is handled, fails, and sent for a retry
+ */
+ $throwable = $runWorker('the_failure_transport');
+ // make sure this is failing for the reason we think
+ $this->assertInstanceOf(HandlerFailedException::class, $throwable);
+ // only the "failed" handler is called a 3rd time
+ $this->assertSame(3, $transport1HandlerThatFails->getTimesCalled());
+ $this->assertSame(1, $allTransportHandlerThatWorks->getTimesCalled());
+ // handling fails again, message is retried
+ $this->assertCount(1, $failureTransport->getMessagesWaitingToBeReceived());
+ // transport2 still only holds the original message
+ // a new message was never mistakenly delivered to it
+ $this->assertCount(1, $transport2->getMessagesWaitingToBeReceived());
+
+ /*
+ * Message is retried on failure transport then discarded
+ */
+ $runWorker('the_failure_transport');
+ // only the "failed" handler is called a 4th time
+ $this->assertSame(4, $transport1HandlerThatFails->getTimesCalled());
+ $this->assertSame(1, $allTransportHandlerThatWorks->getTimesCalled());
+ // handling fails again, message is discarded
+ $this->assertCount(0, $failureTransport->getMessagesWaitingToBeReceived());
+
+ /*
+ * Execute handlers on transport2
+ */
+ $runWorker('transport2');
+ // transport1 handler is not called again
+ $this->assertSame(4, $transport1HandlerThatFails->getTimesCalled());
+ // all transport handler is now called again
+ $this->assertSame(2, $allTransportHandlerThatWorks->getTimesCalled());
+ // transport1 handler called for the first time
+ $this->assertSame(1, $transport2HandlerThatWorks->getTimesCalled());
+ // all transport should be empty
+ $this->assertEmpty($transport1->getMessagesWaitingToBeReceived());
+ $this->assertEmpty($transport2->getMessagesWaitingToBeReceived());
+ $this->assertEmpty($failureTransport->getMessagesWaitingToBeReceived());
+
+ /*
+ * Dispatch the original message again
+ */
+ $bus->dispatch($envelope);
+ // handle the failing message so it goes into the failure transport
+ $runWorker('transport1');
+ $runWorker('transport1');
+ // now make the handler work!
+ $transport1HandlerThatFails->setShouldThrow(false);
+ $runWorker('the_failure_transport');
+ // the failure transport is empty because it worked
+ $this->assertEmpty($failureTransport->getMessagesWaitingToBeReceived());
+ }
+
+ public function testMultipleFailedTransportsWithoutGlobalFailureTransport()
+ {
+ $transport1 = new DummyFailureTestSenderAndReceiver();
+ $transport2 = new DummyFailureTestSenderAndReceiver();
+ $failureTransport1 = new DummyFailureTestSenderAndReceiver();
+ $failureTransport2 = new DummyFailureTestSenderAndReceiver();
+
+ $sendersLocatorFailureTransport = new ServiceLocator([
+ 'transport1' => function () use ($failureTransport1) {
+ return $failureTransport1;
+ },
+ 'transport2' => function () use ($failureTransport2) {
+ return $failureTransport2;
+ },
+ ]);
+
+ $transports = [
+ 'transport1' => $transport1,
+ 'transport2' => $transport2,
+ 'the_failure_transport1' => $failureTransport1,
+ 'the_failure_transport2' => $failureTransport2,
+ ];
+
+ $locator = $this->createMock(ContainerInterface::class);
+ $locator->expects($this->any())
+ ->method('has')
+ ->willReturn(true);
+ $locator->expects($this->any())
+ ->method('get')
+ ->willReturnCallback(function ($transportName) use ($transports) {
+ return $transports[$transportName];
+ });
+ $senderLocator = new SendersLocator(
+ [DummyMessage::class => ['transport1', 'transport2']],
+ $locator
+ );
+
+ // retry strategy with zero retries so it goes to the failed transport after failure
+ $retryStrategyLocator = $this->createMock(ContainerInterface::class);
+ $retryStrategyLocator->expects($this->any())
+ ->method('has')
+ ->willReturn(true);
+ $retryStrategyLocator->expects($this->any())
+ ->method('get')
+ ->willReturn(new MultiplierRetryStrategy(0));
+
+ // using to so we can lazily get the bus later and avoid circular problem
+ $transport1HandlerThatFails = new DummyTestHandler(true);
+ $transport2HandlerThatFails = new DummyTestHandler(true);
+ $handlerLocator = new HandlersLocator([
+ DummyMessage::class => [
+ new HandlerDescriptor($transport1HandlerThatFails, [
+ 'from_transport' => 'transport1',
+ ]),
+ new HandlerDescriptor($transport2HandlerThatFails, [
+ 'from_transport' => 'transport2',
+ ]),
+ ],
+ ]);
+
+ $dispatcher = new EventDispatcher();
+ $bus = new MessageBus([
+ new FailedMessageProcessingMiddleware(),
+ new SendMessageMiddleware($senderLocator),
+ new HandleMessageMiddleware($handlerLocator),
+ ]);
+
+ $dispatcher->addSubscriber(new SendFailedMessageForRetryListener($locator, $retryStrategyLocator));
+ $dispatcher->addSubscriber(new SendFailedMessageToFailureTransportListener(
+ $sendersLocatorFailureTransport,
+ new NullLogger()
+ ));
+ $dispatcher->addSubscriber(new StopWorkerOnMessageLimitListener(1));
+
+ $runWorker = function (string $transportName) use ($transports, $bus, $dispatcher): ?\Throwable {
+ $throwable = null;
+ $failedListener = function (WorkerMessageFailedEvent $event) use (&$throwable) {
+ $throwable = $event->getThrowable();
+ };
+ $dispatcher->addListener(WorkerMessageFailedEvent::class, $failedListener);
+
+ $worker = new Worker([$transportName => $transports[$transportName]], $bus, $dispatcher);
+
+ $worker->run();
+
+ $dispatcher->removeListener(WorkerMessageFailedEvent::class, $failedListener);
+
+ return $throwable;
+ };
+
+ // send the message
+ $envelope = new Envelope(new DummyMessage('API'));
+ $bus->dispatch($envelope);
+
+ // message has been sent
+ $this->assertCount(1, $transport1->getMessagesWaitingToBeReceived());
+ $this->assertCount(1, $transport2->getMessagesWaitingToBeReceived());
+ $this->assertCount(0, $failureTransport1->getMessagesWaitingToBeReceived());
+ $this->assertCount(0, $failureTransport2->getMessagesWaitingToBeReceived());
+
+ // Receive the message from "transport1"
+ $throwable = $runWorker('transport1');
+ $this->assertInstanceOf(HandlerFailedException::class, $throwable);
+ // handler for transport1 is called
+ $this->assertSame(1, $transport1HandlerThatFails->getTimesCalled());
+ $this->assertSame(0, $transport2HandlerThatFails->getTimesCalled());
+ // one handler failed and the message is sent to the failed transport of transport1
+ $this->assertCount(1, $failureTransport1->getMessagesWaitingToBeReceived());
+ $this->assertCount(0, $failureTransport2->getMessagesWaitingToBeReceived());
+
+ // consume the failure message failed on "transport1"
+ $runWorker('the_failure_transport1');
+ // "transport1" handler is called again from the "the_failed_transport1" and it fails
+ $this->assertSame(2, $transport1HandlerThatFails->getTimesCalled());
+ $this->assertSame(0, $transport2HandlerThatFails->getTimesCalled());
+ $this->assertCount(0, $failureTransport1->getMessagesWaitingToBeReceived());
+ $this->assertCount(0, $failureTransport2->getMessagesWaitingToBeReceived());
+
+ // Receive the message from "transport2"
+ $throwable = $runWorker('transport2');
+ $this->assertInstanceOf(HandlerFailedException::class, $throwable);
+ $this->assertSame(2, $transport1HandlerThatFails->getTimesCalled());
+ // handler for "transport2" is called
+ $this->assertSame(1, $transport2HandlerThatFails->getTimesCalled());
+ $this->assertCount(0, $failureTransport1->getMessagesWaitingToBeReceived());
+ // the failure transport "the_failure_transport2" has 1 new message failed from "transport2"
+ $this->assertCount(1, $failureTransport2->getMessagesWaitingToBeReceived());
+
+ // Consume the failure message failed on "transport2"
+ $runWorker('the_failure_transport2');
+ $this->assertSame(2, $transport1HandlerThatFails->getTimesCalled());
+ // "transport2" handler is called again from the "the_failed_transport2" and it fails
+ $this->assertSame(2, $transport2HandlerThatFails->getTimesCalled());
+ $this->assertCount(0, $failureTransport1->getMessagesWaitingToBeReceived());
+ // After the message fails again, the message is discarded from the "the_failure_transport2"
+ $this->assertCount(0, $failureTransport2->getMessagesWaitingToBeReceived());
+ }
}
class DummyFailureTestSenderAndReceiver implements ReceiverInterface, SenderInterface