Skip to content

Commit 8876dbc

Browse files
committed
feature #47008 [Messenger] Add options to FailedMessagesShowCommand (Florian Guimier, fabpot)
This PR was merged into the 6.2 branch. Discussion ---------- [Messenger] Add options to `FailedMessagesShowCommand` | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tickets | Fix #39330 <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead --> | License | MIT | Doc PR | symfony/symfony-docs#14689 See #39330 Commits ------- e99d2ca Finish work 7f774b4 Add group and class-filter options to FailedMessagesShowCommand
2 parents 69e0a52 + e99d2ca commit 8876dbc

File tree

3 files changed

+109
-6
lines changed

3 files changed

+109
-6
lines changed

src/Symfony/Component/Messenger/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ CHANGELOG
2727
* Deprecate not setting the `reset_on_message` config option, its default value will change to `true` in 6.0
2828
* Add log when worker should stop.
2929
* Add log when `SIGTERM` is received.
30+
* Add `--stats` and `--class-filter` options to `FailedMessagesShowCommand`
3031

3132
5.3
3233
---

src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ protected function configure(): void
3939
new InputArgument('id', InputArgument::OPTIONAL, 'Specific message id to show'),
4040
new InputOption('max', null, InputOption::VALUE_REQUIRED, 'Maximum number of messages to list', 50),
4141
new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
42+
new InputOption('stats', null, InputOption::VALUE_NONE, 'Display the message count by class'),
43+
new InputOption('class-filter', null, InputOption::VALUE_REQUIRED, 'Filter by a specific class name'),
4244
])
4345
->setHelp(<<<'EOF'
4446
The <info>%command.name%</info> shows message that are pending in the failure transport.
@@ -77,23 +79,36 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7779
throw new RuntimeException(sprintf('The "%s" receiver does not support listing or showing specific messages.', $failureTransportName));
7880
}
7981

80-
if (null === $id = $input->getArgument('id')) {
81-
$this->listMessages($failureTransportName, $io, $input->getOption('max'));
82+
if ($input->getOption('stats')) {
83+
$this->listMessagesPerClass($failureTransportName, $io, $input->getOption('max'));
84+
} elseif (null === $id = $input->getArgument('id')) {
85+
$this->listMessages($failureTransportName, $io, $input->getOption('max'), $input->getOption('class-filter'));
8286
} else {
8387
$this->showMessage($failureTransportName, $id, $io);
8488
}
8589

8690
return 0;
8791
}
8892

