Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Attribute;
/**
* Service tag to autoconfigure message handlers.
*
* @author Alireza Mirsepassi <alirezamirsepassi@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class AsMessageHandler
{
public function __construct(
public ?string $bus = null,
public ?string $fromTransport = null,
public ?string $handles = null,
public ?string $method = null,
public int $priority = 0,
) {
}
}

View File

@@ -0,0 +1,246 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Helper\Dumper;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
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\MessageDecodingFailedStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
use Symfony\Component\VarDumper\Caster\Caster;
use Symfony\Component\VarDumper\Caster\TraceStub;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Cloner\Stub;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Contracts\Service\ServiceProviderInterface;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*
* @internal
*/
abstract class AbstractFailedMessagesCommand extends Command
{
protected const DEFAULT_TRANSPORT_OPTION = 'choose';
protected ServiceProviderInterface $failureTransports;
protected ?PhpSerializer $phpSerializer;
private ?string $globalFailureReceiverName;
public function __construct(?string $globalFailureReceiverName, ServiceProviderInterface $failureTransports, ?PhpSerializer $phpSerializer = null)
{
$this->failureTransports = $failureTransports;
$this->globalFailureReceiverName = $globalFailureReceiverName;
$this->phpSerializer = $phpSerializer;
parent::__construct();
}
protected function getGlobalFailureReceiverName(): ?string
{
return $this->globalFailureReceiverName;
}
protected function getMessageId(Envelope $envelope): mixed
{
/** @var TransportMessageIdStamp $stamp */
$stamp = $envelope->last(TransportMessageIdStamp::class);
return $stamp?->getId();
}
protected function displaySingleMessage(Envelope $envelope, SymfonyStyle $io): void
{
$io->title('Failed Message Details');
/** @var SentToFailureTransportStamp|null $sentToFailureTransportStamp */
$sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class);
/** @var RedeliveryStamp|null $lastRedeliveryStamp */
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
$lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class);
/** @var MessageDecodingFailedStamp|null $lastMessageDecodingFailedStamp */
$lastMessageDecodingFailedStamp = $envelope->last(MessageDecodingFailedStamp::class);
$rows = [
['Class', $envelope->getMessage()::class],
];
if (null !== $id = $this->getMessageId($envelope)) {
$rows[] = ['Message Id', $id];
}
if (null === $sentToFailureTransportStamp) {
$io->warning('Message does not appear to have been sent to this transport after failing');
} else {
$failedAt = '';
$errorMessage = '';
$errorCode = '';
$errorClass = '(unknown)';
if (null !== $lastRedeliveryStamp) {
$failedAt = $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s');
}
if (null !== $lastErrorDetailsStamp) {
$errorMessage = $lastErrorDetailsStamp->getExceptionMessage();
$errorCode = $lastErrorDetailsStamp->getExceptionCode();
$errorClass = $lastErrorDetailsStamp->getExceptionClass();
}
$rows = array_merge($rows, [
['Failed at', $failedAt],
['Error', $errorMessage],
['Error Code', $errorCode],
['Error Class', $errorClass],
['Transport', $sentToFailureTransportStamp->getOriginalReceiverName()],
]);
}
$io->table([], $rows);
/** @var RedeliveryStamp[] $redeliveryStamps */
$redeliveryStamps = $envelope->all(RedeliveryStamp::class);
$io->writeln(' Message history:');
foreach ($redeliveryStamps as $redeliveryStamp) {
$io->writeln(\sprintf(' * Message failed at <info>%s</info> and was redelivered', $redeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s')));
}
$io->newLine();
if ($io->isVeryVerbose()) {
$io->title('Message:');
if (null !== $lastMessageDecodingFailedStamp) {
$io->error('The message could not be decoded. See below an APPROXIMATIVE representation of the class.');
}
$dump = new Dumper($io, null, $this->createCloner());
$io->writeln($dump($envelope->getMessage()));
$io->title('Exception:');
$flattenException = $lastErrorDetailsStamp?->getFlattenException();
$io->writeln(null === $flattenException ? '(no data)' : $dump($flattenException));
} else {
if (null !== $lastMessageDecodingFailedStamp) {
$io->error('The message could not be decoded.');
}
$io->writeln(' Re-run command with <info>-vv</info> to see more message & error details.');
}
}
protected function printPendingMessagesMessage(ReceiverInterface $receiver, SymfonyStyle $io): void
{
if ($receiver instanceof MessageCountAwareInterface) {
if (1 === $receiver->getMessageCount()) {
$io->writeln('There is <comment>1</comment> message pending in the failure transport.');
} else {
$io->writeln(\sprintf('There are <comment>%d</comment> messages pending in the failure transport.', $receiver->getMessageCount()));
}
}
}
protected function getReceiver(?string $name = null): ReceiverInterface
{
if (null === $name ??= $this->globalFailureReceiverName) {
throw new InvalidArgumentException(\sprintf('No default failure transport is defined. Available transports are: "%s".', implode('", "', array_keys($this->failureTransports->getProvidedServices()))));
}
if (!$this->failureTransports->has($name)) {
throw new InvalidArgumentException(\sprintf('The "%s" failure transport was not found. Available transports are: "%s".', $name, implode('", "', array_keys($this->failureTransports->getProvidedServices()))));
}
return $this->failureTransports->get($name);
}
private function createCloner(): ?ClonerInterface
{
if (!class_exists(VarCloner::class)) {
return null;
}
$cloner = new VarCloner();
$cloner->addCasters([FlattenException::class => function (FlattenException $flattenException, array $a, Stub $stub): array {
$stub->class = $flattenException->getClass();
return [
Caster::PREFIX_VIRTUAL.'message' => $flattenException->getMessage(),
Caster::PREFIX_VIRTUAL.'code' => $flattenException->getCode(),
Caster::PREFIX_VIRTUAL.'file' => $flattenException->getFile(),
Caster::PREFIX_VIRTUAL.'line' => $flattenException->getLine(),
Caster::PREFIX_VIRTUAL.'trace' => new TraceStub($flattenException->getTrace()),
];
}]);
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 <comment>global</comment> failure transport <comment>%s</comment>.', $failureTransportName),
'> To use a different failure transport, pass <comment>--transport=</comment>.',
\sprintf('> Available failure transports are: <comment>%s</comment>', implode(', ', $failureTransports)),
"\n",
]);
}
}
protected function interactiveChooseFailureTransport(SymfonyStyle $io): string
{
$failedTransports = array_keys($this->failureTransports->getProvidedServices());
$question = new ChoiceQuestion('Select failed transport:', $failedTransports, 0);
$question->setMultiselect(false);
return $io->askQuestion($question);
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestOptionValuesFor('transport')) {
$suggestions->suggestValues(array_keys($this->failureTransports->getProvidedServices()));
return;
}
if ($input->mustSuggestArgumentValuesFor('id')) {
$transport = $input->getOption('transport');
$transport = self::DEFAULT_TRANSPORT_OPTION === $transport ? $this->getGlobalFailureReceiverName() : $transport;
$receiver = $this->getReceiver($transport);
if (!$receiver instanceof ListableReceiverInterface) {
return;
}
$ids = [];
foreach ($receiver->all(50) as $envelope) {
$ids[] = $this->getMessageId($envelope);
}
$suggestions->suggestValues($ids);
return;
}
}
}

View File

