diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md
index 6bc1111fc85db..000bb702fe297 100644
--- a/src/Symfony/Component/Messenger/CHANGELOG.md
+++ b/src/Symfony/Component/Messenger/CHANGELOG.md
@@ -27,6 +27,7 @@ CHANGELOG
* Deprecate not setting the `reset_on_message` config option, its default value will change to `true` in 6.0
* Add log when worker should stop.
* Add log when `SIGTERM` is received.
+ * Add `--stats` and `--class-filter` options to `FailedMessagesShowCommand`
5.3
---
diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php
index 86f453bd0d553..8abef72aecf1b 100644
--- a/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php
+++ b/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php
@@ -39,6 +39,8 @@ protected function configure(): void
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),
+ new InputOption('stats', null, InputOption::VALUE_NONE, 'Display the message count by class'),
+ new InputOption('class-filter', null, InputOption::VALUE_REQUIRED, 'Filter by a specific class name'),
])
->setHelp(<<<'EOF'
The %command.name% shows message that are pending in the failure transport.
@@ -77,8 +79,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
throw new RuntimeException(sprintf('The "%s" receiver does not support listing or showing specific messages.', $failureTransportName));
}
- if (null === $id = $input->getArgument('id')) {
- $this->listMessages($failureTransportName, $io, $input->getOption('max'));
+ if ($input->getOption('stats')) {
+ $this->listMessagesPerClass($failureTransportName, $io, $input->getOption('max'));
+ } elseif (null === $id = $input->getArgument('id')) {
+ $this->listMessages($failureTransportName, $io, $input->getOption('max'), $input->getOption('class-filter'));
} else {
$this->showMessage($failureTransportName, $id, $io);
}
@@ -86,14 +90,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return 0;
}
- private function listMessages(?string $failedTransportName, SymfonyStyle $io, int $max)
+ private function listMessages(?string $failedTransportName, SymfonyStyle $io, int $max, string $classFilter = null)
{
/** @var ListableReceiverInterface $receiver */
$receiver = $this->getReceiver($failedTransportName);
$envelopes = $receiver->all($max);
$rows = [];
+
+ if ($classFilter) {
+ $io->comment(sprintf('Displaying only \'%s\' messages', $classFilter));
+ }
+
foreach ($envelopes as $envelope) {
+ $currentClassName = \get_class($envelope->getMessage());
+
+ if ($classFilter && $classFilter !== $currentClassName) {
+ continue;
+ }
+
/** @var RedeliveryStamp|null $lastRedeliveryStamp */
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
@@ -106,13 +121,15 @@ private function listMessages(?string $failedTransportName, SymfonyStyle $io, in
$rows[] = [
$this->getMessageId($envelope),
- \get_class($envelope->getMessage()),
+ $currentClassName,
null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s'),
$errorMessage,
];
}
- if (0 === \count($rows)) {
+ $rowsCount = \count($rows);
+
+ if (0 === $rowsCount) {
$io->success('No failed messages were found.');
return;
@@ -120,13 +137,42 @@ private function listMessages(?string $failedTransportName, SymfonyStyle $io, in
$io->table(['Id', 'Class', 'Failed at', 'Error'], $rows);
- if (\count($rows) === $max) {
+ if ($rowsCount === $max) {
$io->comment(sprintf('Showing first %d messages.', $max));
+ } elseif ($classFilter) {
+ $io->comment(sprintf('Showing %d message(s).', $rowsCount));
}
$io->comment(sprintf('Run messenger:failed:show {id} --transport=%s -vv to see message details.', $failedTransportName));
}
+ private function listMessagesPerClass(?string $failedTransportName, SymfonyStyle $io, int $max)
+ {
+ /** @var ListableReceiverInterface $receiver */
+ $receiver = $this->getReceiver($failedTransportName);
+ $envelopes = $receiver->all($max);
+
+ $countPerClass = [];
+
+ foreach ($envelopes as $envelope) {
+ $c = \get_class($envelope->getMessage());
+
+ if (!isset($countPerClass[$c])) {
+ $countPerClass[$c] = [$c, 0];
+ }
+
+ ++$countPerClass[$c][1];
+ }
+
+ if (0 === \count($countPerClass)) {
+ $io->success('No failed messages were found.');
+
+ return;
+ }
+
+ $io->table(['Class', 'Count'], $countPerClass);
+ }
+
private function showMessage(?string $failedTransportName, string $id, SymfonyStyle $io)
{
/** @var ListableReceiverInterface $receiver */
diff --git a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php
index 147176ee04d99..7476970ac30f4 100644
--- a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php
+++ b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php
@@ -238,6 +238,62 @@ public function testListMessagesReturnsPaginatedMessagesWithServiceLocator()
$this->assertStringContainsString('Showing first 1 messages.', $tester->getDisplay(true));
}
+ public function testListMessagesReturnsFilteredByClassMessage()
+ {
+ $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->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('failure_receiver', $serviceLocator);
+
+ $tester = new CommandTester($command);
+ $tester->execute([]);
+ $this->assertStringContainsString('Things are bad!', $tester->getDisplay(true));
+ $tester->execute(['--class-filter' => 'stdClass']);
+ $this->assertStringContainsString('Things are bad!', $tester->getDisplay(true));
+ $this->assertStringContainsString('Showing 1 message(s).', $tester->getDisplay(true));
+ $this->assertStringContainsString('Displaying only \'stdClass\' messages', $tester->getDisplay(true));
+
+ $tester->execute(['--class-filter' => 'namespace\otherClass']);
+ $this->assertStringContainsString('[OK] No failed messages were found.', $tester->getDisplay(true));
+ $this->assertStringContainsString('Displaying only \'namespace\otherClass\' messages', $tester->getDisplay(true));
+ }
+
+ public function testListMessagesReturnsCountByClassName()
+ {
+ $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->method('all')->with()->willReturn([$envelope, $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('failure_receiver', $serviceLocator);
+
+ $tester = new CommandTester($command);
+ $tester->execute(['--stats' => 1]);
+ $this->assertStringContainsString('stdClass 2', $tester->getDisplay(true));
+ }
+
public function testInvalidMessagesThrowsExceptionWithServiceLocator()
{
$receiver = $this->createMock(ListableReceiverInterface::class);