89-
private function listMessages(?string $failedTransportName, SymfonyStyle $io, int $max)
93+
private function listMessages(?string $failedTransportName, SymfonyStyle $io, int $max, string $classFilter = null)
9094
{
9195
/** @var ListableReceiverInterface $receiver */
9296
$receiver = $this->getReceiver($failedTransportName);
9397
$envelopes = $receiver->all($max);
9498

9599
$rows = [];
100+
101+
if ($classFilter) {
102+
$io->comment(sprintf('Displaying only \'%s\' messages', $classFilter));
103+
}
104+
96105
foreach ($envelopes as $envelope) {
106+
$currentClassName = \get_class($envelope->getMessage());
107+
108+
if ($classFilter && $classFilter !== $currentClassName) {
109+
continue;
110+
}
111+
97112
/** @var RedeliveryStamp|null $lastRedeliveryStamp */
98113
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
99114
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
@@ -106,27 +121,58 @@ private function listMessages(?string $failedTransportName, SymfonyStyle $io, in
106121

107122
$rows[] = [
108123
$this->getMessageId($envelope),
109-
\get_class($envelope->getMessage()),
124+
$currentClassName,
110125
null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s'),
111126
$errorMessage,
112127
];
113128
}
114129

115-
if (0 === \count($rows)) {
130+
$rowsCount = \count($rows);
131+
132+
if (0 === $rowsCount) {
116133
$io->success('No failed messages were found.');
117134

118135
return;
119136
}
120137

121138
$io->table(['Id', 'Class', 'Failed at', 'Error'], $rows);
122139

123-
if (\count($rows) === $max) {
140+
if ($rowsCount === $max) {
124141
$io->comment(sprintf('Showing first %d messages.', $max));
142+
} elseif ($classFilter) {
143+
$io->comment(sprintf('Showing %d message(s).', $rowsCount));
125144
}
126145

127146
$io->comment(sprintf('Run <comment>messenger:failed:show {id} --transport=%s -vv</comment> to see message details.', $failedTransportName));
128147
}
129148

149+
private function listMessagesPerClass(?string $failedTransportName, SymfonyStyle $io, int $max)
150+
{
151+
/** @var ListableReceiverInterface $receiver */
152+
$receiver = $this->getReceiver($failedTransportName);
153+
$envelopes = $receiver->all($max);
154+
155+
$countPerClass = [];
156+
157+
foreach ($envelopes as $envelope) {
158+
$c = \get_class($envelope->getMessage());
159+
160+
if (!isset($countPerClass[$c])) {
161+
$countPerClass[$c] = [$c, 0];
162+
}
163+
164+
++$countPerClass[$c][1];
165+
}
166+
167+
if (0 === \count($countPerClass)) {
168+
$io->success('No failed messages were found.');
169+
170+
return;
171+
}
172+
173+
$io->table(['Class', 'Count'], $countPerClass);
174+
}
175+
130176
private function showMessage(?string $failedTransportName, string $id, SymfonyStyle $io)
131177
{
132178
/** @var ListableReceiverInterface $receiver */

src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,62 @@ public function testListMessagesReturnsPaginatedMessagesWithServiceLocator()
238238
$this->assertStringContainsString('Showing first 1 messages.', $tester->getDisplay(true));
239239
}
240240

241+
public function testListMessagesReturnsFilteredByClassMessage()
242+
{
243+
$sentToFailureStamp = new SentToFailureTransportStamp('async');
244+
$envelope = new Envelope(new \stdClass(), [
245+
new TransportMessageIdStamp(15),
246+
$sentToFailureStamp,
247+
new RedeliveryStamp(0),
248+
ErrorDetailsStamp::create(new \RuntimeException('Things are bad!')),
249+
]);
250+
$receiver = $this->createMock(ListableReceiverInterface::class);
251+
$receiver->method('all')->with()->willReturn([$envelope]);
252+
253+
$failureTransportName = 'failure_receiver';
254+
$serviceLocator = $this->createMock(ServiceLocator::class);
255+
$serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
256+
$serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
257+
258+
$command = new FailedMessagesShowCommand('failure_receiver', $serviceLocator);
259+
260+
$tester = new CommandTester($command);
261+
$tester->execute([]);
262+
$this->assertStringContainsString('Things are bad!', $tester->getDisplay(true));
263+
$tester->execute(['--class-filter' => 'stdClass']);
264+
$this->assertStringContainsString('Things are bad!', $tester->getDisplay(true));
265+
$this->assertStringContainsString('Showing 1 message(s).', $tester->getDisplay(true));
266+
$this->assertStringContainsString('Displaying only \'stdClass\' messages', $tester->getDisplay(true));
267+
268+
$tester->execute(['--class-filter' => 'namespace\otherClass']);
269+
$this->assertStringContainsString('[OK] No failed messages were found.', $tester->getDisplay(true));
270+
$this->assertStringContainsString('Displaying only \'namespace\otherClass\' messages', $tester->getDisplay(true));
271+
}
272+
273+
public function testListMessagesReturnsCountByClassName()
274+
{
275+
$sentToFailureStamp = new SentToFailureTransportStamp('async');
276+
$envelope = new Envelope(new \stdClass(), [
277+
new TransportMessageIdStamp(15),
278+
$sentToFailureStamp,
279+
new RedeliveryStamp(0),
280+
ErrorDetailsStamp::create(new \RuntimeException('Things are bad!')),
281+
]);
282+
$receiver = $this->createMock(ListableReceiverInterface::class);
283+
$receiver->method('all')->with()->willReturn([$envelope, $envelope]);
284+
285+
$failureTransportName = 'failure_receiver';
286+
$serviceLocator = $this->createMock(ServiceLocator::class);
287+
$serviceLocator->method('has')->with($failureTransportName)->willReturn(true);
288+
$serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver);
289+
290+
$command = new FailedMessagesShowCommand('failure_receiver', $serviceLocator);
291+
292+
$tester = new CommandTester($command);
293+
$tester->execute(['--stats' => 1]);
294+
$this->assertStringContainsString('stdClass 2', $tester->getDisplay(true));
295+
}
296+
241297
public function testInvalidMessagesThrowsExceptionWithServiceLocator()
242298
{
243299
$receiver = $this->createMock(ListableReceiverInterface::class);

0 commit comments

Comments
 (0)