@@ -0,0 +1,307 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Messenger\EventListener\ResetServicesListener;
use Symfony\Component\Messenger\EventListener\StopWorkerOnFailureLimitListener;
use Symfony\Component\Messenger\EventListener\StopWorkerOnMemoryLimitListener;
use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener;
use Symfony\Component\Messenger\EventListener\StopWorkerOnTimeLimitListener;
use Symfony\Component\Messenger\RoutableMessageBus;
use Symfony\Component\Messenger\Worker;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
#[AsCommand(name: 'messenger:consume', description: 'Consume messages')]
class ConsumeMessagesCommand extends Command implements SignalableCommandInterface
{
private RoutableMessageBus $routableBus;
private ContainerInterface $receiverLocator;
private EventDispatcherInterface $eventDispatcher;
private ?LoggerInterface $logger;
private array $receiverNames;
private ?ResetServicesListener $resetServicesListener;
private array $busIds;
private ?ContainerInterface $rateLimiterLocator;
private ?array $signals;
private ?Worker $worker = null;
public function __construct(RoutableMessageBus $routableBus, ContainerInterface $receiverLocator, EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null, array $receiverNames = [], ?ResetServicesListener $resetServicesListener = null, array $busIds = [], ?ContainerInterface $rateLimiterLocator = null, ?array $signals = null)
{
$this->routableBus = $routableBus;
$this->receiverLocator = $receiverLocator;
$this->eventDispatcher = $eventDispatcher;
$this->logger = $logger;
$this->receiverNames = $receiverNames;
$this->resetServicesListener = $resetServicesListener;
$this->busIds = $busIds;
$this->rateLimiterLocator = $rateLimiterLocator;
$this->signals = $signals;
parent::__construct();
}
protected function configure(): void
{
$defaultReceiverName = 1 === \count($this->receiverNames) ? current($this->receiverNames) : null;
$this
->setDefinition([
new InputArgument('receivers', InputArgument::IS_ARRAY, 'Names of the receivers/transports to consume in order of priority', $defaultReceiverName ? [$defaultReceiverName] : []),
new InputOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limit the number of received messages'),
new InputOption('failure-limit', 'f', InputOption::VALUE_REQUIRED, 'The number of failed messages the worker can consume'),
new InputOption('memory-limit', 'm', InputOption::VALUE_REQUIRED, 'The memory limit the worker can consume'),
new InputOption('time-limit', 't', InputOption::VALUE_REQUIRED, 'The time limit in seconds the worker can handle new messages'),
new InputOption('sleep', null, InputOption::VALUE_REQUIRED, 'Seconds to sleep before asking for new messages after no messages were found', 1),
new InputOption('bus', 'b', InputOption::VALUE_REQUIRED, 'Name of the bus to which received messages should be dispatched (if not passed, bus is determined automatically)'),
new InputOption('queues', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Limit receivers to only consume from the specified queues'),
new InputOption('no-reset', null, InputOption::VALUE_NONE, 'Do not reset container services after each message'),
])
->setHelp(<<<'EOF'
The <info>%command.name%</info> command consumes messages and dispatches them to the message bus.
<info>php %command.full_name% <receiver-name></info>
To receive from multiple transports, pass each name:
<info>php %command.full_name% receiver1 receiver2</info>
Use the --limit option to limit the number of messages received:
<info>php %command.full_name% <receiver-name> --limit=10</info>
Use the --failure-limit option to stop the worker when the given number of failed messages is reached:
<info>php %command.full_name% <receiver-name> --failure-limit=2</info>
Use the --memory-limit option to stop the worker if it exceeds a given memory usage limit. You can use shorthand byte values [K, M or G]:
<info>php %command.full_name% <receiver-name> --memory-limit=128M</info>
Use the --time-limit option to stop the worker when the given time limit (in seconds) is reached.
If a message is being handled, the worker will stop after the processing is finished:
<info>php %command.full_name% <receiver-name> --time-limit=3600</info>
Use the --bus option to specify the message bus to dispatch received messages
to instead of trying to determine it automatically. This is required if the
messages didn't originate from Messenger:
<info>php %command.full_name% <receiver-name> --bus=event_bus</info>
Use the --queues option to limit a receiver to only certain queues (only supported by some receivers):
<info>php %command.full_name% <receiver-name> --queues=fasttrack</info>
Use the --no-reset option to prevent services resetting after each message (may lead to leaking services' state between messages):
<info>php %command.full_name% <receiver-name> --no-reset</info>
EOF
)
;
}
/**
* @return void
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
if ($this->receiverNames && !$input->getArgument('receivers')) {
if (1 === \count($this->receiverNames)) {
$input->setArgument('receivers', $this->receiverNames);
return;
}
$io->block('Which transports/receivers do you want to consume?', null, 'fg=white;bg=blue', ' ', true);
$io->writeln('Choose which receivers you want to consume messages from in order of priority.');
if (\count($this->receiverNames) > 1) {
$io->writeln(\sprintf('Hint: to consume from multiple, use a list of their names, e.g. <comment>%s</comment>', implode(', ', $this->receiverNames)));
}
$question = new ChoiceQuestion('Select receivers to consume:', $this->receiverNames, 0);
$question->setMultiselect(true);
$input->setArgument('receivers', $io->askQuestion($question));
}
if (!$input->getArgument('receivers')) {
throw new RuntimeException('Please pass at least one receiver.');
}
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$receivers = [];
$rateLimiters = [];
foreach ($receiverNames = $input->getArgument('receivers') as $receiverName) {
if (!$this->receiverLocator->has($receiverName)) {
$message = \sprintf('The receiver "%s" does not exist.', $receiverName);
if ($this->receiverNames) {
$message .= \sprintf(' Valid receivers are: %s.', implode(', ', $this->receiverNames));
}
throw new RuntimeException($message);
}
$receivers[$receiverName] = $this->receiverLocator->get($receiverName);
if ($this->rateLimiterLocator?->has($receiverName)) {
$rateLimiters[$receiverName] = $this->rateLimiterLocator->get($receiverName);
}
}
if (null !== $this->resetServicesListener && !$input->getOption('no-reset')) {
$this->eventDispatcher->addSubscriber($this->resetServicesListener);
}
$stopsWhen = [];
if (null !== $limit = $input->getOption('limit')) {
if (!is_numeric($limit) || 0 >= $limit) {
throw new InvalidOptionException(\sprintf('Option "limit" must be a positive integer, "%s" passed.', $limit));
}
$stopsWhen[] = "processed {$limit} messages";
$this->eventDispatcher->addSubscriber(new StopWorkerOnMessageLimitListener($limit, $this->logger));
}
if ($failureLimit = $input->getOption('failure-limit')) {
$stopsWhen[] = "reached {$failureLimit} failed messages";
$this->eventDispatcher->addSubscriber(new StopWorkerOnFailureLimitListener($failureLimit, $this->logger));
}
if ($memoryLimit = $input->getOption('memory-limit')) {
$stopsWhen[] = "exceeded {$memoryLimit} of memory";
$this->eventDispatcher->addSubscriber(new StopWorkerOnMemoryLimitListener($this->convertToBytes($memoryLimit), $this->logger));
}
if (null !== $timeLimit = $input->getOption('time-limit')) {
if (!is_numeric($timeLimit) || 0 >= $timeLimit) {
throw new InvalidOptionException(\sprintf('Option "time-limit" must be a positive integer, "%s" passed.', $timeLimit));
}
$stopsWhen[] = "been running for {$timeLimit}s";
$this->eventDispatcher->addSubscriber(new StopWorkerOnTimeLimitListener($timeLimit, $this->logger));
}
$stopsWhen[] = 'received a stop signal via the messenger:stop-workers command';
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
$io->success(\sprintf('Consuming messages from transport%s "%s".', \count($receivers) > 1 ? 's' : '', implode(', ', $receiverNames)));
if ($stopsWhen) {
$last = array_pop($stopsWhen);
$stopsWhen = ($stopsWhen ? implode(', ', $stopsWhen).' or ' : '').$last;
$io->comment("The worker will automatically exit once it has {$stopsWhen}.");
}
$io->comment('Quit the worker with CONTROL-C.');
if (OutputInterface::VERBOSITY_VERBOSE > $output->getVerbosity()) {
$io->comment('Re-run the command with a -vv option to see logs about consumed messages.');
}
$bus = $input->getOption('bus') ? $this->routableBus->getMessageBus($input->getOption('bus')) : $this->routableBus;
$this->worker = new Worker($receivers, $bus, $this->eventDispatcher, $this->logger, $rateLimiters);
$options = [
'sleep' => $input->getOption('sleep') * 1000000,
];
if ($queues = $input->getOption('queues')) {
$options['queues'] = $queues;
}
try {
$this->worker->run($options);
} finally {
$this->worker = null;
}
return 0;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('receivers')) {
$suggestions->suggestValues(array_diff($this->receiverNames, array_diff($input->getArgument('receivers'), [$input->getCompletionValue()])));
return;
}
if ($input->mustSuggestOptionValuesFor('bus')) {
$suggestions->suggestValues($this->busIds);
}
}
public function getSubscribedSignals(): array
{
return $this->signals ?? (\extension_loaded('pcntl') ? [\SIGTERM, \SIGINT] : []);
}
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
{
if (!$this->worker) {
return false;
}
$this->logger?->info('Received signal {signal}.', ['signal' => $signal, 'transport_names' => $this->worker->getMetadata()->getTransportNames()]);
$this->worker->stop();
return false;
}
private function convertToBytes(string $memoryLimit): int
{
$memoryLimit = strtolower($memoryLimit);
$max = ltrim($memoryLimit, '+');
if (str_starts_with($max, '0x')) {
$max = \intval($max, 16);
} elseif (str_starts_with($max, '0')) {
$max = \intval($max, 8);
} else {
$max = (float) $max;
}
switch (substr(rtrim($memoryLimit, 'b'), -1)) {
case 't': $max *= 1024;
// no break
case 'g': $max *= 1024;
// no break
case 'm': $max *= 1024;
// no break
case 'k': $max *= 1024;
}
return (int) $max;
}
}

View File

@@ -0,0 +1,145 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* A console command to debug Messenger information.
*
* @author Roland Franssen <franssen.roland@gmail.com>
*/
#[AsCommand(name: 'debug:messenger', description: 'List messages you can dispatch using the message buses')]
class DebugCommand extends Command
{
private array $mapping;
public function __construct(array $mapping)
{
$this->mapping = $mapping;
parent::__construct();
}
/**
* @return void
*/
protected function configure()
{
$this
->addArgument('bus', InputArgument::OPTIONAL, \sprintf('The bus id (one of "%s")', implode('", "', array_keys($this->mapping))))
->setHelp(<<<'EOF'
The <info>%command.name%</info> command displays all messages that can be
dispatched using the message buses:
<info>php %command.full_name%</info>
Or for a specific bus only:
<info>php %command.full_name% command_bus</info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Messenger');
$mapping = $this->mapping;
if ($bus = $input->getArgument('bus')) {
if (!isset($mapping[$bus])) {
throw new RuntimeException(\sprintf('Bus "%s" does not exist. Known buses are "%s".', $bus, implode('", "', array_keys($this->mapping))));
}
$mapping = [$bus => $mapping[$bus]];
}
foreach ($mapping as $bus => $handlersByMessage) {
$io->section($bus);
$tableRows = [];
foreach ($handlersByMessage as $message => $handlers) {
if ($description = self::getClassDescription($message)) {
$tableRows[] = [\sprintf('<comment>%s</>', $description)];
}
$tableRows[] = [\sprintf('<fg=cyan>%s</fg=cyan>', $message)];
foreach ($handlers as $handler) {
$tableRows[] = [
\sprintf(' handled by <info>%s</>', $handler[0]).$this->formatConditions($handler[1]),
];
if ($handlerDescription = self::getClassDescription($handler[0])) {
$tableRows[] = [\sprintf(' <comment>%s</>', $handlerDescription)];
}
}
$tableRows[] = [''];
}
if ($tableRows) {
$io->text('The following messages can be dispatched:');
$io->newLine();
$io->table([], $tableRows);
} else {
$io->warning(\sprintf('No handled message found in bus "%s".', $bus));
}
}
return 0;
}
private function formatConditions(array $options): string
{
if (!$options) {
return '';
}
$optionsMapping = [];
foreach ($options as $key => $value) {
$optionsMapping[] = $key.'='.$value;
}
return ' (when '.implode(', ', $optionsMapping).')';
}
private static function getClassDescription(string $class): string
{
try {
$r = new \ReflectionClass($class);
if ($docComment = $r->getDocComment()) {
$docComment = preg_split('#\n\s*\*\s*[\n@]#', substr($docComment, 3, -2), 2)[0];
return trim(preg_replace('#\s*\n\s*\*\s*#', ' ', $docComment));
}
} catch (\ReflectionException) {
}
return '';
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('bus')) {
$suggestions->suggestValues(array_keys($this->mapping));
}
}
}

View File

@@ -0,0 +1,148 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
#[AsCommand(name: 'messenger:failed:remove', description: 'Remove given messages from the failure transport')]
class FailedMessagesRemoveCommand extends AbstractFailedMessagesCommand
{
protected function configure(): void
{
$this
->setDefinition([
new InputArgument('id', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Specific message id(s) to remove'),
new InputOption('all', null, InputOption::VALUE_NONE, 'Remove all failed messages from the transport'),
new InputOption('force', null, InputOption::VALUE_NONE, 'Force the operation without confirmation'),
new InputOption('transport', null, InputOption::VALUE_REQUIRED, '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)'),
])
->setHelp(<<<'EOF'
The <info>%command.name%</info> removes given messages that are pending in the failure transport.
<info>php %command.full_name% {id1} [{id2} ...]</info>
The specific ids can be found via the messenger:failed:show command.
You can remove all failed messages from the failure transport by using the "--all" option:
<info>php %command.full_name% --all</info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
$failureTransportName = $input->getOption('transport');
if (self::DEFAULT_TRANSPORT_OPTION === $failureTransportName) {
$failureTransportName = $this->getGlobalFailureReceiverName();
}
$receiver = $this->getReceiver($failureTransportName);
$shouldForce = $input->getOption('force');
$ids = (array) $input->getArgument('id');
$shouldDeleteAllMessages = $input->getOption('all');
$idsCount = \count($ids);
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) {
throw new RuntimeException('You cannot specify message ids when using the "--all" option.');
}
$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 {
$this->removeMessagesById($ids, $receiver, $io, $shouldForce, $shouldDisplayMessages);
}
return 0;
}
private function removeMessagesById(array $ids, ListableReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce, bool $shouldDisplayMessages): void
{
foreach ($ids as $id) {
$this->phpSerializer?->acceptPhpIncompleteClass();
try {
$envelope = $receiver->find($id);
} finally {
$this->phpSerializer?->rejectPhpIncompleteClass();
}
if (null === $envelope) {
$io->error(\sprintf('The message with id "%s" was not found.', $id));
continue;
}
if ($shouldDisplayMessages) {
$this->displaySingleMessage($envelope, $io);
}
if ($shouldForce || $io->confirm('Do you want to permanently remove this message?', false)) {
$receiver->reject($envelope);
$io->success(\sprintf('Message with id %s removed.', $id));
} else {
$io->note(\sprintf('Message with id %s not removed.', $id));
}
}
}
private function removeAllMessages(ListableReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce, bool $shouldDisplayMessages): void
{
if (!$shouldForce) {
if ($receiver instanceof MessageCountAwareInterface) {
$question = \sprintf('Do you want to permanently remove all (%d) messages?', $receiver->getMessageCount());
} else {
$question = 'Do you want to permanently remove all failed messages?';
}
if (!$io->confirm($question, false)) {
return;
}
}
$count = 0;
foreach ($receiver->all() as $envelope) {
if ($shouldDisplayMessages) {
$this->displaySingleMessage($envelope, $io);
}
$receiver->reject($envelope);
++$count;
}
$io->note(\sprintf('%d messages were removed.', $count));
}
}

View File

@@ -0,0 +1,281 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\MessageDecodingFailedStamp;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
use Symfony\Component\Messenger\Transport\Receiver\SingleMessageReceiver;
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
use Symfony\Component\Messenger\Worker;
use Symfony\Contracts\Service\ServiceProviderInterface;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
#[AsCommand(name: 'messenger:failed:retry', description: 'Retry one or more messages from the failure transport')]
class FailedMessagesRetryCommand extends AbstractFailedMessagesCommand implements SignalableCommandInterface
{
private EventDispatcherInterface $eventDispatcher;
private MessageBusInterface $messageBus;
private ?LoggerInterface $logger;
private ?array $signals;
private bool $shouldStop = false;
private bool $forceExit = false;
private ?Worker $worker = null;
public function __construct(?string $globalReceiverName, ServiceProviderInterface $failureTransports, MessageBusInterface $messageBus, EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null, ?PhpSerializer $phpSerializer = null, ?array $signals = null)
{
$this->eventDispatcher = $eventDispatcher;
$this->messageBus = $messageBus;
$this->logger = $logger;
$this->signals = $signals;
parent::__construct($globalReceiverName, $failureTransports, $phpSerializer);
}
protected function configure(): void
{
$this
->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_REQUIRED, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
])
->setHelp(<<<'EOF'
The <info>%command.name%</info> retries message in the failure transport.
<info>php %command.full_name%</info>
The command will interactively ask if each message should be retried
or discarded.
Some transports support retrying a specific message id, which comes
from the <info>messenger:failed:show</info> command.
<info>php %command.full_name% {id}</info>
Or pass multiple ids at once to process multiple messages:
<info>php %command.full_name% {id1} {id2} {id3}</info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->eventDispatcher->addSubscriber(new StopWorkerOnMessageLimitListener(1));
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
$io->comment('Quit this command with CONTROL-C.');
if (!$output->isVeryVerbose()) {
$io->comment('Re-run the command with a -vv option to see logs about consumed messages.');
}
$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 <comment>messenger:consume %s</comment>', $failureTransportName));
$shouldForce = $input->getOption('force');
$ids = $input->getArgument('id');
if (0 === \count($ids)) {
if (!$input->isInteractive()) {
throw new RuntimeException('Message id must be passed when in non-interactive mode.');
}
$this->runInteractive($failureTransportName, $io, $shouldForce);
return 0;
}
$this->retrySpecificIds($failureTransportName, $ids, $io, $shouldForce);
if (!$this->shouldStop) {
$io->success('All done!');
}
return 0;
}
public function getSubscribedSignals(): array
{
return $this->signals ?? (\extension_loaded('pcntl') ? [\SIGTERM, \SIGINT] : []);
}
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
{
if (!$this->worker) {
return false;
}
$this->logger?->info('Received signal {signal}.', ['signal' => $signal, 'transport_names' => $this->worker->getMetadata()->getTransportNames()]);
$this->worker->stop();
$this->shouldStop = true;
return $this->forceExit ? 0 : false;
}
private function runInteractive(string $failureTransportName, SymfonyStyle $io, bool $shouldForce): void
{
$receiver = $this->failureTransports->get($failureTransportName);
$count = 0;
if ($receiver instanceof ListableReceiverInterface) {
// for listable receivers, find the messages one-by-one
// this avoids using get(), which for some less-robust
// transports (like Doctrine), will cause the message
// to be temporarily "acked", even if the user aborts
// handling the message
while (!$this->shouldStop) {
$envelopes = [];
$this->phpSerializer?->acceptPhpIncompleteClass();
try {
foreach ($receiver->all(1) as $envelope) {
++$count;
$envelopes[] = $envelope;
}
} finally {
$this->phpSerializer?->rejectPhpIncompleteClass();
}
// break the loop if all messages are consumed
if (0 === \count($envelopes)) {
break;
}
$this->retrySpecificEnvelopes($envelopes, $failureTransportName, $io, $shouldForce);
}
} else {
// get() and ask messages one-by-one
$count = $this->runWorker($failureTransportName, $receiver, $io, $shouldForce);
}
// avoid success message if nothing was processed
if (1 <= $count && !$this->shouldStop) {
$io->success('All failed messages have been handled or removed!');
}
}
private function runWorker(string $failureTransportName, ReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce): int
{
$count = 0;
$listener = function (WorkerMessageReceivedEvent $messageReceivedEvent) use ($io, $receiver, $shouldForce, &$count) {
++$count;
$envelope = $messageReceivedEvent->getEnvelope();
$this->displaySingleMessage($envelope, $io);
if ($envelope->last(MessageDecodingFailedStamp::class)) {
throw new \RuntimeException(\sprintf('The message with id "%s" could not decoded, it can only be shown or removed.', $this->getMessageId($envelope) ?? '?'));
}
$this->forceExit = true;
try {
$shouldHandle = $shouldForce || 'retry' === $io->choice('Please select an action', ['retry', 'delete'], 'retry');
} finally {
$this->forceExit = false;
}
if ($shouldHandle) {
return;
}
$messageReceivedEvent->shouldHandle(false);
$receiver->reject($envelope);
};
$this->eventDispatcher->addListener(WorkerMessageReceivedEvent::class, $listener);
$this->worker = new Worker(
[$failureTransportName => $receiver],
$this->messageBus,
$this->eventDispatcher,
$this->logger
);
try {
$this->worker->run();
} finally {
$this->worker = null;
$this->eventDispatcher->removeListener(WorkerMessageReceivedEvent::class, $listener);
}
return $count;
}
private function retrySpecificIds(string $failureTransportName, array $ids, SymfonyStyle $io, bool $shouldForce): void
{
$receiver = $this->getReceiver($failureTransportName);
if (!$receiver instanceof ListableReceiverInterface) {
throw new RuntimeException(\sprintf('The "%s" receiver does not support retrying messages by id.', $failureTransportName));
}
foreach ($ids as $id) {
$this->phpSerializer?->acceptPhpIncompleteClass();
try {
$envelope = $receiver->find($id);
} finally {
$this->phpSerializer?->rejectPhpIncompleteClass();
}
if (null === $envelope) {
throw new RuntimeException(\sprintf('The message "%s" was not found.', $id));
}
$singleReceiver = new SingleMessageReceiver($receiver, $envelope);
$this->runWorker($failureTransportName, $singleReceiver, $io, $shouldForce);
if ($this->shouldStop) {
break;
}
}
}
private function retrySpecificEnvelopes(array $envelopes, string $failureTransportName, SymfonyStyle $io, bool $shouldForce): void
{
$receiver = $this->getReceiver($failureTransportName);
foreach ($envelopes as $envelope) {
$singleReceiver = new SingleMessageReceiver($receiver, $envelope);
$this->runWorker($failureTransportName, $singleReceiver, $io, $shouldForce);
if ($this->shouldStop) {
break;
}
}
}
}

View File

@@ -0,0 +1,197 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
#[AsCommand(name: 'messenger:failed:show', description: 'Show one or more messages from the failure transport')]
class FailedMessagesShowCommand extends AbstractFailedMessagesCommand
{
protected function configure(): void
{
$this
->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_REQUIRED, '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 <info>%command.name%</info> shows message that are pending in the failure transport.
<info>php %command.full_name%</info>
Or look at a specific message by its id:
<info>php %command.full_name% {id}</info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
$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.', $failureTransportName));
}
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);
}
return 0;
}
private function listMessages(?string $failedTransportName, SymfonyStyle $io, int $max, ?string $classFilter = null): void
{
/** @var ListableReceiverInterface $receiver */
$receiver = $this->getReceiver($failedTransportName);
$envelopes = $receiver->all($max);
$rows = [];
if ($classFilter) {
$io->comment(\sprintf('Displaying only \'%s\' messages', $classFilter));
}
$this->phpSerializer?->acceptPhpIncompleteClass();
try {
foreach ($envelopes as $envelope) {
$currentClassName = $envelope->getMessage()::class;
if ($classFilter && $classFilter !== $currentClassName) {
continue;
}
/** @var RedeliveryStamp|null $lastRedeliveryStamp */
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
$lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class);
$rows[] = [
$this->getMessageId($envelope),
$currentClassName,
null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s'),
$lastErrorDetailsStamp?->getExceptionMessage() ?? '',
];
}
} finally {
$this->phpSerializer?->rejectPhpIncompleteClass();
}
$rowsCount = \count($rows);
if (0 === $rowsCount) {
$io->success('No failed messages were found.');
return;
}
$io->table(['Id', 'Class', 'Failed at', 'Error'], $rows);
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 <comment>messenger:failed:show {id} --transport=%s -vv</comment> to see message details.', $failedTransportName));
}
private function listMessagesPerClass(?string $failedTransportName, SymfonyStyle $io, int $max): void
{
/** @var ListableReceiverInterface $receiver */
$receiver = $this->getReceiver($failedTransportName);
$envelopes = $receiver->all($max);
$countPerClass = [];
$this->phpSerializer?->acceptPhpIncompleteClass();
try {
foreach ($envelopes as $envelope) {
$c = $envelope->getMessage()::class;
if (!isset($countPerClass[$c])) {
$countPerClass[$c] = [$c, 0];
}
++$countPerClass[$c][1];
}
} finally {
$this->phpSerializer?->rejectPhpIncompleteClass();
}
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): void
{
/** @var ListableReceiverInterface $receiver */
$receiver = $this->getReceiver($failedTransportName);
$this->phpSerializer?->acceptPhpIncompleteClass();
try {
$envelope = $receiver->find($id);
} finally {
$this->phpSerializer?->rejectPhpIncompleteClass();
}
if (null === $envelope) {
throw new RuntimeException(\sprintf('The message "%s" was not found.', $id));
}
$this->displaySingleMessage($envelope, $io);
$io->writeln([
'',
\sprintf(' Run <comment>messenger:failed:retry %s --transport=%s</comment> to retry this message.', $id, $failedTransportName),
\sprintf(' Run <comment>messenger:failed:remove %s --transport=%s</comment> to delete it.', $id, $failedTransportName),
]);
}
}

