From 1924719f580665671ced8f16746418856dce6be4 Mon Sep 17 00:00:00 2001 From: Arnaud De Abreu Date: Fri, 14 Mar 2025 15:42:59 +0100 Subject: [PATCH] [Messenger] Add `--class-filter` option to the `messenger:failed:remove` command --- src/Symfony/Component/Messenger/CHANGELOG.md | 1 + .../Command/FailedMessagesRemoveCommand.php | 43 +++++++++++++++-- .../FailedMessagesRemoveCommandTest.php | 47 +++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index b7973518fe41d..21fef52a1edd6 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Add `CloseableTransportInterface` to allow closing the transport * Add `SentForRetryStamp` that identifies whether a failed message was sent for retry * Add `Symfony\Component\Messenger\Middleware\DeduplicateMiddleware` and `Symfony\Component\Messenger\Stamp\DeduplicateStamp` + * Add `--class-filter` option to the `messenger:failed:remove` command 7.2 --- diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php index 4c8d44e9b72fb..de2b6f3f14d12 100644 --- a/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php +++ b/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php @@ -37,6 +37,7 @@ protected function configure(): void new InputOption('force', null, InputOption::VALUE_NONE, 'Force the operation without confirmation'), new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION), new InputOption('show-messages', null, InputOption::VALUE_NONE, 'Display messages before removing it (if multiple ids are given)'), + new InputOption('class-filter', null, InputOption::VALUE_REQUIRED, 'Filter by a specific class name'), ]) ->setHelp(<<<'EOF' The %command.name% removes given messages that are pending in the failure transport. @@ -69,6 +70,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int $shouldDeleteAllMessages = $input->getOption('all'); $idsCount = \count($ids); + + if (!$receiver instanceof ListableReceiverInterface) { + throw new RuntimeException(\sprintf('The "%s" receiver does not support removing specific messages.', $failureTransportName)); + } + + if (!$idsCount && null !== $input->getOption('class-filter')) { + $ids = $this->getMessageIdsByClassFilter($input->getOption('class-filter'), $receiver); + $idsCount = \count($ids); + + if (!$idsCount) { + throw new RuntimeException('No failed messages were found with this filter.'); + } + + if (!$io->confirm(\sprintf('Can you confirm you want to remove %d message%s?', $idsCount, 1 === $idsCount ? '' : 's'))) { + return 0; + } + } + if (!$shouldDeleteAllMessages && !$idsCount) { throw new RuntimeException('Please specify at least one message id. If you want to remove all failed messages, use the "--all" option.'); } elseif ($shouldDeleteAllMessages && $idsCount) { @@ -77,10 +96,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $shouldDisplayMessages = $input->getOption('show-messages') || 1 === $idsCount; - if (!$receiver instanceof ListableReceiverInterface) { - throw new RuntimeException(\sprintf('The "%s" receiver does not support removing specific messages.', $failureTransportName)); - } - if ($shouldDeleteAllMessages) { $this->removeAllMessages($receiver, $io, $shouldForce, $shouldDisplayMessages); } else { @@ -119,6 +134,26 @@ private function removeMessagesById(array $ids, ListableReceiverInterface $recei } } + private function getMessageIdsByClassFilter(string $classFilter, ListableReceiverInterface $receiver): array + { + $ids = []; + + $this->phpSerializer?->acceptPhpIncompleteClass(); + try { + foreach ($receiver->all() as $envelope) { + if ($classFilter !== $envelope->getMessage()::class) { + continue; + } + + $ids[] = $this->getMessageId($envelope); + }; + } finally { + $this->phpSerializer?->rejectPhpIncompleteClass(); + } + + return $ids; + } + private function removeAllMessages(ListableReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce, bool $shouldDisplayMessages): void { if (!$shouldForce) { diff --git a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRemoveCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRemoveCommandTest.php index bb8365d351637..64812a74aa91f 100644 --- a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRemoveCommandTest.php +++ b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRemoveCommandTest.php @@ -182,6 +182,53 @@ public function testRemoveMultipleMessagesAndDisplayMessagesWithServiceLocator() $this->assertStringContainsString('Message with id 30 removed.', $tester->getDisplay()); } + public function testRemoveMessagesFilteredByClassMessage() + { + $globalFailureReceiverName = 'failure_receiver'; + $receiver = $this->createMock(ListableReceiverInterface::class); + + $anotherClass = new class extends \stdClass {}; + + $series = [ + new Envelope(new \stdClass(), [new TransportMessageIdStamp(10)]), + new Envelope(new $anotherClass(), [new TransportMessageIdStamp(20)]), + new Envelope(new \stdClass(), [new TransportMessageIdStamp(30)]), + ]; + $receiver->expects($this->once())->method('all')->willReturn($series); + + $expectedRemovedIds = [10, 30]; + $receiver->expects($this->exactly(2))->method('find') + ->willReturnCallback(function (...$args) use ($series, &$expectedRemovedIds) { + $expectedArgs = array_shift($expectedRemovedIds); + $this->assertSame([$expectedArgs], $args); + + $return = array_filter( + $series, + static fn (Envelope $envelope) => [$envelope->last(TransportMessageIdStamp::class)->getId()] === $args, + ); + + return current($return); + }) + ; + + $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(['--class-filter' => "stdClass", '--force' => true, '--show-messages' => true]); + + $this->assertStringContainsString('There is 2 messages to remove. Do you want to continue? (yes/no)', $tester->getDisplay()); + $this->assertStringContainsString('Failed Message Details', $tester->getDisplay()); + $this->assertStringContainsString('Message with id 10 removed.', $tester->getDisplay()); + $this->assertStringContainsString('Message with id 30 removed.', $tester->getDisplay()); + } + public function testCompletingTransport() { $globalFailureReceiverName = 'failure_receiver';