vendor/symfony/messenger/Command/AbstractFailedMessagesCommand.php line 53

  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Messenger\Command;
  11. use Symfony\Component\Console\Command\Command;
  12. use Symfony\Component\Console\Completion\CompletionInput;
  13. use Symfony\Component\Console\Completion\CompletionSuggestions;
  14. use Symfony\Component\Console\Helper\Dumper;
  15. use Symfony\Component\Console\Question\ChoiceQuestion;
  16. use Symfony\Component\Console\Style\SymfonyStyle;
  17. use Symfony\Component\ErrorHandler\Exception\FlattenException;
  18. use Symfony\Component\Messenger\Envelope;
  19. use Symfony\Component\Messenger\Exception\InvalidArgumentException;
  20. use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
  21. use Symfony\Component\Messenger\Stamp\MessageDecodingFailedStamp;
  22. use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
  23. use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
  24. use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
  25. use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
  26. use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;
  27. use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
  28. use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
  29. use Symfony\Component\VarDumper\Caster\Caster;
  30. use Symfony\Component\VarDumper\Caster\TraceStub;
  31. use Symfony\Component\VarDumper\Cloner\ClonerInterface;
  32. use Symfony\Component\VarDumper\Cloner\Stub;
  33. use Symfony\Component\VarDumper\Cloner\VarCloner;
  34. use Symfony\Contracts\Service\ServiceProviderInterface;
  35. /**
  36.  * @author Ryan Weaver <ryan@symfonycasts.com>
  37.  *
  38.  * @internal
  39.  */
  40. abstract class AbstractFailedMessagesCommand extends Command
  41. {
  42.     protected const DEFAULT_TRANSPORT_OPTION 'choose';
  43.     protected $failureTransports;
  44.     protected ?PhpSerializer $phpSerializer;
  45.     private ?string $globalFailureReceiverName;
  46.     public function __construct(?string $globalFailureReceiverNameServiceProviderInterface $failureTransportsPhpSerializer $phpSerializer null)
  47.     {
  48.         $this->failureTransports $failureTransports;
  49.         $this->globalFailureReceiverName $globalFailureReceiverName;
  50.         $this->phpSerializer $phpSerializer;
  51.         parent::__construct();
  52.     }
  53.     protected function getGlobalFailureReceiverName(): ?string
  54.     {
  55.         return $this->globalFailureReceiverName;
  56.     }
  57.     protected function getMessageId(Envelope $envelope): mixed
  58.     {
  59.         /** @var TransportMessageIdStamp $stamp */
  60.         $stamp $envelope->last(TransportMessageIdStamp::class);
  61.         return $stamp?->getId();
  62.     }
  63.     protected function displaySingleMessage(Envelope $envelopeSymfonyStyle $io)
  64.     {
  65.         $io->title('Failed Message Details');
  66.         /** @var SentToFailureTransportStamp|null $sentToFailureTransportStamp */
  67.         $sentToFailureTransportStamp $envelope->last(SentToFailureTransportStamp::class);
  68.         /** @var RedeliveryStamp|null $lastRedeliveryStamp */
  69.         $lastRedeliveryStamp $envelope->last(RedeliveryStamp::class);
  70.         /** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
  71.         $lastErrorDetailsStamp $envelope->last(ErrorDetailsStamp::class);
  72.         /** @var MessageDecodingFailedStamp|null $lastMessageDecodingFailedStamp */
  73.         $lastMessageDecodingFailedStamp $envelope->last(MessageDecodingFailedStamp::class);
  74.         $rows = [
  75.             ['Class'\get_class($envelope->getMessage())],
  76.         ];
  77.         if (null !== $id $this->getMessageId($envelope)) {
  78.             $rows[] = ['Message Id'$id];
  79.         }
  80.         if (null === $sentToFailureTransportStamp) {
  81.             $io->warning('Message does not appear to have been sent to this transport after failing');
  82.         } else {
  83.             $failedAt '';
  84.             $errorMessage '';
  85.             $errorCode '';
  86.             $errorClass '(unknown)';
  87.             if (null !== $lastRedeliveryStamp) {
  88.                 $failedAt $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s');
  89.             }
  90.             if (null !== $lastErrorDetailsStamp) {
  91.                 $errorMessage $lastErrorDetailsStamp->getExceptionMessage();
  92.                 $errorCode $lastErrorDetailsStamp->getExceptionCode();
  93.                 $errorClass $lastErrorDetailsStamp->getExceptionClass();
  94.             }
  95.             $rows array_merge($rows, [
  96.                 ['Failed at'$failedAt],
  97.                 ['Error'$errorMessage],
  98.                 ['Error Code'$errorCode],
  99.                 ['Error Class'$errorClass],
  100.                 ['Transport'$sentToFailureTransportStamp->getOriginalReceiverName()],
  101.             ]);
  102.         }
  103.         $io->table([], $rows);
  104.         /** @var RedeliveryStamp[] $redeliveryStamps */
  105.         $redeliveryStamps $envelope->all(RedeliveryStamp::class);
  106.         $io->writeln(' Message history:');
  107.         foreach ($redeliveryStamps as $redeliveryStamp) {
  108.             $io->writeln(sprintf('  * Message failed at <info>%s</info> and was redelivered'$redeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s')));
  109.         }
  110.         $io->newLine();
  111.         if ($io->isVeryVerbose()) {
  112.             $io->title('Message:');
  113.             if (null !== $lastMessageDecodingFailedStamp) {
  114.                 $io->error('The message could not be decoded. See below an APPROXIMATIVE representation of the class.');
  115.             }
  116.             $dump = new Dumper($ionull$this->createCloner());
  117.             $io->writeln($dump($envelope->getMessage()));
  118.             $io->title('Exception:');
  119.             $flattenException $lastErrorDetailsStamp?->getFlattenException();
  120.             $io->writeln(null === $flattenException '(no data)' $dump($flattenException));
  121.         } else {
  122.             if (null !== $lastMessageDecodingFailedStamp) {
  123.                 $io->error('The message could not be decoded.');
  124.             }
  125.             $io->writeln(' Re-run command with <info>-vv</info> to see more message & error details.');
  126.         }
  127.     }
  128.     protected function printPendingMessagesMessage(ReceiverInterface $receiverSymfonyStyle $io)
  129.     {
  130.         if ($receiver instanceof MessageCountAwareInterface) {
  131.             if (=== $receiver->getMessageCount()) {
  132.                 $io->writeln('There is <comment>1</comment> message pending in the failure transport.');
  133.             } else {
  134.                 $io->writeln(sprintf('There are <comment>%d</comment> messages pending in the failure transport.'$receiver->getMessageCount()));
  135.             }
  136.         }
  137.     }
  138.     protected function getReceiver(string $name null): ReceiverInterface
  139.     {
  140.         if (null === $name ??= $this->globalFailureReceiverName) {
  141.             throw new InvalidArgumentException(sprintf('No default failure transport is defined. Available transports are: "%s".'implode('", "'array_keys($this->failureTransports->getProvidedServices()))));
  142.         }
  143.         if (!$this->failureTransports->has($name)) {
  144.             throw new InvalidArgumentException(sprintf('The "%s" failure transport was not found. Available transports are: "%s".'$nameimplode('", "'array_keys($this->failureTransports->getProvidedServices()))));
  145.         }
  146.         return $this->failureTransports->get($name);
  147.     }
  148.     private function createCloner(): ?ClonerInterface
  149.     {
  150.         if (!class_exists(VarCloner::class)) {
  151.             return null;
  152.         }
  153.         $cloner = new VarCloner();
  154.         $cloner->addCasters([FlattenException::class => function (FlattenException $flattenException, array $aStub $stub): array {
  155.             $stub->class $flattenException->getClass();
  156.             return [
  157.                 Caster::PREFIX_VIRTUAL.'message' => $flattenException->getMessage(),
  158.                 Caster::PREFIX_VIRTUAL.'code' => $flattenException->getCode(),
  159.                 Caster::PREFIX_VIRTUAL.'file' => $flattenException->getFile(),
  160.                 Caster::PREFIX_VIRTUAL.'line' => $flattenException->getLine(),
  161.                 Caster::PREFIX_VIRTUAL.'trace' => new TraceStub($flattenException->getTrace()),
  162.             ];
  163.         }]);
  164.         return $cloner;
  165.     }
  166.     protected function printWarningAvailableFailureTransports(SymfonyStyle $io, ?string $failureTransportName): void
  167.     {
  168.         $failureTransports array_keys($this->failureTransports->getProvidedServices());
  169.         $failureTransportsCount \count($failureTransports);
  170.         if ($failureTransportsCount 1) {
  171.             $io->writeln([
  172.                 sprintf('> Loading messages from the <comment>global</comment> failure transport <comment>%s</comment>.'$failureTransportName),
  173.                 '> To use a different failure transport, pass <comment>--transport=</comment>.',
  174.                 sprintf('> Available failure transports are: <comment>%s</comment>'implode(', '$failureTransports)),
  175.                 "\n",
  176.             ]);
  177.         }
  178.     }
  179.     protected function interactiveChooseFailureTransport(SymfonyStyle $io)
  180.     {
  181.         $failedTransports array_keys($this->failureTransports->getProvidedServices());
  182.         $question = new ChoiceQuestion('Select failed transport:'$failedTransports0);
  183.         $question->setMultiselect(false);
  184.         return $io->askQuestion($question);
  185.     }
  186.     public function complete(CompletionInput $inputCompletionSuggestions $suggestions): void
  187.     {
  188.         if ($input->mustSuggestOptionValuesFor('transport')) {
  189.             $suggestions->suggestValues(array_keys($this->failureTransports->getProvidedServices()));
  190.             return;
  191.         }
  192.         if ($input->mustSuggestArgumentValuesFor('id')) {
  193.             $transport $input->getOption('transport');
  194.             $transport self::DEFAULT_TRANSPORT_OPTION === $transport $this->getGlobalFailureReceiverName() : $transport;
  195.             $receiver $this->getReceiver($transport);
  196.             if (!$receiver instanceof ListableReceiverInterface) {
  197.                 return;
  198.             }
  199.             $ids = [];
  200.             foreach ($receiver->all(50) as $envelope) {
  201.                 $ids[] = $this->getMessageId($envelope);
  202.             }
  203.             $suggestions->suggestValues($ids);
  204.             return;
  205.         }
  206.     }
  207. }