View File

@@ -0,0 +1,101 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\Transport\SetupableTransportInterface;
/**
* @author Vincent Touzet <vincent.touzet@gmail.com>
*/
#[AsCommand(name: 'messenger:setup-transports', description: 'Prepare the required infrastructure for the transport')]
class SetupTransportsCommand extends Command
{
private ContainerInterface $transportLocator;
private array $transportNames;
public function __construct(ContainerInterface $transportLocator, array $transportNames = [])
{
$this->transportLocator = $transportLocator;
$this->transportNames = $transportNames;
parent::__construct();
}
/**
* @return void
*/
protected function configure()
{
$this
->addArgument('transport', InputArgument::OPTIONAL, 'Name of the transport to setup', null)
->setHelp(<<<EOF
The <info>%command.name%</info> command setups the transports:
<info>php %command.full_name%</info>
Or a specific transport only:
<info>php %command.full_name% <transport></info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$transportNames = $this->transportNames;
// do we want to set up only one transport?
if ($transport = $input->getArgument('transport')) {
if (!$this->transportLocator->has($transport)) {
throw new \RuntimeException(\sprintf('The "%s" transport does not exist.', $transport));
}
$transportNames = [$transport];
}
foreach ($transportNames as $id => $transportName) {
$transport = $this->transportLocator->get($transportName);
if (!$transport instanceof SetupableTransportInterface) {
$io->note(\sprintf('The "%s" transport does not support setup.', $transportName));
continue;
}
try {
$transport->setup();
$io->success(\sprintf('The "%s" transport was set up successfully.', $transportName));
} catch (\Exception $e) {
throw new \RuntimeException(\sprintf('An error occurred while setting up the "%s" transport: ', $transportName).$e->getMessage(), 0, $e);
}
}
return 0;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('transport')) {
$suggestions->suggestValues($this->transportNames);
return;
}
}
}

View File

@@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;
/**
* @author Kévin Thérage <therage.kevin@gmail.com>
*/
#[AsCommand(name: 'messenger:stats', description: 'Show the message count for one or more transports')]
class StatsCommand extends Command
{
private ContainerInterface $transportLocator;
private array $transportNames;
public function __construct(ContainerInterface $transportLocator, array $transportNames = [])
{
$this->transportLocator = $transportLocator;
$this->transportNames = $transportNames;
parent::__construct();
}
/**
* @return void
*/
protected function configure()
{
$this
->addArgument('transport_names', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'List of transports\' names')
->setHelp(<<<EOF
The <info>%command.name%</info> command counts the messages for all the transports:
<info>php %command.full_name%</info>
Or specific transports only:
<info>php %command.full_name% <transportNames></info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
$transportNames = $this->transportNames;
if ($input->getArgument('transport_names')) {
$transportNames = $input->getArgument('transport_names');
}
$outputTable = [];
$uncountableTransports = [];
foreach ($transportNames as $transportName) {
if (!$this->transportLocator->has($transportName)) {
$io->warning(\sprintf('The "%s" transport does not exist.', $transportName));
continue;
}
$transport = $this->transportLocator->get($transportName);
if (!$transport instanceof MessageCountAwareInterface) {
$uncountableTransports[] = $transportName;
continue;
}
$outputTable[] = [$transportName, $transport->getMessageCount()];
}
$io->table(['Transport', 'Count'], $outputTable);
if ($uncountableTransports) {
$io->note(\sprintf('Unable to get message count for the following transports: "%s".', implode('", "', $uncountableTransports)));
}
return 0;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\EventListener\StopWorkerOnRestartSignalListener;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
#[AsCommand(name: 'messenger:stop-workers', description: 'Stop workers after their current message')]
class StopWorkersCommand extends Command
{
private CacheItemPoolInterface $restartSignalCachePool;
public function __construct(CacheItemPoolInterface $restartSignalCachePool)
{
$this->restartSignalCachePool = $restartSignalCachePool;
parent::__construct();
}
protected function configure(): void
{
$this
->setDefinition([])
->setHelp(<<<'EOF'
The <info>%command.name%</info> command sends a signal to stop any <info>messenger:consume</info> processes that are running.
<info>php %command.full_name%</info>
Each worker command will finish the message they are currently processing
and then exit. Worker commands are *not* automatically restarted: that
should be handled by a process control system.
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
$cacheItem = $this->restartSignalCachePool->getItem(StopWorkerOnRestartSignalListener::RESTART_REQUESTED_TIMESTAMP_KEY);
$cacheItem->set(microtime(true));
$this->restartSignalCachePool->save($cacheItem);
$io->success('Signal successfully sent to stop any running workers.');
return 0;
}
}

View File

@@ -0,0 +1,132 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\Messenger\TraceableMessageBus;
use Symfony\Component\VarDumper\Caster\ClassStub;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*
* @final
*/
class MessengerDataCollector extends DataCollector implements LateDataCollectorInterface
{
private array $traceableBuses = [];
public function registerBus(string $name, TraceableMessageBus $bus): void
{
$this->traceableBuses[$name] = $bus;
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
// Noop. Everything is collected live by the traceable buses & cloned as late as possible.
}
public function lateCollect(): void
{
$this->data = ['messages' => [], 'buses' => array_keys($this->traceableBuses)];
$messages = [];
foreach ($this->traceableBuses as $busName => $bus) {
foreach ($bus->getDispatchedMessages() as $message) {
$debugRepresentation = $this->cloneVar($this->collectMessage($busName, $message));
$messages[] = [$debugRepresentation, $message['callTime']];
}
}
// Order by call time
usort($messages, fn ($a, $b) => $a[1] <=> $b[1]);
// Keep the messages clones only
$this->data['messages'] = array_column($messages, 0);
}
public function getName(): string
{
return 'messenger';
}
public function reset(): void
{
$this->data = [];
foreach ($this->traceableBuses as $traceableBus) {
$traceableBus->reset();
}
}
protected function getCasters(): array
{
$casters = parent::getCasters();
// Unset the default caster truncating collectors data.
unset($casters['*']);
return $casters;
}
private function collectMessage(string $busName, array $tracedMessage): array
{
$message = $tracedMessage['message'];
$debugRepresentation = [
'bus' => $busName,
'stamps' => $tracedMessage['stamps'] ?? null,
'stamps_after_dispatch' => $tracedMessage['stamps_after_dispatch'] ?? null,
'message' => [
'type' => new ClassStub($message::class),
'value' => $message,
],
'caller' => $tracedMessage['caller'],
];
if (isset($tracedMessage['exception'])) {
$exception = $tracedMessage['exception'];
$debugRepresentation['exception'] = [
'type' => $exception::class,
'value' => $exception,
];
}
return $debugRepresentation;
}
public function getExceptionsCount(?string $bus = null): int
{
$count = 0;
foreach ($this->getMessages($bus) as $message) {
$count += (int) isset($message['exception']);
}
return $count;
}
public function getMessages(?string $bus = null): array
{
if (null === $bus) {
return $this->data['messages'];
}
return array_filter($this->data['messages'], fn ($message) => $bus === $message['bus']);
}
public function getBuses(): array
{
return $this->data['buses'];
}
}

View File

@@ -0,0 +1,411 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\DependencyInjection;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Handler\HandlerDescriptor;
use Symfony\Component\Messenger\Handler\HandlersLocator;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Component\Messenger\Handler\MessageSubscriberInterface;
use Symfony\Component\Messenger\TraceableMessageBus;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class MessengerPass implements CompilerPassInterface
{
/**
* @return void
*/
public function process(ContainerBuilder $container)
{
$busIds = [];
foreach ($container->findTaggedServiceIds('messenger.bus') as $busId => $tags) {
$busIds[] = $busId;
if ($container->hasParameter($busMiddlewareParameter = $busId.'.middleware')) {
$this->registerBusMiddleware($container, $busId, $container->getParameter($busMiddlewareParameter));
$container->getParameterBag()->remove($busMiddlewareParameter);
}
if ($container->hasDefinition('data_collector.messenger')) {
$this->registerBusToCollector($container, $busId);
}
}
if ($container->hasDefinition('messenger.receiver_locator')) {
$this->registerReceivers($container, $busIds);
}
$this->registerHandlers($container, $busIds);
}
private function registerHandlers(ContainerBuilder $container, array $busIds): void
{
$definitions = [];
$handlersByBusAndMessage = [];
$handlerToOriginalServiceIdMapping = [];
foreach ($container->findTaggedServiceIds('messenger.message_handler', true) as $serviceId => $tags) {
foreach ($tags as $tag) {
if (isset($tag['bus']) && !\in_array($tag['bus'], $busIds, true)) {
throw new RuntimeException(\sprintf('Invalid handler service "%s": bus "%s" specified on the tag "messenger.message_handler" does not exist (known ones are: "%s").', $serviceId, $tag['bus'], implode('", "', $busIds)));
}
$className = $this->getServiceClass($container, $serviceId);
$r = $container->getReflectionClass($className);
if (null === $r) {
throw new RuntimeException(\sprintf('Invalid service "%s": class "%s" does not exist.', $serviceId, $className));
}
if (isset($tag['handles'])) {
$handles = isset($tag['method']) ? [$tag['handles'] => $tag['method']] : [$tag['handles']];
} else {
$handles = $this->guessHandledClasses($r, $serviceId, $tag['method'] ?? '__invoke');
}
$message = null;
$handlerBuses = (array) ($tag['bus'] ?? $busIds);
foreach ($handles as $message => $options) {
$buses = $handlerBuses;
if (\is_int($message)) {
if (\is_string($options)) {
$message = $options;
$options = [];
} else {
throw new RuntimeException(\sprintf('The handler configuration needs to return an array of messages or an associated array of message and configuration. Found value of type "%s" at position "%d" for service "%s".', get_debug_type($options), $message, $serviceId));
}
}
if (\is_string($options)) {
$options = ['method' => $options];
}
$options += array_filter($tag);
unset($options['handles']);
$priority = $options['priority'] ?? 0;
$method = $options['method'] ?? '__invoke';
$fromTransport = $options['from_transport'] ?? '';
if (isset($options['bus'])) {
if (!\in_array($options['bus'], $busIds)) {
// @deprecated since Symfony 6.2, in 7.0 change to:
// $messageLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method);
$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 '.$messageLocation.' for message "%s": bus "%s" does not exist.', $message, $options['bus']));
}
$buses = [$options['bus']];
}
if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) {
// @deprecated since Symfony 6.2, in 7.0 change to:
// $messageLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method);
$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" '.$messageLocation.' not found.', $serviceId, $message));
}
if (!$r->hasMethod($method)) {
throw new RuntimeException(\sprintf('Invalid handler service "%s": method "%s::%s()" does not exist.', $serviceId, $r->getName(), $method));
}
if ('__invoke' !== $method || '' !== $fromTransport) {
$wrapperDefinition = (new Definition('Closure'))->addArgument([new Reference($serviceId), $method])->setFactory('Closure::fromCallable');
$definitions[$definitionId = '.messenger.method_on_object_wrapper.'.ContainerBuilder::hash($message.':'.$priority.':'.$serviceId.':'.$method.':'.$fromTransport)] = $wrapperDefinition;
} else {
$definitionId = $serviceId;
}
$handlerToOriginalServiceIdMapping[$definitionId] = $serviceId;
foreach ($buses as $handlerBus) {
$handlersByBusAndMessage[$handlerBus][$message][$priority][] = [$definitionId, $options];
}
}
if (null === $message) {
throw new RuntimeException(\sprintf('Invalid handler service "%s": method "%s::getHandledMessages()" must return one or more messages.', $serviceId, $r->getName()));
}
}
}
foreach ($handlersByBusAndMessage as $bus => $handlersByMessage) {
foreach ($handlersByMessage as $message => $handlersByPriority) {
krsort($handlersByPriority);
$handlersByBusAndMessage[$bus][$message] = array_merge(...$handlersByPriority);
}
}
$handlersLocatorMappingByBus = [];
foreach ($handlersByBusAndMessage as $bus => $handlersByMessage) {
foreach ($handlersByMessage as $message => $handlers) {
$handlerDescriptors = [];
foreach ($handlers as $handler) {
$definitions[$definitionId = '.messenger.handler_descriptor.'.ContainerBuilder::hash($bus.':'.$message.':'.$handler[0])] = (new Definition(HandlerDescriptor::class))->setArguments([new Reference($handler[0]), $handler[1]]);
$handlerDescriptors[] = new Reference($definitionId);
}
$handlersLocatorMappingByBus[$bus][$message] = new IteratorArgument($handlerDescriptors);
}
}
$container->addDefinitions($definitions);
foreach ($busIds as $bus) {
$container->register($locatorId = $bus.'.messenger.handlers_locator', HandlersLocator::class)
->setArgument(0, $handlersLocatorMappingByBus[$bus] ?? [])
;
if ($container->has($handleMessageId = $bus.'.middleware.handle_message')) {
$container->getDefinition($handleMessageId)
->replaceArgument(0, new Reference($locatorId))
;
}
}
if ($container->hasDefinition('console.command.messenger_debug')) {
$debugCommandMapping = $handlersByBusAndMessage;
foreach ($busIds as $bus) {
if (!isset($debugCommandMapping[$bus])) {
$debugCommandMapping[$bus] = [];
}
foreach ($debugCommandMapping[$bus] as $message => $handlers) {
foreach ($handlers as $key => $handler) {
$debugCommandMapping[$bus][$message][$key][0] = $handlerToOriginalServiceIdMapping[$handler[0]];
}
}
}
$container->getDefinition('console.command.messenger_debug')->replaceArgument(0, $debugCommandMapping);
}
}
private function guessHandledClasses(\ReflectionClass $handlerClass, string $serviceId, string $methodName): iterable
{
if ($handlerClass->implementsInterface(MessageSubscriberInterface::class)) {
trigger_deprecation('symfony/messenger', '6.2', 'Implementing "%s" is deprecated, use the "%s" attribute instead.', MessageSubscriberInterface::class, AsMessageHandler::class);
return $handlerClass->getName()::getHandledMessages();
}
if ($handlerClass->implementsInterface(MessageHandlerInterface::class)) {
trigger_deprecation('symfony/messenger', '6.2', 'Implementing "%s" is deprecated, use the "%s" attribute instead.', MessageHandlerInterface::class, AsMessageHandler::class);
}
try {
$method = $handlerClass->getMethod($methodName);
} catch (\ReflectionException) {
throw new RuntimeException(\sprintf('Invalid handler service "%s": class "%s" must have an "%s()" method.', $serviceId, $handlerClass->getName(), $methodName));
}
if (0 === $method->getNumberOfRequiredParameters()) {
throw new RuntimeException(\sprintf('Invalid handler service "%s": method "%s::%s()" requires at least one argument, first one being the message it handles.', $serviceId, $handlerClass->getName(), $methodName));
}
$parameters = $method->getParameters();
/** @var \ReflectionNamedType|\ReflectionUnionType|null */
$type = $parameters[0]->getType();
if (!$type) {
throw new RuntimeException(\sprintf('Invalid handler service "%s": argument "$%s" of method "%s::%s()" must have a type-hint corresponding to the message class it handles.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), $methodName));
}
if ($type instanceof \ReflectionUnionType) {
$types = [];
$invalidTypes = [];
foreach ($type->getTypes() as $type) {
if (!$type->isBuiltin()) {
$types[] = (string) $type;
} else {
$invalidTypes[] = (string) $type;
}
}
if ($types) {
return ('__invoke' === $methodName) ? $types : array_fill_keys($types, $methodName);
}
throw new RuntimeException(\sprintf('Invalid handler service "%s": type-hint of argument "$%s" in method "%s::__invoke()" must be a class , "%s" given.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), implode('|', $invalidTypes)));
}
if ($type->isBuiltin()) {
throw new RuntimeException(\sprintf('Invalid handler service "%s": type-hint of argument "$%s" in method "%s::%s()" must be a class , "%s" given.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), $methodName, $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type));
}
return ('__invoke' === $methodName) ? [$type->getName()] : [$type->getName() => $methodName];
}
private function registerReceivers(ContainerBuilder $container, array $busIds): void
{
$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) {
if ($container->hasAlias('messenger.failure_transports.default')) {
$failureTransportsMap[$globalReceiverName] = new Reference('messenger.failure_transports.default');
} else {
$failureTransportsMap[$globalReceiverName] = new Reference('messenger.transport.'.$globalReceiverName);
}
}
}
$consumableReceiverNames = [];
foreach ($container->findTaggedServiceIds('messenger.receiver') as $id => $tags) {
$receiverClass = $this->getServiceClass($container, $id);
if (!is_subclass_of($receiverClass, ReceiverInterface::class)) {
throw new RuntimeException(\sprintf('Invalid receiver "%s": class "%s" must implement interface "%s".', $id, $receiverClass, ReceiverInterface::class));
}
$receiverMapping[$id] = new Reference($id);
foreach ($tags as $tag) {
if (isset($tag['alias'])) {
$receiverMapping[$tag['alias']] = $receiverMapping[$id];
if ($tag['is_failure_transport'] ?? false) {
$failureTransportsMap[$tag['alias']] = $receiverMapping[$id];
}
}
if (!isset($tag['is_consumable']) || false !== $tag['is_consumable']) {
$consumableReceiverNames[] = $tag['alias'] ?? $id;
}
}
}
$receiverNames = [];
foreach ($receiverMapping as $name => $reference) {
$receiverNames[(string) $reference] = $name;
}
$buses = [];
foreach ($busIds as $busId) {
$buses[$busId] = new Reference($busId);
}
if ($hasRoutableMessageBus = $container->hasDefinition('messenger.routable_message_bus')) {
$container->getDefinition('messenger.routable_message_bus')
->replaceArgument(0, ServiceLocatorTagPass::register($container, $buses));
}
if ($container->hasDefinition('console.command.messenger_consume_messages')) {
$consumeCommandDefinition = $container->getDefinition('console.command.messenger_consume_messages');
if ($hasRoutableMessageBus) {
$consumeCommandDefinition->replaceArgument(0, new Reference('messenger.routable_message_bus'));
}
$consumeCommandDefinition->replaceArgument(4, $consumableReceiverNames);
try {
$consumeCommandDefinition->replaceArgument(6, $busIds);
} catch (OutOfBoundsException) {
// ignore to preserve compatibility with symfony/framework-bundle < 5.4
}
}
if ($container->hasDefinition('console.command.messenger_setup_transports')) {
$container->getDefinition('console.command.messenger_setup_transports')
->replaceArgument(1, array_values($receiverNames));
}
if ($container->hasDefinition('console.command.messenger_stats')) {
$container->getDefinition('console.command.messenger_stats')
->replaceArgument(1, array_values($receiverNames));
}
$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',
'console.command.messenger_failed_messages_remove',
];
foreach ($failedCommandIds as $failedCommandId) {
if ($container->hasDefinition($failedCommandId)) {
$definition = $container->getDefinition($failedCommandId);
$definition->replaceArgument(1, $failureTransportsLocator);
}
}
}
private function registerBusToCollector(ContainerBuilder $container, string $busId): void
{
$container->setDefinition(
$tracedBusId = 'debug.traced.'.$busId,
(new Definition(TraceableMessageBus::class, [new Reference($tracedBusId.'.inner')]))->setDecoratedService($busId)
);
$container->getDefinition('data_collector.messenger')->addMethodCall('registerBus', [$busId, new Reference($tracedBusId)]);
}
private function registerBusMiddleware(ContainerBuilder $container, string $busId, array $middlewareCollection): void
{
$middlewareReferences = [];
foreach ($middlewareCollection as $middlewareItem) {
$id = $middlewareItem['id'];
$arguments = $middlewareItem['arguments'] ?? [];
if (!$container->has($messengerMiddlewareId = 'messenger.middleware.'.$id)) {
$messengerMiddlewareId = $id;
}
if (!$container->has($messengerMiddlewareId)) {
throw new RuntimeException(\sprintf('Invalid middleware: service "%s" not found.', $id));
}
if ($container->findDefinition($messengerMiddlewareId)->isAbstract()) {
$childDefinition = new ChildDefinition($messengerMiddlewareId);
$childDefinition->setArguments($arguments);
if (isset($middlewareReferences[$messengerMiddlewareId = $busId.'.middleware.'.$id])) {
$messengerMiddlewareId .= '.'.ContainerBuilder::hash($arguments);
}
$container->setDefinition($messengerMiddlewareId, $childDefinition);
} elseif ($arguments) {
throw new RuntimeException(\sprintf('Invalid middleware factory "%s": a middleware factory must be an abstract definition.', $id));
}
$middlewareReferences[$messengerMiddlewareId] = new Reference($messengerMiddlewareId);
}
$container->getDefinition($busId)->replaceArgument(0, new IteratorArgument(array_values($middlewareReferences)));
}
private function getServiceClass(ContainerBuilder $container, string $serviceId): string
{
while (true) {
$definition = $container->findDefinition($serviceId);
if (!$definition->getClass() && $definition instanceof ChildDefinition) {
$serviceId = $definition->getParent();
continue;
}
return $definition->getClass();
}
}
}

130
vendor/symfony/messenger/Envelope.php vendored Normal file
View File

@@ -0,0 +1,130 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger;
use Symfony\Component\Messenger\Stamp\StampInterface;
/**
* A message wrapped in an envelope with stamps (configurations, markers, ...).
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
final class Envelope
{
/**
* @var array<class-string<StampInterface>, list<StampInterface>>
*/
private array $stamps = [];
private object $message;
/**
* @param object|Envelope $message
* @param StampInterface[] $stamps
*/
public function __construct(object $message, array $stamps = [])
{
$this->message = $message;
foreach ($stamps as $stamp) {
$this->stamps[$stamp::class][] = $stamp;
}
}
/**
* Makes sure the message is in an Envelope and adds the given stamps.
*
* @param StampInterface[] $stamps
*/
public static function wrap(object $message, array $stamps = []): self
{
$envelope = $message instanceof self ? $message : new self($message);
return $envelope->with(...$stamps);
}
/**
* Adds one or more stamps.
*/
public function with(StampInterface ...$stamps): static
{
$cloned = clone $this;
foreach ($stamps as $stamp) {
$cloned->stamps[$stamp::class][] = $stamp;
}
return $cloned;
}
/**
* Removes all stamps of the given class.
*/
public function withoutAll(string $stampFqcn): static
{
$cloned = clone $this;
unset($cloned->stamps[$stampFqcn]);
return $cloned;
}
/**
* Removes all stamps that implement the given type.
*/
public function withoutStampsOfType(string $type): self
{
$cloned = clone $this;
foreach ($cloned->stamps as $class => $stamps) {
if ($class === $type || is_subclass_of($class, $type)) {
unset($cloned->stamps[$class]);
}
}
return $cloned;
}
/**
* @template TStamp of StampInterface
*
* @param class-string<TStamp> $stampFqcn
*
* @return TStamp|null
*/
public function last(string $stampFqcn): ?StampInterface
{
return isset($this->stamps[$stampFqcn]) ? end($this->stamps[$stampFqcn]) : null;
}
/**
* @template TStamp of StampInterface
*
* @param class-string<TStamp>|null $stampFqcn
*
* @return StampInterface[]|StampInterface[][] The stamps for the specified FQCN, or all stamps by their class name
*
* @psalm-return ($stampFqcn is null ? array<class-string<StampInterface>, list<StampInterface>> : list<TStamp>)
*/
public function all(?string $stampFqcn = null): array
{
if (null !== $stampFqcn) {
return $this->stamps[$stampFqcn] ?? [];
}
return $this->stamps;
}
public function getMessage(): object
{
return $this->message;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\StampInterface;
abstract class AbstractWorkerMessageEvent
{
private Envelope $envelope;
private string $receiverName;
public function __construct(Envelope $envelope, string $receiverName)
{
$this->envelope = $envelope;
$this->receiverName = $receiverName;
}
public function getEnvelope(): Envelope
{
return $this->envelope;
}
/**
* Returns a unique identifier for transport receiver this message was received from.
*/
public function getReceiverName(): string
{
return $this->receiverName;
}
public function addStamps(StampInterface ...$stamps): void
{
$this->envelope = $this->envelope->with(...$stamps);
}
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Transport\Sender\SenderInterface;
/**
* Event is dispatched before a message is sent to the transport.
*
* The event is *only* dispatched if the message will actually
* be sent to at least one transport. If the message is sent
* to multiple transports, the message is dispatched only once.
* This message is only dispatched the first time a message
* is sent to a transport, not also if it is retried.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
final class SendMessageToTransportsEvent
{
private Envelope $envelope;
private array $senders;
public function __construct(Envelope $envelope, array $senders)
{
$this->envelope = $envelope;
$this->senders = $senders;
}
public function getEnvelope(): Envelope
{
return $this->envelope;
}
public function setEnvelope(Envelope $envelope): void
{
$this->envelope = $envelope;
}
/**
* @return array<string, SenderInterface>
*/
public function getSenders(): array
{
return $this->senders;
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
use Symfony\Component\Messenger\Envelope;
/**
* Dispatched when a message was received from a transport and handling failed.
*
* The event name is the class name.
*/
final class WorkerMessageFailedEvent extends AbstractWorkerMessageEvent
{
private \Throwable $throwable;
private bool $willRetry = false;
public function __construct(Envelope $envelope, string $receiverName, \Throwable $error)
{
$this->throwable = $error;
parent::__construct($envelope, $receiverName);
}
public function getThrowable(): \Throwable
{
return $this->throwable;
}
public function willRetry(): bool
{
return $this->willRetry;
}
public function setForRetry(): void
{
$this->willRetry = true;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
/**
* Dispatched after a message was received from a transport and successfully handled.
*
* The event name is the class name.
*/
final class WorkerMessageHandledEvent extends AbstractWorkerMessageEvent
{
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
/**
* Dispatched when a message was received from a transport but before sent to the bus.
*
* The event name is the class name.
*/
final class WorkerMessageReceivedEvent extends AbstractWorkerMessageEvent
{
private bool $shouldHandle = true;
public function shouldHandle(?bool $shouldHandle = null): bool
{
if (null !== $shouldHandle) {
$this->shouldHandle = $shouldHandle;
}
return $this->shouldHandle;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
/**
* Dispatched after a message has been sent for retry.
*
* The event name is the class name.
*/
final class WorkerMessageRetriedEvent extends AbstractWorkerMessageEvent
{
}

View File

@@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
use Symfony\Component\RateLimiter\LimiterInterface;
/**
* Dispatched after the worker has been blocked due to a configured rate limiter.
* Can be used to reset the rate limiter.
*
* @author Bob van de Vijver
*/
final class WorkerRateLimitedEvent
{
public function __construct(private LimiterInterface $limiter, private string $transportName)
{
}
public function getLimiter(): LimiterInterface
{
return $this->limiter;
}
public function getTransportName(): string
{
return $this->transportName;
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
use Symfony\Component\Messenger\Worker;
/**
* Dispatched after the worker processed a message or didn't receive a message at all.
*
* @author Tobias Schultze <http://tobion.de>
*/
final class WorkerRunningEvent
{
private Worker $worker;
private bool $isWorkerIdle;
public function __construct(Worker $worker, bool $isWorkerIdle)
{
$this->worker = $worker;
$this->isWorkerIdle = $isWorkerIdle;
}
public function getWorker(): Worker
{
return $this->worker;
}
/**
* Returns true when no message has been received by the worker.
*/
public function isWorkerIdle(): bool
{
return $this->isWorkerIdle;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
use Symfony\Component\Messenger\Worker;
/**
* Dispatched when a worker has been started.
*
* @author Tobias Schultze <http://tobion.de>
*/
final class WorkerStartedEvent
{
private Worker $worker;
public function __construct(Worker $worker)
{
$this->worker = $worker;
}
public function getWorker(): Worker
{
return $this->worker;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
use Symfony\Component\Messenger\Worker;
/**
* Dispatched when a worker has been stopped.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
final class WorkerStoppedEvent
{
private Worker $worker;
public function __construct(Worker $worker)
{
$this->worker = $worker;
}
public function getWorker(): Worker
{
return $this->worker;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
final class AddErrorDetailsStampListener implements EventSubscriberInterface
{
public function onMessageFailed(WorkerMessageFailedEvent $event): void
{
$stamp = ErrorDetailsStamp::create($event->getThrowable());
$previousStamp = $event->getEnvelope()->last(ErrorDetailsStamp::class);
// Do not append duplicate information
if (null === $previousStamp || !$previousStamp->equals($stamp)) {
$event->addStamps($stamp);
}
}
public static function getSubscribedEvents(): array
{
return [
// must have higher priority than SendFailedMessageForRetryListener
WorkerMessageFailedEvent::class => ['onMessageFailed', 200],
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
/**
* @author Tobias Schultze <http://tobion.de>
*/
class DispatchPcntlSignalListener implements EventSubscriberInterface
{
public function onWorkerRunning(): void
{
if (!\function_exists('pcntl_signal_dispatch')) {
return;
}
pcntl_signal_dispatch();
}
public static function getSubscribedEvents(): array
{
if (!\function_exists('pcntl_signal_dispatch')) {
return [];
}
return [
WorkerRunningEvent::class => ['onWorkerRunning', 100],
];
}
}

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Event\WorkerStoppedEvent;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class ResetServicesListener implements EventSubscriberInterface
{
private ServicesResetter $servicesResetter;
public function __construct(ServicesResetter $servicesResetter)
{
$this->servicesResetter = $servicesResetter;
}
public function resetServices(WorkerRunningEvent $event): void
{
if (!$event->isWorkerIdle()) {
$this->servicesResetter->reset();
}
}
public function resetServicesAtStop(WorkerStoppedEvent $event): void
{
$this->servicesResetter->reset();
}
public static function getSubscribedEvents(): array
{
return [
WorkerRunningEvent::class => ['resetServices', -1024],
WorkerStoppedEvent::class => ['resetServicesAtStop', -1024],
];
}
}

View File

@@ -0,0 +1,170 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerMessageRetriedEvent;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\RecoverableExceptionInterface;
use Symfony\Component\Messenger\Exception\RuntimeException;
use Symfony\Component\Messenger\Exception\UnrecoverableExceptionInterface;
use Symfony\Component\Messenger\Retry\RetryStrategyInterface;
use Symfony\Component\Messenger\Stamp\DelayStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\StampInterface;
use Symfony\Component\Messenger\Transport\Sender\SenderInterface;
/**
* @author Tobias Schultze <http://tobion.de>
*/
class SendFailedMessageForRetryListener implements EventSubscriberInterface
{
private ContainerInterface $sendersLocator;
private ContainerInterface $retryStrategyLocator;
private ?LoggerInterface $logger;
private ?EventDispatcherInterface $eventDispatcher;
private int $historySize;
public function __construct(ContainerInterface $sendersLocator, ContainerInterface $retryStrategyLocator, ?LoggerInterface $logger = null, ?EventDispatcherInterface $eventDispatcher = null, int $historySize = 10)
{
$this->sendersLocator = $sendersLocator;
$this->retryStrategyLocator = $retryStrategyLocator;
$this->logger = $logger;
$this->eventDispatcher = $eventDispatcher;
$this->historySize = $historySize;
}
/**
* @return void
*/
public function onMessageFailed(WorkerMessageFailedEvent $event)
{
$retryStrategy = $this->getRetryStrategyForTransport($event->getReceiverName());
$envelope = $event->getEnvelope();
$throwable = $event->getThrowable();
$message = $envelope->getMessage();
$context = [
'class' => $message::class,
];
$shouldRetry = $retryStrategy && $this->shouldRetry($throwable, $envelope, $retryStrategy);
$retryCount = RedeliveryStamp::getRetryCountFromEnvelope($envelope);
if ($shouldRetry) {
$event->setForRetry();
++$retryCount;
$delay = $retryStrategy->getWaitingTime($envelope, $throwable);
$this->logger?->warning('Error thrown while handling message {class}. Sending for retry #{retryCount} using {delay} ms delay. Error: "{error}"', $context + ['retryCount' => $retryCount, 'delay' => $delay, 'error' => $throwable->getMessage(), 'exception' => $throwable]);
// add the delay and retry stamp info
$retryEnvelope = $this->withLimitedHistory($envelope, new DelayStamp($delay), new RedeliveryStamp($retryCount));
// re-send the message for retry
$retryEnvelope = $this->getSenderForTransport($event->getReceiverName())->send($retryEnvelope);
$this->eventDispatcher?->dispatch(new WorkerMessageRetriedEvent($retryEnvelope, $event->getReceiverName()));
} else {
$this->logger?->critical('Error thrown while handling message {class}. Removing from transport after {retryCount} retries. Error: "{error}"', $context + ['retryCount' => $retryCount, 'error' => $throwable->getMessage(), 'exception' => $throwable]);
}
}
/**
* Adds stamps to the envelope by keeping only the First + Last N stamps.
*/
private function withLimitedHistory(Envelope $envelope, StampInterface ...$stamps): Envelope
{
foreach ($stamps as $stamp) {
$history = $envelope->all($stamp::class);
if (\count($history) < $this->historySize) {
$envelope = $envelope->with($stamp);
continue;
}
$history = array_merge(
[$history[0]],
\array_slice($history, -$this->historySize + 2),
[$stamp]
);
$envelope = $envelope->withoutAll($stamp::class)->with(...$history);
}
return $envelope;
}
public static function getSubscribedEvents(): array
{
return [
// must have higher priority than SendFailedMessageToFailureTransportListener
WorkerMessageFailedEvent::class => ['onMessageFailed', 100],
];
}
private function shouldRetry(\Throwable $e, Envelope $envelope, RetryStrategyInterface $retryStrategy): bool
{
if ($e instanceof RecoverableExceptionInterface) {
return true;
}
// if one or more nested Exceptions is an instance of RecoverableExceptionInterface we should retry
// if ALL nested Exceptions are an instance of UnrecoverableExceptionInterface we should not retry
if ($e instanceof HandlerFailedException) {
$shouldNotRetry = true;
foreach ($e->getWrappedExceptions() as $nestedException) {
if ($nestedException instanceof RecoverableExceptionInterface) {
return true;
}
if (!$nestedException instanceof UnrecoverableExceptionInterface) {
$shouldNotRetry = false;
break;
}
}
if ($shouldNotRetry) {
return false;
}
}
if ($e instanceof UnrecoverableExceptionInterface) {
return false;
}
return $retryStrategy->isRetryable($envelope, $e);
}
private function getRetryStrategyForTransport(string $alias): ?RetryStrategyInterface
{
if ($this->retryStrategyLocator->has($alias)) {
return $this->retryStrategyLocator->get($alias);
}
return null;
}
private function getSenderForTransport(string $alias): SenderInterface
{
if ($this->sendersLocator->has($alias)) {
return $this->sendersLocator->get($alias);
}
throw new RuntimeException(\sprintf('Could not find sender "%s" based on the same receiver to send the failed message to for retry.', $alias));
}
}

View File

@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Stamp\DelayStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
/**
* Sends a rejected message to a "failure transport".
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class SendFailedMessageToFailureTransportListener implements EventSubscriberInterface
{
private ContainerInterface $failureSenders;
private ?LoggerInterface $logger;
public function __construct(ContainerInterface $failureSenders, ?LoggerInterface $logger = null)
{
$this->failureSenders = $failureSenders;
$this->logger = $logger;
}
/**
* @return void
*/
public function onMessageFailed(WorkerMessageFailedEvent $event)
{
if ($event->willRetry()) {
return;
}
if (!$this->failureSenders->has($event->getReceiverName())) {
return;
}
$failureSender = $this->failureSenders->get($event->getReceiverName());
$envelope = $event->getEnvelope();
// avoid re-sending to the failed sender
if (null !== $envelope->last(SentToFailureTransportStamp::class)) {
return;
}
$envelope = $envelope->with(
new SentToFailureTransportStamp($event->getReceiverName()),
new DelayStamp(0),
new RedeliveryStamp(0)
);
$this->logger?->info('Rejected message {class} will be sent to the failure transport {transport}.', [
'class' => $envelope->getMessage()::class,
'transport' => $failureSender::class,
]);
$failureSender->send($envelope);
}
public static function getSubscribedEvents(): array
{
return [
WorkerMessageFailedEvent::class => ['onMessageFailed', -100],
];
}
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\StopWorkerExceptionInterface;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class StopWorkerOnCustomStopExceptionListener implements EventSubscriberInterface
{
private bool $stop = false;
public function onMessageFailed(WorkerMessageFailedEvent $event): void
{
$th = $event->getThrowable();
if ($th instanceof StopWorkerExceptionInterface) {
$this->stop = true;
}
if ($th instanceof HandlerFailedException) {
foreach ($th->getWrappedExceptions() as $e) {
if ($e instanceof StopWorkerExceptionInterface) {
$this->stop = true;
break;
}
}
}
}
public function onWorkerRunning(WorkerRunningEvent $event): void
{
if ($this->stop) {
$event->getWorker()->stop();
}
}
public static function getSubscribedEvents(): array
{
return [
WorkerMessageFailedEvent::class => 'onMessageFailed',
WorkerRunningEvent::class => 'onWorkerRunning',
];
}
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
/**
* @author Michel Hunziker <info@michelhunziker.com>
*/
class StopWorkerOnFailureLimitListener implements EventSubscriberInterface
{
private int $maximumNumberOfFailures;
private ?LoggerInterface $logger;
private int $failedMessages = 0;
public function __construct(int $maximumNumberOfFailures, ?LoggerInterface $logger = null)
{
$this->maximumNumberOfFailures = $maximumNumberOfFailures;
$this->logger = $logger;
if ($maximumNumberOfFailures <= 0) {
throw new InvalidArgumentException('Failure limit must be greater than zero.');
}
}
public function onMessageFailed(WorkerMessageFailedEvent $event): void
{
++$this->failedMessages;
}
public function onWorkerRunning(WorkerRunningEvent $event): void
{
if (!$event->isWorkerIdle() && $this->failedMessages >= $this->maximumNumberOfFailures) {
$this->failedMessages = 0;
$event->getWorker()->stop();
$this->logger?->info('Worker stopped due to limit of {count} failed message(s) is reached', ['count' => $this->maximumNumberOfFailures]);
}
}
public static function getSubscribedEvents(): array
{
return [
WorkerMessageFailedEvent::class => 'onMessageFailed',
WorkerRunningEvent::class => 'onWorkerRunning',
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
/**
* @author Simon Delicata <simon.delicata@free.fr>
* @author Tobias Schultze <http://tobion.de>
*/
class StopWorkerOnMemoryLimitListener implements EventSubscriberInterface
{
private int $memoryLimit;
private ?LoggerInterface $logger;
private \Closure $memoryResolver;
public function __construct(int $memoryLimit, ?LoggerInterface $logger = null, ?callable $memoryResolver = null)
{
$this->memoryLimit = $memoryLimit;
$this->logger = $logger;
$memoryResolver ??= static fn () => memory_get_usage(true);
$this->memoryResolver = $memoryResolver(...);
}
public function onWorkerRunning(WorkerRunningEvent $event): void
{
$memoryResolver = $this->memoryResolver;
$usedMemory = $memoryResolver();
if ($usedMemory > $this->memoryLimit) {
$event->getWorker()->stop();
$this->logger?->info('Worker stopped due to memory limit of {limit} bytes exceeded ({memory} bytes used)', ['limit' => $this->memoryLimit, 'memory' => $usedMemory]);
}
}
public static function getSubscribedEvents(): array
{
return [
WorkerRunningEvent::class => 'onWorkerRunning',
];
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
* @author Tobias Schultze <http://tobion.de>
*/
class StopWorkerOnMessageLimitListener implements EventSubscriberInterface
{
private int $maximumNumberOfMessages;
private ?LoggerInterface $logger;
private int $receivedMessages = 0;
public function __construct(int $maximumNumberOfMessages, ?LoggerInterface $logger = null)
{
$this->maximumNumberOfMessages = $maximumNumberOfMessages;
$this->logger = $logger;
if ($maximumNumberOfMessages <= 0) {
throw new InvalidArgumentException('Message limit must be greater than zero.');
}
}
public function onWorkerRunning(WorkerRunningEvent $event): void
{
if (!$event->isWorkerIdle() && ++$this->receivedMessages >= $this->maximumNumberOfMessages) {
$this->receivedMessages = 0;
$event->getWorker()->stop();
$this->logger?->info('Worker stopped due to maximum count of {count} messages processed', ['count' => $this->maximumNumberOfMessages]);
}
}
public static function getSubscribedEvents(): array
{
return [
WorkerRunningEvent::class => 'onWorkerRunning',
];
}
}

View File

@@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Event\WorkerStartedEvent;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class StopWorkerOnRestartSignalListener implements EventSubscriberInterface
{
public const RESTART_REQUESTED_TIMESTAMP_KEY = 'workers.restart_requested_timestamp';
private CacheItemPoolInterface $cachePool;
private ?LoggerInterface $logger;
private float $workerStartedAt = 0;
public function __construct(CacheItemPoolInterface $cachePool, ?LoggerInterface $logger = null)
{
$this->cachePool = $cachePool;
$this->logger = $logger;
}
public function onWorkerStarted(): void
{
$this->workerStartedAt = microtime(true);
}
public function onWorkerRunning(WorkerRunningEvent $event): void
{
if ($this->shouldRestart()) {
$event->getWorker()->stop();
$this->logger?->info('Worker stopped because a restart was requested.');
}
}
public static function getSubscribedEvents(): array
{
return [
WorkerStartedEvent::class => 'onWorkerStarted',
WorkerRunningEvent::class => 'onWorkerRunning',
];
}
private function shouldRestart(): bool
{
$cacheItem = $this->cachePool->getItem(self::RESTART_REQUESTED_TIMESTAMP_KEY);
if (!$cacheItem->isHit()) {
// no restart has ever been scheduled
return false;
}
return $this->workerStartedAt < $cacheItem->get();
}
}

View File

@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerStartedEvent;
/**
* @author Tobias Schultze <http://tobion.de>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*
* @deprecated since Symfony 6.4, use the {@see SignalableCommandInterface} instead
*/
class StopWorkerOnSignalsListener implements EventSubscriberInterface
{
private array $signals;
private ?LoggerInterface $logger;
public function __construct(?array $signals = null, ?LoggerInterface $logger = null)
{
if (null === $signals && \extension_loaded('pcntl')) {
$signals = [\SIGTERM, \SIGINT];
}
$this->signals = $signals ?? [];
$this->logger = $logger;
}
public function onWorkerStarted(WorkerStartedEvent $event): void
{
foreach ($this->signals as $signal) {
pcntl_signal($signal, function () use ($event, $signal) {
$this->logger?->info('Received signal {signal}.', ['signal' => $signal, 'transport_names' => $event->getWorker()->getMetadata()->getTransportNames()]);
$event->getWorker()->stop();
});
}
}
public static function getSubscribedEvents(): array
{
if (!\function_exists('pcntl_signal')) {
return [];
}
return [
WorkerStartedEvent::class => ['onWorkerStarted', 100],
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\SignalableCommandInterface;
trigger_deprecation('symfony/messenger', '6.3', '"%s" is deprecated, use the "%s" instead.', StopWorkerOnSigtermSignalListener::class, SignalableCommandInterface::class);
/**
* @author Tobias Schultze <http://tobion.de>
*
* @deprecated since Symfony 6.3, use the {@see SignalableCommandInterface} instead
*/
class StopWorkerOnSigtermSignalListener extends StopWorkerOnSignalsListener
{
public function __construct(?LoggerInterface $logger = null)
{
parent::__construct(\extension_loaded('pcntl') ? [\SIGTERM] : [], $logger);
}
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Event\WorkerStartedEvent;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
/**
* @author Simon Delicata <simon.delicata@free.fr>
* @author Tobias Schultze <http://tobion.de>
*/
class StopWorkerOnTimeLimitListener implements EventSubscriberInterface
{
private int $timeLimitInSeconds;
private ?LoggerInterface $logger;
private float $endTime = 0;
public function __construct(int $timeLimitInSeconds, ?LoggerInterface $logger = null)
{
$this->timeLimitInSeconds = $timeLimitInSeconds;
$this->logger = $logger;
if ($timeLimitInSeconds <= 0) {
throw new InvalidArgumentException('Time limit must be greater than zero.');
}
}
public function onWorkerStarted(): void
{
$startTime = microtime(true);
$this->endTime = $startTime + $this->timeLimitInSeconds;
}
public function onWorkerRunning(WorkerRunningEvent $event): void
{
if ($this->endTime < microtime(true)) {
$event->getWorker()->stop();
$this->logger?->info('Worker stopped due to time limit of {timeLimit}s exceeded', ['timeLimit' => $this->timeLimitInSeconds]);
}
}
public static function getSubscribedEvents(): array
{
return [
WorkerStartedEvent::class => 'onWorkerStarted',
WorkerRunningEvent::class => 'onWorkerRunning',
];
}
}

View File

@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
use Symfony\Component\Messenger\Envelope;
/**
* When handling queued messages from {@link DispatchAfterCurrentBusMiddleware},
* some handlers caused an exception. This exception contains all those handler exceptions.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class DelayedMessageHandlingException extends RuntimeException implements WrappedExceptionsInterface, EnvelopeAwareExceptionInterface
{
use EnvelopeAwareExceptionTrait;
use WrappedExceptionsTrait;
private array $exceptions;
public function __construct(array $exceptions, ?Envelope $envelope = null)
{
$this->envelope = $envelope;
$exceptionMessages = implode(", \n", array_map(
fn (\Throwable $e) => $e::class.': '.$e->getMessage(),
$exceptions
));
if (1 === \count($exceptions)) {
$message = \sprintf("A delayed message handler threw an exception: \n\n%s", $exceptionMessages);
} else {
$message = \sprintf("Some delayed message handlers threw an exception: \n\n%s", $exceptionMessages);
}
$this->exceptions = $exceptions;
parent::__construct($message, 0, $exceptions[array_key_first($exceptions)]);
}
/**
* @deprecated since Symfony 6.4, use {@see self::getWrappedExceptions()} instead
*/
public function getExceptions(): array
{
trigger_deprecation('symfony/messenger', '6.4', 'The "%s()" method is deprecated, use "%s::getWrappedExceptions()" instead.', __METHOD__, self::class);
return $this->exceptions;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
use Symfony\Component\Messenger\Envelope;
/**
* @internal
*/
interface EnvelopeAwareExceptionInterface
{
public function getEnvelope(): ?Envelope;
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
use Symfony\Component\Messenger\Envelope;
/**
* @internal
*/
trait EnvelopeAwareExceptionTrait
{
private ?Envelope $envelope = null;
public function getEnvelope(): ?Envelope
{
return $this->envelope;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* Base Messenger component's exception.
*
* @author Samuel Roze <samuel.roze@gmail.com>
*/
interface ExceptionInterface extends \Throwable
{
}

View File

@@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
use Symfony\Component\Messenger\Envelope;
class HandlerFailedException extends RuntimeException implements WrappedExceptionsInterface, EnvelopeAwareExceptionInterface
{
use WrappedExceptionsTrait;
private Envelope $envelope;
/**
* @param \Throwable[] $exceptions The name of the handler should be given as key
*/
public function __construct(Envelope $envelope, array $exceptions)
{
$firstFailure = current($exceptions);
$message = \sprintf('Handling "%s" failed: ', $envelope->getMessage()::class);
parent::__construct(
$message.(1 === \count($exceptions)
? $firstFailure->getMessage()
: \sprintf('%d handlers failed. First failure is: %s', \count($exceptions), $firstFailure->getMessage())
),
(int) $firstFailure->getCode(),
$firstFailure
);
$this->envelope = $envelope;
$this->exceptions = $exceptions;
}
public function getEnvelope(): Envelope
{
return $this->envelope;
}
/**
* @deprecated since Symfony 6.4, use {@see self::getWrappedExceptions()} instead
*
* @return \Throwable[]
*/
public function getNestedExceptions(): array
{
trigger_deprecation('symfony/messenger', '6.4', 'The "%s()" method is deprecated, use "%s::getWrappedExceptions()" instead.', __METHOD__, self::class);
return array_values($this->exceptions);
}
/**
* @deprecated since Symfony 6.4, use {@see self::getWrappedExceptions()} instead
*/
public function getNestedExceptionOfClass(string $exceptionClassName): array
{
trigger_deprecation('symfony/messenger', '6.4', 'The "%s()" method is deprecated, use "%s::getWrappedExceptions()" instead.', __METHOD__, self::class);
return array_values(
array_filter(
$this->exceptions,
fn ($exception) => is_a($exception, $exceptionClassName)
)
);
}
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Roland Franssen <franssen.roland@gmail.com>
*/
class LogicException extends \LogicException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* Thrown when a message cannot be decoded in a serializer.
*/
class MessageDecodingFailedException extends InvalidArgumentException
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class NoHandlerForMessageException extends LogicException
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Jérémy Reynaud <jeremy@reynaud.io>
*/
class NoSenderForMessageException extends LogicException
{
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* Marker interface for exceptions to indicate that handling a message should have worked.
*
* If something goes wrong while handling a message that's received from a transport
* and the message should be retried, a handler can throw such an exception.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface RecoverableExceptionInterface extends \Throwable
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* A concrete implementation of RecoverableExceptionInterface that can be used directly.
*
* @author Frederic Bouchery <frederic@bouchery.fr>
*/
class RecoverableMessageHandlingException extends RuntimeException implements RecoverableExceptionInterface
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Tobias Schultze <http://tobion.de>
*/
class RejectRedeliveredMessageException extends RuntimeException
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class StopWorkerException extends RuntimeException implements StopWorkerExceptionInterface
{
public function __construct(string $message = 'Worker should stop.', ?\Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
}
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
interface StopWorkerExceptionInterface extends \Throwable
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Eric Masoero <em@studeal.fr>
*/
class TransportException extends RuntimeException
{
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* Marker interface for exceptions to indicate that handling a message will continue to fail.
*
* If something goes wrong while handling a message that's received from a transport
* and the message should not be retried, a handler can throw such an exception.
*
* @author Tobias Schultze <http://tobion.de>
*/
interface UnrecoverableExceptionInterface extends \Throwable
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* A concrete implementation of UnrecoverableExceptionInterface that can be used directly.
*
* @author Frederic Bouchery <frederic@bouchery.fr>
*/
class UnrecoverableMessageHandlingException extends RuntimeException implements UnrecoverableExceptionInterface
{
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class ValidationFailedException extends RuntimeException implements EnvelopeAwareExceptionInterface
{
use EnvelopeAwareExceptionTrait;
private ConstraintViolationListInterface $violations;
private object $violatingMessage;
public function __construct(object $violatingMessage, ConstraintViolationListInterface $violations, ?Envelope $envelope = null)
{
$this->violatingMessage = $violatingMessage;
$this->violations = $violations;
$this->envelope = $envelope;
parent::__construct(\sprintf('Message of type "%s" failed validation.', $this->violatingMessage::class));
}
/**
* @return object
*/
public function getViolatingMessage()
{
return $this->violatingMessage;
}
public function getViolations(): ConstraintViolationListInterface
{
return $this->violations;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* Exception that holds multiple exceptions thrown by one or more handlers and/or messages.
*
* @author Jeroen <https://github.com/Jeroeny>
*/
interface WrappedExceptionsInterface
{
/**
* @return \Throwable[]
*/
public function getWrappedExceptions(?string $class = null, bool $recursive = false): array;
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Jeroen <https://github.com/Jeroeny>
*
* @internal
*/
trait WrappedExceptionsTrait
{
private array $exceptions;
/**
* @return \Throwable[]
*/
public function getWrappedExceptions(?string $class = null, bool $recursive = false): array
{
return $this->getWrappedExceptionsRecursively($class, $recursive, $this->exceptions);
}
/**
* @param class-string<\Throwable>|null $class
* @param iterable<\Throwable> $exceptions
*
* @return \Throwable[]
*/
private function getWrappedExceptionsRecursively(?string $class, bool $recursive, iterable $exceptions): array
{
$unwrapped = [];
foreach ($exceptions as $key => $exception) {
if ($recursive && $exception instanceof WrappedExceptionsInterface) {
$unwrapped[] = $this->getWrappedExceptionsRecursively($class, $recursive, $exception->getWrappedExceptions());
continue;
}
if ($class && !is_a($exception, $class)) {
continue;
}
$unwrapped[] = [$key => $exception];
}
return array_merge(...$unwrapped);
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger;
use Symfony\Component\Messenger\Exception\LogicException;
use Symfony\Component\Messenger\Stamp\HandledStamp;
/**
* Leverages a message bus to expect a single, synchronous message handling and return its result.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
trait HandleTrait
{
private MessageBusInterface $messageBus;
/**
* Dispatches the given message, expecting to be handled by a single handler
* and returns the result from the handler returned value.
* This behavior is useful for both synchronous command & query buses,
* the last one usually returning the handler result.
*
* @param object|Envelope $message The message or the message pre-wrapped in an envelope
*/
private function handle(object $message): mixed
{
if (!isset($this->messageBus)) {
throw new LogicException(\sprintf('You must provide a "%s" instance in the "%s::$messageBus" property, but that property has not been initialized yet.', MessageBusInterface::class, static::class));
}
$envelope = $this->messageBus->dispatch($message);
/** @var HandledStamp[] $handledStamps */
$handledStamps = $envelope->all(HandledStamp::class);
if (!$handledStamps) {
throw new LogicException(\sprintf('Message of type "%s" was handled zero times. Exactly one handler is expected when using "%s::%s()".', get_debug_type($envelope->getMessage()), static::class, __FUNCTION__));
}
if (\count($handledStamps) > 1) {
$handlers = implode(', ', array_map(fn (HandledStamp $stamp): string => \sprintf('"%s"', $stamp->getHandlerName()), $handledStamps));
throw new LogicException(\sprintf('Message of type "%s" was handled multiple times. Only one handler is expected when using "%s::%s()", got %d: %s.', get_debug_type($envelope->getMessage()), static::class, __FUNCTION__, \count($handledStamps), $handlers));
}
return $handledStamps[0]->getResult();
}
}

View File

@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
use Symfony\Component\Messenger\Exception\LogicException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class Acknowledger
{
private string $handlerClass;
private ?\Closure $ack;
private ?\Throwable $error = null;
private mixed $result = null;
/**
* @param \Closure(\Throwable|null, mixed):void|null $ack
*/
public function __construct(string $handlerClass, ?\Closure $ack = null)
{
$this->handlerClass = $handlerClass;
$this->ack = $ack ?? static function () {};
}
/**
* @param mixed $result
*/
public function ack($result = null): void
{
$this->doAck(null, $result);
}
public function nack(\Throwable $error): void
{
$this->doAck($error);
}
public function getError(): ?\Throwable
{
return $this->error;
}
public function getResult(): mixed
{
return $this->result;
}
public function isAcknowledged(): bool
{
return null === $this->ack;
}
public function __destruct()
{
if (null !== $this->ack) {
throw new LogicException(\sprintf('The acknowledger was not called by the "%s" batch handler.', $this->handlerClass));
}
}
private function doAck(?\Throwable $e = null, mixed $result = null): void
{
if (!$ack = $this->ack) {
throw new LogicException(\sprintf('The acknowledger cannot be called twice by the "%s" batch handler.', $this->handlerClass));
}
$this->ack = null;
$this->error = $e;
$this->result = $result;
$ack($e, $result);
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
interface BatchHandlerInterface
{
/**
* @param Acknowledger|null $ack The function to call to ack/nack the $message.
* The message should be handled synchronously when null.
*
* @return mixed The number of pending messages in the batch if $ack is not null,
* the result from handling the message otherwise
*/
// public function __invoke(object $message, ?Acknowledger $ack = null): mixed;
/**
* Flushes any pending buffers.
*
* @param bool $force Whether flushing is required; it can be skipped if not
*/
public function flush(bool $force): void;
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
trait BatchHandlerTrait
{
private array $jobs = [];
public function flush(bool $force): void
{
if ($jobs = $this->jobs) {
$this->jobs = [];
$this->process($jobs);
}
}
/**
* @param Acknowledger|null $ack The function to call to ack/nack the $message.
* The message should be handled synchronously when null.
*
* @return mixed The number of pending messages in the batch if $ack is not null,
* the result from handling the message otherwise
*/
private function handle(object $message, ?Acknowledger $ack): mixed
{
if (null === $ack) {
$ack = new Acknowledger(get_debug_type($this));
$this->jobs[] = [$message, $ack];
$this->flush(true);
return $ack->getResult();
}
$this->jobs[] = [$message, $ack];
if (!$this->shouldFlush()) {
return \count($this->jobs);
}
$this->flush(true);
return 0;
}
private function shouldFlush(): bool
{
return $this->getBatchSize() <= \count($this->jobs);
}
/**
* Completes the jobs in the list.
*
* @param list<array{0: object, 1: Acknowledger}> $jobs A list of pairs of messages and their corresponding acknowledgers
*/
abstract private function process(array $jobs): void;
private function getBatchSize(): int
{
return 10;
}
}

View File

@@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
/**
* Describes a handler and the possible associated options, such as `from_transport`, `bus`, etc.
*
* @author Samuel Roze <samuel.roze@gmail.com>
*/
final class HandlerDescriptor
{
private \Closure $handler;
private string $name;
private ?BatchHandlerInterface $batchHandler = null;
private array $options;
public function __construct(callable $handler, array $options = [])
{
$handler = $handler(...);
$this->handler = $handler;
$this->options = $options;
$r = new \ReflectionFunction($handler);
if (str_contains($r->name, '{closure')) {
$this->name = 'Closure';
} elseif (!$handler = $r->getClosureThis()) {
$class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass();
$this->name = ($class ? $class->name.'::' : '').$r->name;
} else {
if ($handler instanceof BatchHandlerInterface) {
$this->batchHandler = $handler;
}
$this->name = $handler::class.'::'.$r->name;
}
}
public function getHandler(): callable
{
return $this->handler;
}
public function getName(): string
{
$name = $this->name;
$alias = $this->options['alias'] ?? null;
if (null !== $alias) {
$name .= '@'.$alias;
}
return $name;
}
public function getBatchHandler(): ?BatchHandlerInterface
{
return $this->batchHandler;
}
public function getOption(string $option): mixed
{
return $this->options[$option] ?? null;
}
public function getOptions(): array
{
return $this->options;
}
}

View File

@@ -0,0 +1,99 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
/**
* Maps a message to a list of handlers.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class HandlersLocator implements HandlersLocatorInterface
{
private array $handlers;
/**
* @param HandlerDescriptor[][]|callable[][] $handlers
*/
public function __construct(array $handlers)
{
$this->handlers = $handlers;
}
public function getHandlers(Envelope $envelope): iterable
{
$seen = [];
foreach (self::listTypes($envelope) as $type) {
foreach ($this->handlers[$type] ?? [] as $handlerDescriptor) {
if (\is_callable($handlerDescriptor)) {
$handlerDescriptor = new HandlerDescriptor($handlerDescriptor);
}
if (!$this->shouldHandle($envelope, $handlerDescriptor)) {
continue;
}
$name = $handlerDescriptor->getName();
if (\in_array($name, $seen)) {
continue;
}
$seen[] = $name;
yield $handlerDescriptor;
}
}
}
/**
* @internal
*/
public static function listTypes(Envelope $envelope): array
{
$class = $envelope->getMessage()::class;
return [$class => $class]
+ class_parents($class)
+ class_implements($class)
+ self::listWildcards($class)
+ ['*' => '*'];
}
private static function listWildcards(string $type): array
{
$type .= '\*';
$wildcards = [];
while ($i = strrpos($type, '\\', -3)) {
$type = substr_replace($type, '\*', $i);
$wildcards[$type] = $type;
}
return $wildcards;
}
private function shouldHandle(Envelope $envelope, HandlerDescriptor $handlerDescriptor): bool
{
if (null === $received = $envelope->last(ReceivedStamp::class)) {
return true;
}
if (null === $expectedTransport = $handlerDescriptor->getOption('from_transport')) {
return true;
}
return $received->getTransportName() === $expectedTransport;
}
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
use Symfony\Component\Messenger\Envelope;
/**
* Maps a message to a list of handlers.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface HandlersLocatorInterface
{
/**
* Returns the handlers for the given message name.
*
* @return iterable<int, HandlerDescriptor>
*/
public function getHandlers(Envelope $envelope): iterable;
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
/**
* Marker interface for message handlers.
*
* @author Samuel Roze <samuel.roze@gmail.com>
*
* @deprecated since Symfony 6.2, use the {@see AsMessageHandler} attribute instead
*/
interface MessageHandlerInterface
{
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
/**
* Handlers can implement this interface to handle multiple messages.
*
* @author Samuel Roze <samuel.roze@gmail.com>
*
* @deprecated since Symfony 6.2, use the {@see AsMessageHandler} attribute instead
*/
interface MessageSubscriberInterface extends MessageHandlerInterface
{
/**
* Returns a list of messages to be handled.
*
* It returns a list of messages like in the following example:
*
* yield MyMessage::class;
*
* It can also change the priority per classes.
*
* yield FirstMessage::class => ['priority' => 0];
* yield SecondMessage::class => ['priority' => -10];
*
* It can also specify a method, a priority, a bus and/or a transport per message:
*
* yield FirstMessage::class => ['method' => 'firstMessageMethod'];
* yield SecondMessage::class => [
* 'method' => 'secondMessageMethod',
* 'priority' => 20,
* 'bus' => 'my_bus_name',
* 'from_transport' => 'your_transport_name',
* ];
*
* The benefit of using `yield` instead of returning an array is that you can `yield` multiple times the
* same key and therefore subscribe to the same message multiple times with different options.
*
* The `__invoke` method of the handler will be called as usual with the message to handle.
*/
public static function getHandledMessages(): iterable;
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
use Symfony\Component\Messenger\Message\RedispatchMessage;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\TransportNamesStamp;
final class RedispatchMessageHandler
{
public function __construct(
private MessageBusInterface $bus,
) {
}
public function __invoke(RedispatchMessage $message): void
{
$this->bus->dispatch($message->envelope, [new TransportNamesStamp($message->transportNames)]);
}
}

19
vendor/symfony/messenger/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2018-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Message;
use Symfony\Component\Messenger\Envelope;
final class RedispatchMessage implements \Stringable
{
/**
* @param object|Envelope $envelope The message or the message pre-wrapped in an envelope
* @param string[]|string $transportNames Transport names to be used for the message
*/
public function __construct(
public readonly object $envelope,
public readonly array|string $transportNames = [],
) {
}
public function __toString(): string
{
$message = $this->envelope instanceof Envelope ? $this->envelope->getMessage() : $this->envelope;
return \sprintf('%s via %s', $message instanceof \Stringable ? (string) $message : $message::class, implode(', ', (array) $this->transportNames));
}
}

72
vendor/symfony/messenger/MessageBus.php vendored Normal file
View File

@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackMiddleware;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
* @author Matthias Noback <matthiasnoback@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class MessageBus implements MessageBusInterface
{
private \IteratorAggregate $middlewareAggregate;
/**
* @param iterable<mixed, MiddlewareInterface> $middlewareHandlers
*/
public function __construct(iterable $middlewareHandlers = [])
{
if ($middlewareHandlers instanceof \IteratorAggregate) {
$this->middlewareAggregate = $middlewareHandlers;
} elseif (\is_array($middlewareHandlers)) {
$this->middlewareAggregate = new \ArrayObject($middlewareHandlers);
} else {
// $this->middlewareAggregate should be an instance of IteratorAggregate.
// When $middlewareHandlers is an Iterator, we wrap it to ensure it is lazy-loaded and can be rewound.
$this->middlewareAggregate = new class($middlewareHandlers) implements \IteratorAggregate {
private \Traversable $middlewareHandlers;
private \ArrayObject $cachedIterator;
public function __construct(\Traversable $middlewareHandlers)
{
$this->middlewareHandlers = $middlewareHandlers;
}
public function getIterator(): \Traversable
{
return $this->cachedIterator ??= new \ArrayObject(iterator_to_array($this->middlewareHandlers, false));
}
};
}
}
public function dispatch(object $message, array $stamps = []): Envelope
{
$envelope = Envelope::wrap($message, $stamps);
$middlewareIterator = $this->middlewareAggregate->getIterator();
while ($middlewareIterator instanceof \IteratorAggregate) {
$middlewareIterator = $middlewareIterator->getIterator();
}
$middlewareIterator->rewind();
if (!$middlewareIterator->valid()) {
return $envelope;
}
$stack = new StackMiddleware($middlewareIterator);
return $middlewareIterator->current()->handle($envelope, $stack);
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger;
use Symfony\Component\Messenger\Stamp\StampInterface;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
interface MessageBusInterface
{
/**
* Dispatches the given message.
*
* @param object|Envelope $message The message or the message pre-wrapped in an envelope
* @param StampInterface[] $stamps
*/
public function dispatch(object $message, array $stamps = []): Envelope;
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
/**
* Execute the inner middleware according to an activation strategy.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class ActivationMiddleware implements MiddlewareInterface
{
private MiddlewareInterface $inner;
private \Closure|bool $activated;
public function __construct(MiddlewareInterface $inner, bool|callable $activated)
{
$this->inner = $inner;
$this->activated = \is_bool($activated) ? $activated : $activated(...);
}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
if (\is_callable($this->activated) ? ($this->activated)($envelope) : $this->activated) {
return $this->inner->handle($envelope, $stack);
}
return $stack->next()->handle($envelope, $stack);
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\BusNameStamp;
/**
* Adds the BusNameStamp to the bus.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class AddBusNameStampMiddleware implements MiddlewareInterface
{
private string $busName;
public function __construct(string $busName)
{
$this->busName = $busName;
}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
if (null === $envelope->last(BusNameStamp::class)) {
$envelope = $envelope->with(new BusNameStamp($this->busName));
}
return $stack->next()->handle($envelope, $stack);
}
}

View File

@@ -0,0 +1,130 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\DelayedMessageHandlingException;
use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp;
/**
* Allow to configure messages to be handled after the current bus is finished.
*
* I.e, messages dispatched from a handler with a DispatchAfterCurrentBus stamp
* will actually be handled once the current message being dispatched is fully
* handled.
*
* For instance, using this middleware before the DoctrineTransactionMiddleware
* means sub-dispatched messages with a DispatchAfterCurrentBus stamp would be
* handled after the Doctrine transaction has been committed.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class DispatchAfterCurrentBusMiddleware implements MiddlewareInterface
{
/**
* @var QueuedEnvelope[] A queue of messages and next middleware
*/
private array $queue = [];
/**
* @var bool this property is used to signal if we are inside a the first/root call to
* MessageBusInterface::dispatch() or if dispatch has been called inside a message handler
*/
private bool $isRootDispatchCallRunning = false;
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
if (null !== $envelope->last(DispatchAfterCurrentBusStamp::class)) {
if ($this->isRootDispatchCallRunning) {
$this->queue[] = new QueuedEnvelope($envelope, $stack);
return $envelope;
}
$envelope = $envelope->withoutAll(DispatchAfterCurrentBusStamp::class);
}
if ($this->isRootDispatchCallRunning) {
/*
* A call to MessageBusInterface::dispatch() was made from inside the main bus handling,
* but the message does not have the stamp. So, process it like normal.
*/
return $stack->next()->handle($envelope, $stack);
}
// First time we get here, mark as inside a "root dispatch" call:
$this->isRootDispatchCallRunning = true;
try {
// Execute the whole middleware stack & message handling for main dispatch:
$returnedEnvelope = $stack->next()->handle($envelope, $stack);
} catch (\Throwable $exception) {
/*
* Whenever an exception occurs while handling a message that has
* queued other messages, we drop the queued ones.
* This is intentional since the queued commands were likely dependent
* on the preceding command.
*/
$this->queue = [];
$this->isRootDispatchCallRunning = false;
throw $exception;
}
// "Root dispatch" call is finished, dispatch stored messages.
$exceptions = [];
while (null !== $queueItem = array_shift($this->queue)) {
// Save how many messages are left in queue before handling the message
$queueLengthBefore = \count($this->queue);
try {
// Execute the stored messages
$queueItem->getStack()->next()->handle($queueItem->getEnvelope(), $queueItem->getStack());
} catch (\Exception $exception) {
// Gather all exceptions
$exceptions[] = $exception;
// Restore queue to previous state
$this->queue = \array_slice($this->queue, 0, $queueLengthBefore);
}
}
$this->isRootDispatchCallRunning = false;
if (\count($exceptions) > 0) {
throw new DelayedMessageHandlingException($exceptions, $returnedEnvelope);
}
return $returnedEnvelope;
}
}
/**
* @internal
*/
final class QueuedEnvelope
{
private Envelope $envelope;
private StackInterface $stack;
public function __construct(Envelope $envelope, StackInterface $stack)
{
$this->envelope = $envelope->withoutAll(DispatchAfterCurrentBusStamp::class);
$this->stack = $stack;
}
public function getEnvelope(): Envelope
{
return $this->envelope;
}
public function getStack(): StackInterface
{
return $this->stack;
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class FailedMessageProcessingMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
// look for "received" messages decorated with the SentToFailureTransportStamp
/** @var SentToFailureTransportStamp|null $sentToFailureStamp */
$sentToFailureStamp = $envelope->last(SentToFailureTransportStamp::class);
if (null !== $sentToFailureStamp && null !== $envelope->last(ReceivedStamp::class)) {
// mark the message as "received" from the original transport
// this guarantees the same behavior as when originally received
$envelope = $envelope->with(new ReceivedStamp($sentToFailureStamp->getOriginalReceiverName()));
}
return $stack->next()->handle($envelope, $stack);
}
}

View File

@@ -0,0 +1,154 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\LogicException;
use Symfony\Component\Messenger\Exception\NoHandlerForMessageException;
use Symfony\Component\Messenger\Handler\Acknowledger;
use Symfony\Component\Messenger\Handler\HandlerDescriptor;
use Symfony\Component\Messenger\Handler\HandlersLocatorInterface;
use Symfony\Component\Messenger\Stamp\AckStamp;
use Symfony\Component\Messenger\Stamp\FlushBatchHandlersStamp;
use Symfony\Component\Messenger\Stamp\HandledStamp;
use Symfony\Component\Messenger\Stamp\HandlerArgumentsStamp;
use Symfony\Component\Messenger\Stamp\NoAutoAckStamp;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class HandleMessageMiddleware implements MiddlewareInterface
{
use LoggerAwareTrait;
public function __construct(
private HandlersLocatorInterface $handlersLocator,
private bool $allowNoHandlers = false,
) {
}
/**
* @throws NoHandlerForMessageException When no handler is found and $allowNoHandlers is false
*/
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$handler = null;
$message = $envelope->getMessage();
$context = [
'class' => $message::class,
];
$exceptions = [];
$alreadyHandled = false;
foreach ($this->handlersLocator->getHandlers($envelope) as $handlerDescriptor) {
if ($this->messageHasAlreadyBeenHandled($envelope, $handlerDescriptor)) {
$alreadyHandled = true;
continue;
}
try {
$handler = $handlerDescriptor->getHandler();
$batchHandler = $handlerDescriptor->getBatchHandler();
/** @var AckStamp $ackStamp */
if ($batchHandler && $ackStamp = $envelope->last(AckStamp::class)) {
$ack = new Acknowledger(get_debug_type($batchHandler), static function (?\Throwable $e = null, $result = null) use ($envelope, $ackStamp, $handlerDescriptor) {
if (null !== $e) {
$e = new HandlerFailedException($envelope, [$handlerDescriptor->getName() => $e]);
} else {
$envelope = $envelope->with(HandledStamp::fromDescriptor($handlerDescriptor, $result));
}
$ackStamp->ack($envelope, $e);
});
$result = $this->callHandler($handler, $message, $ack, $envelope->last(HandlerArgumentsStamp::class));
if (!\is_int($result) || 0 > $result) {
throw new LogicException(\sprintf('A handler implementing BatchHandlerInterface must return the size of the current batch as a positive integer, "%s" returned from "%s".', \is_int($result) ? $result : get_debug_type($result), get_debug_type($batchHandler)));
}
if (!$ack->isAcknowledged()) {
$envelope = $envelope->with(new NoAutoAckStamp($handlerDescriptor));
} elseif ($ack->getError()) {
throw $ack->getError();
} else {
$result = $ack->getResult();
}
} else {
$result = $this->callHandler($handler, $message, null, $envelope->last(HandlerArgumentsStamp::class));
}
$handledStamp = HandledStamp::fromDescriptor($handlerDescriptor, $result);
$envelope = $envelope->with($handledStamp);
$this->logger?->info('Message {class} handled by {handler}', $context + ['handler' => $handledStamp->getHandlerName()]);
} catch (\Throwable $e) {
$exceptions[$handlerDescriptor->getName()] = $e;
}
}
/** @var FlushBatchHandlersStamp $flushStamp */
if ($flushStamp = $envelope->last(FlushBatchHandlersStamp::class)) {
/** @var NoAutoAckStamp $stamp */
foreach ($envelope->all(NoAutoAckStamp::class) as $stamp) {
try {
$handler = $stamp->getHandlerDescriptor()->getBatchHandler();
$handler->flush($flushStamp->force());
} catch (\Throwable $e) {
$exceptions[$stamp->getHandlerDescriptor()->getName()] = $e;
}
}
}
if (null === $handler && !$alreadyHandled) {
if (!$this->allowNoHandlers) {
throw new NoHandlerForMessageException(\sprintf('No handler for message "%s".', $context['class']));
}
$this->logger?->info('No handler for message {class}', $context);
}
if (\count($exceptions)) {
throw new HandlerFailedException($envelope, $exceptions);
}
return $stack->next()->handle($envelope, $stack);
}
private function messageHasAlreadyBeenHandled(Envelope $envelope, HandlerDescriptor $handlerDescriptor): bool
{
/** @var HandledStamp $stamp */
foreach ($envelope->all(HandledStamp::class) as $stamp) {
if ($stamp->getHandlerName() === $handlerDescriptor->getName()) {
return true;
}
}
return false;
}
private function callHandler(callable $handler, object $message, ?Acknowledger $ack, ?HandlerArgumentsStamp $handlerArgumentsStamp): mixed
{
$arguments = [$message];
if (null !== $ack) {
$arguments[] = $ack;
}
if (null !== $handlerArgumentsStamp) {
$arguments = [...$arguments, ...$handlerArgumentsStamp->getAdditionalArguments()];
}
return $handler(...$arguments);
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
interface MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope;
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceivedStamp;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\RejectRedeliveredMessageException;
/**
* Middleware that throws a RejectRedeliveredMessageException when a message is detected that has been redelivered by AMQP.
*
* The middleware runs before the HandleMessageMiddleware and prevents redelivered messages from being handled directly.
* The thrown exception is caught by the worker and will trigger the retry logic according to the retry strategy.
*
* AMQP redelivers messages when they do not get acknowledged or rejected. This can happen when the connection times out
* or an exception is thrown before acknowledging or rejecting. When such errors happen again while handling the
* redelivered message, the message would get redelivered again and again. The purpose of this middleware is to prevent
* infinite redelivery loops and to unblock the queue by republishing the redelivered messages as retries with a retry
* limit and potential delay.
*
* @author Tobias Schultze <http://tobion.de>
*/
class RejectRedeliveredMessageMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$amqpReceivedStamp = $envelope->last(AmqpReceivedStamp::class);
if ($amqpReceivedStamp instanceof AmqpReceivedStamp && $amqpReceivedStamp->getAmqpEnvelope()->isRedelivery()) {
throw new RejectRedeliveredMessageException('Redelivered message from AMQP detected that will be rejected and trigger the retry logic.');
}
return $stack->next()->handle($envelope, $stack);
}
}

View File

@@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp;
use Symfony\Component\Messenger\Stamp\RouterContextStamp;
use Symfony\Component\Routing\RequestContextAwareInterface;
/**
* Restore the Router context when processing the message.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class RouterContextMiddleware implements MiddlewareInterface
{
private RequestContextAwareInterface $router;
public function __construct(RequestContextAwareInterface $router)
{
$this->router = $router;
}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
if (!$envelope->last(ConsumedByWorkerStamp::class) || !$contextStamp = $envelope->last(RouterContextStamp::class)) {
$context = $this->router->getContext();
$envelope = $envelope->with(new RouterContextStamp(
$context->getBaseUrl(),
$context->getMethod(),
$context->getHost(),
$context->getScheme(),
$context->getHttpPort(),
$context->getHttpsPort(),
$context->getPathInfo(),
$context->getQueryString()
));
return $stack->next()->handle($envelope, $stack);
}
$context = $this->router->getContext();
$currentBaseUrl = $context->getBaseUrl();
$currentMethod = $context->getMethod();
$currentHost = $context->getHost();
$currentScheme = $context->getScheme();
$currentHttpPort = $context->getHttpPort();
$currentHttpsPort = $context->getHttpsPort();
$currentPathInfo = $context->getPathInfo();
$currentQueryString = $context->getQueryString();
$context
->setBaseUrl($contextStamp->getBaseUrl())
->setMethod($contextStamp->getMethod())
->setHost($contextStamp->getHost())
->setScheme($contextStamp->getScheme())
->setHttpPort($contextStamp->getHttpPort())
->setHttpsPort($contextStamp->getHttpsPort())
->setPathInfo($contextStamp->getPathInfo())
->setQueryString($contextStamp->getQueryString())
;
try {
return $stack->next()->handle($envelope, $stack);
} finally {
$context
->setBaseUrl($currentBaseUrl)
->setMethod($currentMethod)
->setHost($currentHost)
->setScheme($currentScheme)
->setHttpPort($currentHttpPort)
->setHttpsPort($currentHttpsPort)
->setPathInfo($currentPathInfo)
->setQueryString($currentQueryString)
;
}
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent;
use Symfony\Component\Messenger\Exception\NoSenderForMessageException;
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
use Symfony\Component\Messenger\Stamp\SentStamp;
use Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
* @author Tobias Schultze <http://tobion.de>
*/
class SendMessageMiddleware implements MiddlewareInterface
{
use LoggerAwareTrait;
public function __construct(
private SendersLocatorInterface $sendersLocator,
private ?EventDispatcherInterface $eventDispatcher = null,
private bool $allowNoSenders = true,
) {
}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$context = [
'class' => $envelope->getMessage()::class,
];
$sender = null;
if ($envelope->all(ReceivedStamp::class)) {
// it's a received message, do not send it back
$this->logger?->info('Received message {class}', $context);
} else {
$shouldDispatchEvent = true;
$senders = $this->sendersLocator->getSenders($envelope);
$senders = \is_array($senders) ? $senders : iterator_to_array($senders);
foreach ($senders as $alias => $sender) {
if (null !== $this->eventDispatcher && $shouldDispatchEvent) {
$event = new SendMessageToTransportsEvent($envelope, $senders);
$this->eventDispatcher->dispatch($event);
$envelope = $event->getEnvelope();
$shouldDispatchEvent = false;
}
$this->logger?->info('Sending message {class} with {alias} sender using {sender}', $context + ['alias' => $alias, 'sender' => $sender::class]);
$envelope = $sender->send($envelope->with(new SentStamp($sender::class, \is_string($alias) ? $alias : null)));
}
if (!$this->allowNoSenders && !$sender) {
throw new NoSenderForMessageException(\sprintf('No sender for message "%s".', $context['class']));
}
}
if (null === $sender) {
return $stack->next()->handle($envelope, $stack);
}
// message should only be sent and not be handled by the next middleware
return $envelope;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* Implementations must be cloneable, and each clone must unstack the stack independently.
*/
interface StackInterface
{
/**
* Returns the next middleware to process a message.
*/
public function next(): MiddlewareInterface;
}

View File

@@ -0,0 +1,90 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class StackMiddleware implements MiddlewareInterface, StackInterface
{
private MiddlewareStack $stack;
private int $offset = 0;
/**
* @param iterable<mixed, MiddlewareInterface>|MiddlewareInterface|null $middlewareIterator
*/
public function __construct(iterable|MiddlewareInterface|null $middlewareIterator = null)
{
$this->stack = new MiddlewareStack();
if (null === $middlewareIterator) {
return;
}
if ($middlewareIterator instanceof \Iterator) {
$this->stack->iterator = $middlewareIterator;
} elseif ($middlewareIterator instanceof MiddlewareInterface) {
$this->stack->stack[] = $middlewareIterator;
} else {
$this->stack->iterator = (function () use ($middlewareIterator) {
yield from $middlewareIterator;
})();
}
}
public function next(): MiddlewareInterface
{
if (null === $next = $this->stack->next($this->offset)) {
return $this;
}
++$this->offset;
return $next;
}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
return $envelope;
}
}
/**
* @internal
*/
class MiddlewareStack
{
/** @var \Iterator<mixed, MiddlewareInterface>|null */
public ?\Iterator $iterator = null;
public array $stack = [];
public function next(int $offset): ?MiddlewareInterface
{
if (isset($this->stack[$offset])) {
return $this->stack[$offset];
}
if (null === $this->iterator) {
return null;
}
$this->iterator->next();
if (!$this->iterator->valid()) {
return $this->iterator = null;
}
return $this->stack[] = $this->iterator->current();
}
}

View File

@@ -0,0 +1,96 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Stopwatch\Stopwatch;
/**
* Collects some data about a middleware.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class TraceableMiddleware implements MiddlewareInterface
{
private Stopwatch $stopwatch;
private string $busName;
private string $eventCategory;
public function __construct(Stopwatch $stopwatch, string $busName, string $eventCategory = 'messenger.middleware')
{
$this->stopwatch = $stopwatch;
$this->busName = $busName;
$this->eventCategory = $eventCategory;
}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$stack = new TraceableStack($stack, $this->stopwatch, $this->busName, $this->eventCategory);
try {
return $stack->next()->handle($envelope, $stack);
} finally {
$stack->stop();
}
}
}
/**
* @internal
*/
class TraceableStack implements StackInterface
{
private StackInterface $stack;
private Stopwatch $stopwatch;
private string $busName;
private string $eventCategory;
private ?string $currentEvent = null;
public function __construct(StackInterface $stack, Stopwatch $stopwatch, string $busName, string $eventCategory)
{
$this->stack = $stack;
$this->stopwatch = $stopwatch;
$this->busName = $busName;
$this->eventCategory = $eventCategory;
}
public function next(): MiddlewareInterface
{
if (null !== $this->currentEvent && $this->stopwatch->isStarted($this->currentEvent)) {
$this->stopwatch->stop($this->currentEvent);
}
if ($this->stack === $nextMiddleware = $this->stack->next()) {
$this->currentEvent = 'Tail';
} else {
$this->currentEvent = \sprintf('"%s"', get_debug_type($nextMiddleware));
}
$this->currentEvent .= \sprintf(' on "%s"', $this->busName);
$this->stopwatch->start($this->currentEvent, $this->eventCategory);
return $nextMiddleware;
}
public function stop(): void
{
if (null !== $this->currentEvent && $this->stopwatch->isStarted($this->currentEvent)) {
$this->stopwatch->stop($this->currentEvent);
}
$this->currentEvent = null;
}
public function __clone()
{
$this->stack = clone $this->stack;
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\ValidationFailedException;
use Symfony\Component\Messenger\Stamp\ValidationStamp;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class ValidationMiddleware implements MiddlewareInterface
{
private ValidatorInterface $validator;
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$message = $envelope->getMessage();
$groups = null;
/** @var ValidationStamp|null $validationStamp */
if ($validationStamp = $envelope->last(ValidationStamp::class)) {
$groups = $validationStamp->getGroups();
}
$violations = $this->validator->validate($message, null, $groups);
if (\count($violations)) {
throw new ValidationFailedException($message, $violations, $envelope);
}
return $stack->next()->handle($envelope, $stack);
}
}

View File

@@ -0,0 +1,91 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Retry;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
/**
* A retry strategy with a constant or exponential retry delay.
*
* For example, if $delayMilliseconds=10000 & $multiplier=1 (default),
* each retry will wait exactly 10 seconds.
*
* But if $delayMilliseconds=10000 & $multiplier=2:
* * Retry 1: 10 second delay
* * Retry 2: 20 second delay (10000 * 2 = 20000)
* * Retry 3: 40 second delay (20000 * 2 = 40000)
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*
* @final
*/
class MultiplierRetryStrategy implements RetryStrategyInterface
{
private int $maxRetries;
private int $delayMilliseconds;
private float $multiplier;
private int $maxDelayMilliseconds;
/**
* @param int $maxRetries The maximum number of times to retry
* @param int $delayMilliseconds Amount of time to delay (or the initial value when multiplier is used)
* @param float $multiplier Multiplier to apply to the delay each time a retry occurs
* @param int $maxDelayMilliseconds Maximum delay to allow (0 means no maximum)
*/
public function __construct(int $maxRetries = 3, int $delayMilliseconds = 1000, float $multiplier = 1, int $maxDelayMilliseconds = 0)
{
$this->maxRetries = $maxRetries;
if ($delayMilliseconds < 0) {
throw new InvalidArgumentException(\sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMilliseconds));
}
$this->delayMilliseconds = $delayMilliseconds;
if ($multiplier < 1) {
throw new InvalidArgumentException(\sprintf('Multiplier must be greater than zero: "%s" given.', $multiplier));
}
$this->multiplier = $multiplier;
if ($maxDelayMilliseconds < 0) {
throw new InvalidArgumentException(\sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMilliseconds));
}
$this->maxDelayMilliseconds = $maxDelayMilliseconds;
}
/**
* @param \Throwable|null $throwable The cause of the failed handling
*/
public function isRetryable(Envelope $message, ?\Throwable $throwable = null): bool
{
$retries = RedeliveryStamp::getRetryCountFromEnvelope($message);
return $retries < $this->maxRetries;
}
/**
* @param \Throwable|null $throwable The cause of the failed handling
*/
public function getWaitingTime(Envelope $message, ?\Throwable $throwable = null): int
{
$retries = RedeliveryStamp::getRetryCountFromEnvelope($message);
$delay = $this->delayMilliseconds * $this->multiplier ** $retries;
if ($delay > $this->maxDelayMilliseconds && 0 !== $this->maxDelayMilliseconds) {
return $this->maxDelayMilliseconds;
}
return (int) ceil($delay);
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Retry;
use Symfony\Component\Messenger\Envelope;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
interface RetryStrategyInterface
{
/**
* @param \Throwable|null $throwable The cause of the failed handling
*/
public function isRetryable(Envelope $message, ?\Throwable $throwable = null): bool;
/**
* @param \Throwable|null $throwable The cause of the failed handling
*
* @return int The time to delay/wait in milliseconds
*/
public function getWaitingTime(Envelope $message, ?\Throwable $throwable = null): int;
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger;
use Psr\Container\ContainerInterface;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
use Symfony\Component\Messenger\Stamp\BusNameStamp;
/**
* Bus of buses that is routable using a BusNameStamp.
*
* This is useful when passed to Worker: messages received
* from the transport can be sent to the correct bus.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class RoutableMessageBus implements MessageBusInterface
{
private ContainerInterface $busLocator;
private ?MessageBusInterface $fallbackBus;
public function __construct(ContainerInterface $busLocator, ?MessageBusInterface $fallbackBus = null)
{
$this->busLocator = $busLocator;
$this->fallbackBus = $fallbackBus;
}
public function dispatch(object $envelope, array $stamps = []): Envelope
{
if (!$envelope instanceof Envelope) {
throw new InvalidArgumentException('Messages passed to RoutableMessageBus::dispatch() must be inside an Envelope.');
}
/** @var BusNameStamp|null $busNameStamp */
$busNameStamp = $envelope->last(BusNameStamp::class);
if (null === $busNameStamp) {
if (null === $this->fallbackBus) {
throw new InvalidArgumentException('Envelope is missing a BusNameStamp and no fallback message bus is configured on RoutableMessageBus.');
}
return $this->fallbackBus->dispatch($envelope, $stamps);
}
return $this->getMessageBus($busNameStamp->getBusName())->dispatch($envelope, $stamps);
}
/**
* @internal
*/
public function getMessageBus(string $busName): MessageBusInterface
{
if (!$this->busLocator->has($busName)) {
throw new InvalidArgumentException(\sprintf('Bus named "%s" does not exist.', $busName));
}
return $this->busLocator->get($busName);
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\Messenger\Envelope;
/**
* Marker stamp for messages that can be ack/nack'ed.
*/
final class AckStamp implements NonSendableStampInterface
{
/**
* @param \Closure(Envelope, \Throwable|null) $ack
*/
public function __construct(
private readonly \Closure $ack,
) {
}
public function ack(Envelope $envelope, ?\Throwable $e = null): void
{
($this->ack)($envelope, $e);
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* Stamp used to identify which bus it was passed to.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
final class BusNameStamp implements StampInterface
{
private string $busName;
public function __construct(string $busName)
{
$this->busName = $busName;
}
public function getBusName(): string
{
return $this->busName;
}
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* A marker that this message was consumed by a worker process.
*/
class ConsumedByWorkerStamp implements NonSendableStampInterface
{
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* Apply this stamp to delay delivery of your message on a transport.
*/
final class DelayStamp implements StampInterface
{
private int $delay;
/**
* @param int $delay The delay in milliseconds
*/
public function __construct(int $delay)
{
$this->delay = $delay;
}
public function getDelay(): int
{
return $this->delay;
}
public static function delayFor(\DateInterval $interval): self
{
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
$end = $now->add($interval);
return new self(($end->getTimestamp() - $now->getTimestamp()) * 1000);
}
public static function delayUntil(\DateTimeInterface $dateTime): self
{
return new self(($dateTime->getTimestamp() - time()) * 1000);
}
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* Marker item to tell this message should be handled in after the current bus has finished.
*
* @see \Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class DispatchAfterCurrentBusStamp implements NonSendableStampInterface
{
}

View File

@@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
/**
* Stamp applied when a messages fails due to an exception in the handler.
*/
final class ErrorDetailsStamp implements StampInterface
{
private string $exceptionClass;
private int|string $exceptionCode;
private string $exceptionMessage;
private ?FlattenException $flattenException;
public function __construct(string $exceptionClass, int|string $exceptionCode, string $exceptionMessage, ?FlattenException $flattenException = null)
{
$this->exceptionClass = $exceptionClass;
$this->exceptionCode = $exceptionCode;
$this->exceptionMessage = $exceptionMessage;
$this->flattenException = $flattenException;
}
public static function create(\Throwable $throwable): self
{
if ($throwable instanceof HandlerFailedException) {
$throwable = $throwable->getPrevious();
}
$flattenException = null;
if (class_exists(FlattenException::class)) {
$flattenException = FlattenException::createFromThrowable($throwable);
}
return new self($throwable::class, $throwable->getCode(), $throwable->getMessage(), $flattenException);
}
public function getExceptionClass(): string
{
return $this->exceptionClass;
}
public function getExceptionCode(): int|string
{
return $this->exceptionCode;
}
public function getExceptionMessage(): string
{
return $this->exceptionMessage;
}
public function getFlattenException(): ?FlattenException
{
return $this->flattenException;
}
public function equals(?self $that): bool
{
if (null === $that) {
return false;
}
if ($this->flattenException && $that->flattenException) {
return $this->flattenException->getClass() === $that->flattenException->getClass()
&& $this->flattenException->getCode() === $that->flattenException->getCode()
&& $this->flattenException->getMessage() === $that->flattenException->getMessage();
}
return $this->exceptionClass === $that->exceptionClass
&& $this->exceptionCode === $that->exceptionCode
&& $this->exceptionMessage === $that->exceptionMessage;
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* Marker telling that any batch handlers bound to the envelope should be flushed.
*/
final class FlushBatchHandlersStamp implements NonSendableStampInterface
{
private bool $force;
public function __construct(bool $force)
{
$this->force = $force;
}
public function force(): bool
{
return $this->force;
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\Messenger\Handler\HandlerDescriptor;
/**
* Stamp identifying a message handled by the `HandleMessageMiddleware` middleware
* and storing the handler returned value.
*
* This is used by synchronous command buses expecting a return value and the retry logic
* to only execute handlers that didn't succeed.
*
* @see \Symfony\Component\Messenger\Middleware\HandleMessageMiddleware
* @see \Symfony\Component\Messenger\HandleTrait
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
final class HandledStamp implements StampInterface
{
private mixed $result;
private string $handlerName;
public function __construct(mixed $result, string $handlerName)
{
$this->result = $result;
$this->handlerName = $handlerName;
}
public static function fromDescriptor(HandlerDescriptor $handler, mixed $result): self
{
return new self($result, $handler->getName());
}
public function getResult(): mixed
{
return $this->result;
}
public function getHandlerName(): string
{
return $this->handlerName;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* @author Jáchym Toušek <enumag@gmail.com>
*/
final class HandlerArgumentsStamp implements NonSendableStampInterface
{
public function __construct(
private array $additionalArguments,
) {
}
/**
* @return array
*/
public function getAdditionalArguments()
{
return $this->additionalArguments;
}
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class MessageDecodingFailedStamp implements StampInterface
{
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\Messenger\Handler\HandlerDescriptor;
/**
* Marker telling that ack should not be done automatically for this message.
*/
final class NoAutoAckStamp implements NonSendableStampInterface
{
private HandlerDescriptor $handlerDescriptor;
public function __construct(HandlerDescriptor $handlerDescriptor)
{
$this->handlerDescriptor = $handlerDescriptor;
}
public function getHandlerDescriptor(): HandlerDescriptor
{
return $this->handlerDescriptor;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* A stamp that should not be included with the Envelope if sent to a transport.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
interface NonSendableStampInterface extends StampInterface
{
}

Some files were not shown because too many files have changed in this diff Show More