├── Attribute ├── AsMessage.php └── AsMessageHandler.php ├── CHANGELOG.md ├── Command ├── AbstractFailedMessagesCommand.php ├── ConsumeMessagesCommand.php ├── DebugCommand.php ├── FailedMessagesRemoveCommand.php ├── FailedMessagesRetryCommand.php ├── FailedMessagesShowCommand.php ├── SetupTransportsCommand.php ├── StatsCommand.php └── StopWorkersCommand.php ├── DataCollector └── MessengerDataCollector.php ├── DependencyInjection └── MessengerPass.php ├── Envelope.php ├── Event ├── AbstractWorkerMessageEvent.php ├── SendMessageToTransportsEvent.php ├── WorkerMessageFailedEvent.php ├── WorkerMessageHandledEvent.php ├── WorkerMessageReceivedEvent.php ├── WorkerMessageRetriedEvent.php ├── WorkerMessageSkipEvent.php ├── WorkerRateLimitedEvent.php ├── WorkerRunningEvent.php ├── WorkerStartedEvent.php └── WorkerStoppedEvent.php ├── EventListener ├── AddErrorDetailsStampListener.php ├── DispatchPcntlSignalListener.php ├── ResetMemoryUsageListener.php ├── ResetServicesListener.php ├── SendFailedMessageForRetryListener.php ├── SendFailedMessageToFailureTransportListener.php ├── StopWorkerOnCustomStopExceptionListener.php ├── StopWorkerOnFailureLimitListener.php ├── StopWorkerOnMemoryLimitListener.php ├── StopWorkerOnMessageLimitListener.php ├── StopWorkerOnRestartSignalListener.php └── StopWorkerOnTimeLimitListener.php ├── Exception ├── DelayedMessageHandlingException.php ├── EnvelopeAwareExceptionInterface.php ├── EnvelopeAwareExceptionTrait.php ├── ExceptionInterface.php ├── HandlerFailedException.php ├── InvalidArgumentException.php ├── LogicException.php ├── MessageDecodingFailedException.php ├── NoHandlerForMessageException.php ├── NoSenderForMessageException.php ├── RecoverableExceptionInterface.php ├── RecoverableMessageHandlingException.php ├── RejectRedeliveredMessageException.php ├── RuntimeException.php ├── StopWorkerException.php ├── StopWorkerExceptionInterface.php ├── TransportException.php ├── UnrecoverableExceptionInterface.php ├── UnrecoverableMessageHandlingException.php ├── ValidationFailedException.php ├── WrappedExceptionsInterface.php └── WrappedExceptionsTrait.php ├── HandleTrait.php ├── Handler ├── Acknowledger.php ├── BatchHandlerInterface.php ├── BatchHandlerTrait.php ├── HandlerDescriptor.php ├── HandlersLocator.php ├── HandlersLocatorInterface.php └── RedispatchMessageHandler.php ├── LICENSE ├── Message └── RedispatchMessage.php ├── MessageBus.php ├── MessageBusInterface.php ├── Middleware ├── ActivationMiddleware.php ├── AddBusNameStampMiddleware.php ├── DeduplicateMiddleware.php ├── DispatchAfterCurrentBusMiddleware.php ├── FailedMessageProcessingMiddleware.php ├── HandleMessageMiddleware.php ├── MiddlewareInterface.php ├── RejectRedeliveredMessageMiddleware.php ├── RouterContextMiddleware.php ├── SendMessageMiddleware.php ├── StackInterface.php ├── StackMiddleware.php ├── TraceableMiddleware.php └── ValidationMiddleware.php ├── README.md ├── Retry ├── MultiplierRetryStrategy.php └── RetryStrategyInterface.php ├── RoutableMessageBus.php ├── Stamp ├── AckStamp.php ├── BusNameStamp.php ├── ConsumedByWorkerStamp.php ├── DeduplicateStamp.php ├── DelayStamp.php ├── DispatchAfterCurrentBusStamp.php ├── ErrorDetailsStamp.php ├── FlushBatchHandlersStamp.php ├── HandledStamp.php ├── HandlerArgumentsStamp.php ├── MessageDecodingFailedStamp.php ├── NoAutoAckStamp.php ├── NonSendableStampInterface.php ├── ReceivedStamp.php ├── RedeliveryStamp.php ├── RouterContextStamp.php ├── SentForRetryStamp.php ├── SentStamp.php ├── SentToFailureTransportStamp.php ├── SerializedMessageStamp.php ├── SerializerStamp.php ├── StampInterface.php ├── TransportMessageIdStamp.php ├── TransportNamesStamp.php └── ValidationStamp.php ├── Test └── Middleware │ └── MiddlewareTestCase.php ├── TraceableMessageBus.php ├── Transport ├── CloseableTransportInterface.php ├── InMemory │ ├── InMemoryTransport.php │ └── InMemoryTransportFactory.php ├── Receiver │ ├── KeepaliveReceiverInterface.php │ ├── ListableReceiverInterface.php │ ├── MessageCountAwareInterface.php │ ├── QueueReceiverInterface.php │ ├── ReceiverInterface.php │ └── SingleMessageReceiver.php ├── Sender │ ├── SenderInterface.php │ ├── SendersLocator.php │ └── SendersLocatorInterface.php ├── Serialization │ ├── Normalizer │ │ └── FlattenExceptionNormalizer.php │ ├── PhpSerializer.php │ ├── Serializer.php │ └── SerializerInterface.php ├── SetupableTransportInterface.php ├── Sync │ ├── SyncTransport.php │ └── SyncTransportFactory.php ├── TransportFactory.php ├── TransportFactoryInterface.php └── TransportInterface.php ├── Worker.php ├── WorkerMetadata.php └── composer.json /Attribute/AsMessage.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Attribute; 13 | 14 | /** 15 | * Attribute for configuring message routing. 16 | * 17 | * @author Pierre Rineau pierre.rineau@processus.org> 18 | */ 19 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] 20 | class AsMessage 21 | { 22 | public function __construct( 23 | /** 24 | * Name of the transports to which the message should be routed. 25 | */ 26 | public string|array|null $transport = null, 27 | ) { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Attribute/AsMessageHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Attribute; 13 | 14 | /** 15 | * Service tag to autoconfigure message handlers. 16 | * 17 | * @author Alireza Mirsepassi 18 | */ 19 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] 20 | class AsMessageHandler 21 | { 22 | public function __construct( 23 | /** 24 | * Name of the bus from which this handler can receive messages, by default all buses. 25 | */ 26 | public ?string $bus = null, 27 | 28 | /** 29 | * Name of the transport from which this handler can receive messages, by default all transports. 30 | */ 31 | public ?string $fromTransport = null, 32 | 33 | /** 34 | * Type of messages (FQCN) that can be processed by the handler, only needed if can't be guessed by type-hint. 35 | */ 36 | public ?string $handles = null, 37 | 38 | /** 39 | * Name of the method that will process the message, only if the target is a class. 40 | */ 41 | public ?string $method = null, 42 | 43 | /** 44 | * Priority of this handler when multiple handlers can process the same message. 45 | */ 46 | public int $priority = 0, 47 | ) { 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Command/DebugCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Command; 13 | 14 | use Symfony\Component\Console\Attribute\AsCommand; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Completion\CompletionInput; 17 | use Symfony\Component\Console\Completion\CompletionSuggestions; 18 | use Symfony\Component\Console\Exception\RuntimeException; 19 | use Symfony\Component\Console\Input\InputArgument; 20 | use Symfony\Component\Console\Input\InputInterface; 21 | use Symfony\Component\Console\Output\OutputInterface; 22 | use Symfony\Component\Console\Style\SymfonyStyle; 23 | 24 | /** 25 | * A console command to debug Messenger information. 26 | * 27 | * @author Roland Franssen 28 | */ 29 | #[AsCommand(name: 'debug:messenger', description: 'List messages you can dispatch using the message buses')] 30 | class DebugCommand extends Command 31 | { 32 | public function __construct( 33 | private array $mapping, 34 | ) { 35 | parent::__construct(); 36 | } 37 | 38 | protected function configure(): void 39 | { 40 | $this 41 | ->addArgument('bus', InputArgument::OPTIONAL, \sprintf('The bus id (one of "%s")', implode('", "', array_keys($this->mapping)))) 42 | ->setHelp(<<<'EOF' 43 | The %command.name% command displays all messages that can be 44 | dispatched using the message buses: 45 | 46 | php %command.full_name% 47 | 48 | Or for a specific bus only: 49 | 50 | php %command.full_name% command_bus 51 | 52 | EOF 53 | ) 54 | ; 55 | } 56 | 57 | protected function execute(InputInterface $input, OutputInterface $output): int 58 | { 59 | $io = new SymfonyStyle($input, $output); 60 | $io->title('Messenger'); 61 | 62 | $mapping = $this->mapping; 63 | if ($bus = $input->getArgument('bus')) { 64 | if (!isset($mapping[$bus])) { 65 | throw new RuntimeException(\sprintf('Bus "%s" does not exist. Known buses are "%s".', $bus, implode('", "', array_keys($this->mapping)))); 66 | } 67 | $mapping = [$bus => $mapping[$bus]]; 68 | } 69 | 70 | foreach ($mapping as $bus => $handlersByMessage) { 71 | $io->section($bus); 72 | 73 | $tableRows = []; 74 | foreach ($handlersByMessage as $message => $handlers) { 75 | if ($description = self::getClassDescription($message)) { 76 | $tableRows[] = [\sprintf('%s', $description)]; 77 | } 78 | 79 | $tableRows[] = [\sprintf('%s', $message)]; 80 | foreach ($handlers as $handler) { 81 | $tableRows[] = [ 82 | \sprintf(' handled by %s', $handler[0]).$this->formatConditions($handler[1]), 83 | ]; 84 | if ($handlerDescription = self::getClassDescription($handler[0])) { 85 | $tableRows[] = [\sprintf(' %s', $handlerDescription)]; 86 | } 87 | } 88 | $tableRows[] = ['']; 89 | } 90 | 91 | if ($tableRows) { 92 | $io->text('The following messages can be dispatched:'); 93 | $io->newLine(); 94 | $io->table([], $tableRows); 95 | } else { 96 | $io->warning(\sprintf('No handled message found in bus "%s".', $bus)); 97 | } 98 | } 99 | 100 | return 0; 101 | } 102 | 103 | private function formatConditions(array $options): string 104 | { 105 | if (!$options) { 106 | return ''; 107 | } 108 | 109 | $optionsMapping = []; 110 | foreach ($options as $key => $value) { 111 | $optionsMapping[] = $key.'='.$value; 112 | } 113 | 114 | return ' (when '.implode(', ', $optionsMapping).')'; 115 | } 116 | 117 | private static function getClassDescription(string $class): string 118 | { 119 | try { 120 | $r = new \ReflectionClass($class); 121 | 122 | if ($docComment = $r->getDocComment()) { 123 | $docComment = preg_split('#\n\s*\*\s*[\n@]#', substr($docComment, 3, -2), 2)[0]; 124 | 125 | return trim(preg_replace('#\s*\n\s*\*\s*#', ' ', $docComment)); 126 | } 127 | } catch (\ReflectionException) { 128 | } 129 | 130 | return ''; 131 | } 132 | 133 | public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void 134 | { 135 | if ($input->mustSuggestArgumentValuesFor('bus')) { 136 | $suggestions->suggestValues(array_keys($this->mapping)); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Command/SetupTransportsCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Command; 13 | 14 | use Psr\Container\ContainerInterface; 15 | use Symfony\Component\Console\Attribute\AsCommand; 16 | use Symfony\Component\Console\Command\Command; 17 | use Symfony\Component\Console\Completion\CompletionInput; 18 | use Symfony\Component\Console\Completion\CompletionSuggestions; 19 | use Symfony\Component\Console\Input\InputArgument; 20 | use Symfony\Component\Console\Input\InputInterface; 21 | use Symfony\Component\Console\Output\OutputInterface; 22 | use Symfony\Component\Console\Style\SymfonyStyle; 23 | use Symfony\Component\Messenger\Transport\SetupableTransportInterface; 24 | 25 | /** 26 | * @author Vincent Touzet 27 | */ 28 | #[AsCommand(name: 'messenger:setup-transports', description: 'Prepare the required infrastructure for the transport')] 29 | class SetupTransportsCommand extends Command 30 | { 31 | public function __construct( 32 | private ContainerInterface $transportLocator, 33 | private array $transportNames = [], 34 | ) { 35 | parent::__construct(); 36 | } 37 | 38 | protected function configure(): void 39 | { 40 | $this 41 | ->addArgument('transport', InputArgument::OPTIONAL, 'Name of the transport to setup', null) 42 | ->setHelp(<<%command.name% command setups the transports: 44 | 45 | php %command.full_name% 46 | 47 | Or a specific transport only: 48 | 49 | php %command.full_name% 50 | EOF 51 | ) 52 | ; 53 | } 54 | 55 | protected function execute(InputInterface $input, OutputInterface $output): int 56 | { 57 | $io = new SymfonyStyle($input, $output); 58 | 59 | $transportNames = $this->transportNames; 60 | // do we want to set up only one transport? 61 | if ($transport = $input->getArgument('transport')) { 62 | if (!$this->transportLocator->has($transport)) { 63 | throw new \RuntimeException(\sprintf('The "%s" transport does not exist.', $transport)); 64 | } 65 | $transportNames = [$transport]; 66 | } 67 | 68 | foreach ($transportNames as $id => $transportName) { 69 | $transport = $this->transportLocator->get($transportName); 70 | if (!$transport instanceof SetupableTransportInterface) { 71 | $io->note(\sprintf('The "%s" transport does not support setup.', $transportName)); 72 | continue; 73 | } 74 | 75 | try { 76 | $transport->setup(); 77 | $io->success(\sprintf('The "%s" transport was set up successfully.', $transportName)); 78 | } catch (\Exception $e) { 79 | throw new \RuntimeException(\sprintf('An error occurred while setting up the "%s" transport: ', $transportName).$e->getMessage(), 0, $e); 80 | } 81 | } 82 | 83 | return 0; 84 | } 85 | 86 | public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void 87 | { 88 | if ($input->mustSuggestArgumentValuesFor('transport')) { 89 | $suggestions->suggestValues($this->transportNames); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Command/StatsCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Command; 13 | 14 | use Psr\Container\ContainerInterface; 15 | use Symfony\Component\Console\Attribute\AsCommand; 16 | use Symfony\Component\Console\Command\Command; 17 | use Symfony\Component\Console\Completion\CompletionInput; 18 | use Symfony\Component\Console\Completion\CompletionSuggestions; 19 | use Symfony\Component\Console\Input\InputArgument; 20 | use Symfony\Component\Console\Input\InputInterface; 21 | use Symfony\Component\Console\Input\InputOption; 22 | use Symfony\Component\Console\Output\ConsoleOutputInterface; 23 | use Symfony\Component\Console\Output\OutputInterface; 24 | use Symfony\Component\Console\Style\SymfonyStyle; 25 | use Symfony\Component\Messenger\Exception\InvalidArgumentException; 26 | use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface; 27 | 28 | /** 29 | * @author Kévin Thérage 30 | */ 31 | #[AsCommand(name: 'messenger:stats', description: 'Show the message count for one or more transports')] 32 | class StatsCommand extends Command 33 | { 34 | public function __construct( 35 | private ContainerInterface $transportLocator, 36 | private array $transportNames = [], 37 | ) { 38 | parent::__construct(); 39 | } 40 | 41 | protected function configure(): void 42 | { 43 | $this 44 | ->addArgument('transport_names', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'List of transports\' names') 45 | ->addOption('format', '', InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'txt') 46 | ->setHelp(<<%command.name% command counts the messages for all the transports: 48 | 49 | php %command.full_name% 50 | 51 | Or specific transports only: 52 | 53 | php %command.full_name% 54 | 55 | The --format option specifies the format of the command output: 56 | 57 | php %command.full_name% --format=json 58 | EOF 59 | ) 60 | ; 61 | } 62 | 63 | protected function execute(InputInterface $input, OutputInterface $output): int 64 | { 65 | $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); 66 | 67 | $format = $input->getOption('format'); 68 | if ('text' === $format) { 69 | trigger_deprecation('symfony/messenger', '7.2', 'The "text" format is deprecated, use "txt" instead.'); 70 | 71 | $format = 'txt'; 72 | } 73 | if (!\in_array($format, $this->getAvailableFormatOptions(), true)) { 74 | throw new InvalidArgumentException('Invalid output format.'); 75 | } 76 | 77 | $transportNames = $this->transportNames; 78 | if ($input->getArgument('transport_names')) { 79 | $transportNames = $input->getArgument('transport_names'); 80 | } 81 | 82 | $outputTable = []; 83 | $uncountableTransports = []; 84 | foreach ($transportNames as $transportName) { 85 | if (!$this->transportLocator->has($transportName)) { 86 | if ($this->formatSupportsWarnings($format)) { 87 | $io->warning(\sprintf('The "%s" transport does not exist.', $transportName)); 88 | } 89 | 90 | continue; 91 | } 92 | $transport = $this->transportLocator->get($transportName); 93 | if (!$transport instanceof MessageCountAwareInterface) { 94 | $uncountableTransports[] = $transportName; 95 | 96 | continue; 97 | } 98 | $outputTable[] = [$transportName, $transport->getMessageCount()]; 99 | } 100 | 101 | match ($format) { 102 | 'txt' => $this->outputText($io, $outputTable, $uncountableTransports), 103 | 'json' => $this->outputJson($io, $outputTable, $uncountableTransports), 104 | }; 105 | 106 | return 0; 107 | } 108 | 109 | private function outputText(SymfonyStyle $io, array $outputTable, array $uncountableTransports): void 110 | { 111 | $io->table(['Transport', 'Count'], $outputTable); 112 | 113 | if ($uncountableTransports) { 114 | $io->note(\sprintf('Unable to get message count for the following transports: "%s".', implode('", "', $uncountableTransports))); 115 | } 116 | } 117 | 118 | private function outputJson(SymfonyStyle $io, array $outputTable, array $uncountableTransports): void 119 | { 120 | $output = ['transports' => []]; 121 | foreach ($outputTable as [$transportName, $count]) { 122 | $output['transports'][$transportName] = ['count' => $count]; 123 | } 124 | 125 | if ($uncountableTransports) { 126 | $output['uncountable_transports'] = $uncountableTransports; 127 | } 128 | 129 | $io->writeln(json_encode($output, \JSON_PRETTY_PRINT)); 130 | } 131 | 132 | private function formatSupportsWarnings(string $format): bool 133 | { 134 | return match ($format) { 135 | 'txt' => true, 136 | 'json' => false, 137 | }; 138 | } 139 | 140 | public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void 141 | { 142 | if ($input->mustSuggestOptionValuesFor('format')) { 143 | $suggestions->suggestValues($this->getAvailableFormatOptions()); 144 | } 145 | } 146 | 147 | /** @return string[] */ 148 | private function getAvailableFormatOptions(): array 149 | { 150 | return ['txt', 'json']; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Command/StopWorkersCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Command; 13 | 14 | use Psr\Cache\CacheItemPoolInterface; 15 | use Symfony\Component\Console\Attribute\AsCommand; 16 | use Symfony\Component\Console\Command\Command; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Output\ConsoleOutputInterface; 19 | use Symfony\Component\Console\Output\OutputInterface; 20 | use Symfony\Component\Console\Style\SymfonyStyle; 21 | use Symfony\Component\Messenger\EventListener\StopWorkerOnRestartSignalListener; 22 | 23 | /** 24 | * @author Ryan Weaver 25 | */ 26 | #[AsCommand(name: 'messenger:stop-workers', description: 'Stop workers after their current message')] 27 | class StopWorkersCommand extends Command 28 | { 29 | public function __construct( 30 | private CacheItemPoolInterface $restartSignalCachePool, 31 | ) { 32 | parent::__construct(); 33 | } 34 | 35 | protected function configure(): void 36 | { 37 | $this 38 | ->setDefinition([]) 39 | ->setHelp(<<<'EOF' 40 | The %command.name% command sends a signal to stop any messenger:consume processes that are running. 41 | 42 | php %command.full_name% 43 | 44 | Each worker command will finish the message they are currently processing 45 | and then exit. Worker commands are *not* automatically restarted: that 46 | should be handled by a process control system. 47 | EOF 48 | ) 49 | ; 50 | } 51 | 52 | protected function execute(InputInterface $input, OutputInterface $output): int 53 | { 54 | $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); 55 | 56 | $cacheItem = $this->restartSignalCachePool->getItem(StopWorkerOnRestartSignalListener::RESTART_REQUESTED_TIMESTAMP_KEY); 57 | $cacheItem->set(microtime(true)); 58 | $this->restartSignalCachePool->save($cacheItem); 59 | 60 | $io->success('Signal successfully sent to stop any running workers.'); 61 | 62 | return 0; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /DataCollector/MessengerDataCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\DataCollector; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\Response; 16 | use Symfony\Component\HttpKernel\DataCollector\DataCollector; 17 | use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; 18 | use Symfony\Component\Messenger\TraceableMessageBus; 19 | use Symfony\Component\VarDumper\Caster\ClassStub; 20 | 21 | /** 22 | * @author Samuel Roze 23 | * 24 | * @final 25 | */ 26 | class MessengerDataCollector extends DataCollector implements LateDataCollectorInterface 27 | { 28 | private array $traceableBuses = []; 29 | 30 | public function registerBus(string $name, TraceableMessageBus $bus): void 31 | { 32 | $this->traceableBuses[$name] = $bus; 33 | } 34 | 35 | public function collect(Request $request, Response $response, ?\Throwable $exception = null): void 36 | { 37 | // Noop. Everything is collected live by the traceable buses & cloned as late as possible. 38 | } 39 | 40 | public function lateCollect(): void 41 | { 42 | $this->data = ['messages' => [], 'buses' => array_keys($this->traceableBuses)]; 43 | 44 | $messages = []; 45 | foreach ($this->traceableBuses as $busName => $bus) { 46 | foreach ($bus->getDispatchedMessages() as $message) { 47 | $debugRepresentation = $this->cloneVar($this->collectMessage($busName, $message)); 48 | $messages[] = [$debugRepresentation, $message['callTime']]; 49 | } 50 | } 51 | 52 | // Order by call time 53 | usort($messages, fn ($a, $b) => $a[1] <=> $b[1]); 54 | 55 | // Keep the messages clones only 56 | $this->data['messages'] = array_column($messages, 0); 57 | } 58 | 59 | public function getName(): string 60 | { 61 | return 'messenger'; 62 | } 63 | 64 | public function reset(): void 65 | { 66 | $this->data = []; 67 | foreach ($this->traceableBuses as $traceableBus) { 68 | $traceableBus->reset(); 69 | } 70 | } 71 | 72 | protected function getCasters(): array 73 | { 74 | $casters = parent::getCasters(); 75 | 76 | // Unset the default caster truncating collectors data. 77 | unset($casters['*']); 78 | 79 | return $casters; 80 | } 81 | 82 | private function collectMessage(string $busName, array $tracedMessage): array 83 | { 84 | $message = $tracedMessage['message']; 85 | 86 | $debugRepresentation = [ 87 | 'bus' => $busName, 88 | 'stamps' => $tracedMessage['stamps'] ?? null, 89 | 'stamps_after_dispatch' => $tracedMessage['stamps_after_dispatch'] ?? null, 90 | 'message' => [ 91 | 'type' => new ClassStub($message::class), 92 | 'value' => $message, 93 | ], 94 | 'caller' => $tracedMessage['caller'], 95 | ]; 96 | 97 | if (isset($tracedMessage['exception'])) { 98 | $exception = $tracedMessage['exception']; 99 | 100 | $debugRepresentation['exception'] = [ 101 | 'type' => $exception::class, 102 | 'value' => $exception, 103 | ]; 104 | } 105 | 106 | return $debugRepresentation; 107 | } 108 | 109 | public function getExceptionsCount(?string $bus = null): int 110 | { 111 | $count = 0; 112 | foreach ($this->getMessages($bus) as $message) { 113 | $count += (int) isset($message['exception']); 114 | } 115 | 116 | return $count; 117 | } 118 | 119 | public function getMessages(?string $bus = null): array 120 | { 121 | if (null === $bus) { 122 | return $this->data['messages']; 123 | } 124 | 125 | return array_filter($this->data['messages'], fn ($message) => $bus === $message['bus']); 126 | } 127 | 128 | public function getBuses(): array 129 | { 130 | return $this->data['buses']; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Envelope.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger; 13 | 14 | use Symfony\Component\Messenger\Stamp\StampInterface; 15 | 16 | /** 17 | * A message wrapped in an envelope with stamps (configurations, markers, ...). 18 | * 19 | * @author Maxime Steinhausser 20 | */ 21 | final class Envelope 22 | { 23 | /** 24 | * @var array, list> 25 | */ 26 | private array $stamps = []; 27 | 28 | /** 29 | * @param object|Envelope $message 30 | * @param StampInterface[] $stamps 31 | */ 32 | public function __construct( 33 | private object $message, 34 | array $stamps = [], 35 | ) { 36 | foreach ($stamps as $stamp) { 37 | $this->stamps[$stamp::class][] = $stamp; 38 | } 39 | } 40 | 41 | /** 42 | * Makes sure the message is in an Envelope and adds the given stamps. 43 | * 44 | * @param StampInterface[] $stamps 45 | */ 46 | public static function wrap(object $message, array $stamps = []): self 47 | { 48 | $envelope = $message instanceof self ? $message : new self($message); 49 | 50 | return $envelope->with(...$stamps); 51 | } 52 | 53 | /** 54 | * Adds one or more stamps. 55 | */ 56 | public function with(StampInterface ...$stamps): static 57 | { 58 | $cloned = clone $this; 59 | 60 | foreach ($stamps as $stamp) { 61 | $cloned->stamps[$stamp::class][] = $stamp; 62 | } 63 | 64 | return $cloned; 65 | } 66 | 67 | /** 68 | * Removes all stamps of the given class. 69 | */ 70 | public function withoutAll(string $stampFqcn): static 71 | { 72 | $cloned = clone $this; 73 | 74 | unset($cloned->stamps[$stampFqcn]); 75 | 76 | return $cloned; 77 | } 78 | 79 | /** 80 | * Removes all stamps that implement the given type. 81 | */ 82 | public function withoutStampsOfType(string $type): self 83 | { 84 | $cloned = clone $this; 85 | 86 | foreach ($cloned->stamps as $class => $stamps) { 87 | if ($class === $type || is_subclass_of($class, $type)) { 88 | unset($cloned->stamps[$class]); 89 | } 90 | } 91 | 92 | return $cloned; 93 | } 94 | 95 | /** 96 | * @template TStamp of StampInterface 97 | * 98 | * @param class-string $stampFqcn 99 | * 100 | * @return TStamp|null 101 | */ 102 | public function last(string $stampFqcn): ?StampInterface 103 | { 104 | return isset($this->stamps[$stampFqcn]) ? end($this->stamps[$stampFqcn]) : null; 105 | } 106 | 107 | /** 108 | * @template TStamp of StampInterface 109 | * 110 | * @param class-string|null $stampFqcn 111 | * 112 | * @return StampInterface[]|StampInterface[][] The stamps for the specified FQCN, or all stamps by their class name 113 | * 114 | * @psalm-return ($stampFqcn is null ? array, list> : list) 115 | */ 116 | public function all(?string $stampFqcn = null): array 117 | { 118 | if (null !== $stampFqcn) { 119 | return $this->stamps[$stampFqcn] ?? []; 120 | } 121 | 122 | return $this->stamps; 123 | } 124 | 125 | public function getMessage(): object 126 | { 127 | return $this->message; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Event/AbstractWorkerMessageEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Event; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Stamp\StampInterface; 16 | 17 | abstract class AbstractWorkerMessageEvent 18 | { 19 | public function __construct( 20 | private Envelope $envelope, 21 | private string $receiverName, 22 | ) { 23 | } 24 | 25 | public function getEnvelope(): Envelope 26 | { 27 | return $this->envelope; 28 | } 29 | 30 | /** 31 | * Returns a unique identifier for transport receiver this message was received from. 32 | */ 33 | public function getReceiverName(): string 34 | { 35 | return $this->receiverName; 36 | } 37 | 38 | public function addStamps(StampInterface ...$stamps): void 39 | { 40 | $this->envelope = $this->envelope->with(...$stamps); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Event/SendMessageToTransportsEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Event; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Transport\Sender\SenderInterface; 16 | 17 | /** 18 | * Event is dispatched before a message is sent to the transport. 19 | * 20 | * The event is *only* dispatched if the message will actually 21 | * be sent to at least one transport. If the message is sent 22 | * to multiple transports, the message is dispatched only once. 23 | * This message is only dispatched the first time a message 24 | * is sent to a transport, not also if it is retried. 25 | * 26 | * @author Ryan Weaver 27 | */ 28 | final class SendMessageToTransportsEvent 29 | { 30 | /** 31 | * @param array $senders 32 | */ 33 | public function __construct( 34 | private Envelope $envelope, 35 | private array $senders, 36 | ) { 37 | } 38 | 39 | public function getEnvelope(): Envelope 40 | { 41 | return $this->envelope; 42 | } 43 | 44 | public function setEnvelope(Envelope $envelope): void 45 | { 46 | $this->envelope = $envelope; 47 | } 48 | 49 | /** 50 | * @return array 51 | */ 52 | public function getSenders(): array 53 | { 54 | return $this->senders; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Event/WorkerMessageFailedEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Event; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | /** 17 | * Dispatched when a message was received from a transport and handling failed. 18 | * 19 | * The event name is the class name. 20 | */ 21 | final class WorkerMessageFailedEvent extends AbstractWorkerMessageEvent 22 | { 23 | private \Throwable $throwable; 24 | private bool $willRetry = false; 25 | 26 | public function __construct(Envelope $envelope, string $receiverName, \Throwable $error) 27 | { 28 | $this->throwable = $error; 29 | 30 | parent::__construct($envelope, $receiverName); 31 | } 32 | 33 | public function getThrowable(): \Throwable 34 | { 35 | return $this->throwable; 36 | } 37 | 38 | public function willRetry(): bool 39 | { 40 | return $this->willRetry; 41 | } 42 | 43 | public function setForRetry(): void 44 | { 45 | $this->willRetry = true; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Event/WorkerMessageHandledEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Event; 13 | 14 | /** 15 | * Dispatched after a message was received from a transport and successfully handled. 16 | * 17 | * The event name is the class name. 18 | */ 19 | final class WorkerMessageHandledEvent extends AbstractWorkerMessageEvent 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Event/WorkerMessageReceivedEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Event; 13 | 14 | /** 15 | * Dispatched when a message was received from a transport but before sent to the bus. 16 | * 17 | * The event name is the class name. 18 | */ 19 | final class WorkerMessageReceivedEvent extends AbstractWorkerMessageEvent 20 | { 21 | private bool $shouldHandle = true; 22 | 23 | public function shouldHandle(?bool $shouldHandle = null): bool 24 | { 25 | if (null !== $shouldHandle) { 26 | $this->shouldHandle = $shouldHandle; 27 | } 28 | 29 | return $this->shouldHandle; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Event/WorkerMessageRetriedEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Event; 13 | 14 | /** 15 | * Dispatched after a message has been sent for retry. 16 | * 17 | * The event name is the class name. 18 | */ 19 | final class WorkerMessageRetriedEvent extends AbstractWorkerMessageEvent 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Event/WorkerMessageSkipEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Event; 13 | 14 | /** 15 | * Dispatched when a message was skip. 16 | * 17 | * The event name is the class name. 18 | */ 19 | final class WorkerMessageSkipEvent extends AbstractWorkerMessageEvent 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Event/WorkerRateLimitedEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Event; 13 | 14 | use Symfony\Component\RateLimiter\LimiterInterface; 15 | 16 | /** 17 | * Dispatched after the worker has been blocked due to a configured rate limiter. 18 | * Can be used to reset the rate limiter. 19 | * 20 | * @author Bob van de Vijver 21 | */ 22 | final class WorkerRateLimitedEvent 23 | { 24 | public function __construct(private LimiterInterface $limiter, private string $transportName) 25 | { 26 | } 27 | 28 | public function getLimiter(): LimiterInterface 29 | { 30 | return $this->limiter; 31 | } 32 | 33 | public function getTransportName(): string 34 | { 35 | return $this->transportName; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Event/WorkerRunningEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Event; 13 | 14 | use Symfony\Component\Messenger\Worker; 15 | 16 | /** 17 | * Dispatched after the worker processed a message or didn't receive a message at all. 18 | * 19 | * @author Tobias Schultze 20 | */ 21 | final class WorkerRunningEvent 22 | { 23 | public function __construct( 24 | private Worker $worker, 25 | private bool $isWorkerIdle, 26 | ) { 27 | } 28 | 29 | public function getWorker(): Worker 30 | { 31 | return $this->worker; 32 | } 33 | 34 | /** 35 | * Returns true when no message has been received by the worker. 36 | */ 37 | public function isWorkerIdle(): bool 38 | { 39 | return $this->isWorkerIdle; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Event/WorkerStartedEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Event; 13 | 14 | use Symfony\Component\Messenger\Worker; 15 | 16 | /** 17 | * Dispatched when a worker has been started. 18 | * 19 | * @author Tobias Schultze 20 | */ 21 | final class WorkerStartedEvent 22 | { 23 | public function __construct( 24 | private Worker $worker, 25 | ) { 26 | } 27 | 28 | public function getWorker(): Worker 29 | { 30 | return $this->worker; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Event/WorkerStoppedEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Event; 13 | 14 | use Symfony\Component\Messenger\Worker; 15 | 16 | /** 17 | * Dispatched when a worker has been stopped. 18 | * 19 | * @author Robin Chalas 20 | */ 21 | final class WorkerStoppedEvent 22 | { 23 | public function __construct( 24 | private Worker $worker, 25 | ) { 26 | } 27 | 28 | public function getWorker(): Worker 29 | { 30 | return $this->worker; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /EventListener/AddErrorDetailsStampListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\EventListener; 13 | 14 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 15 | use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; 16 | use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp; 17 | 18 | final class AddErrorDetailsStampListener implements EventSubscriberInterface 19 | { 20 | public function onMessageFailed(WorkerMessageFailedEvent $event): void 21 | { 22 | $stamp = ErrorDetailsStamp::create($event->getThrowable()); 23 | $previousStamp = $event->getEnvelope()->last(ErrorDetailsStamp::class); 24 | 25 | // Do not append duplicate information 26 | if (null === $previousStamp || !$previousStamp->equals($stamp)) { 27 | $event->addStamps($stamp); 28 | } 29 | } 30 | 31 | public static function getSubscribedEvents(): array 32 | { 33 | return [ 34 | // must have higher priority than SendFailedMessageForRetryListener 35 | WorkerMessageFailedEvent::class => ['onMessageFailed', 200], 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /EventListener/DispatchPcntlSignalListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\EventListener; 13 | 14 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 15 | use Symfony\Component\Messenger\Event\WorkerRunningEvent; 16 | 17 | /** 18 | * @author Tobias Schultze 19 | */ 20 | class DispatchPcntlSignalListener implements EventSubscriberInterface 21 | { 22 | public function onWorkerRunning(): void 23 | { 24 | if (!\function_exists('pcntl_signal_dispatch')) { 25 | return; 26 | } 27 | 28 | pcntl_signal_dispatch(); 29 | } 30 | 31 | public static function getSubscribedEvents(): array 32 | { 33 | if (!\function_exists('pcntl_signal_dispatch')) { 34 | return []; 35 | } 36 | 37 | return [ 38 | WorkerRunningEvent::class => ['onWorkerRunning', 100], 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /EventListener/ResetMemoryUsageListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\EventListener; 13 | 14 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 15 | use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; 16 | use Symfony\Component\Messenger\Event\WorkerRunningEvent; 17 | 18 | /** 19 | * @author Tim Düsterhus 20 | */ 21 | final class ResetMemoryUsageListener implements EventSubscriberInterface 22 | { 23 | private bool $collect = false; 24 | 25 | public function resetBefore(WorkerMessageReceivedEvent $event): void 26 | { 27 | // Reset the peak memory usage for accurate measurement of the 28 | // memory usage on a per-message basis. 29 | memory_reset_peak_usage(); 30 | $this->collect = true; 31 | } 32 | 33 | public function collectAfter(WorkerRunningEvent $event): void 34 | { 35 | if ($event->isWorkerIdle() && $this->collect) { 36 | gc_collect_cycles(); 37 | $this->collect = false; 38 | } 39 | } 40 | 41 | public static function getSubscribedEvents(): array 42 | { 43 | return [ 44 | WorkerMessageReceivedEvent::class => ['resetBefore', -1024], 45 | WorkerRunningEvent::class => ['collectAfter', -1024], 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /EventListener/ResetServicesListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\EventListener; 13 | 14 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 15 | use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; 16 | use Symfony\Component\Messenger\Event\WorkerRunningEvent; 17 | use Symfony\Component\Messenger\Event\WorkerStoppedEvent; 18 | 19 | /** 20 | * @author Grégoire Pineau 21 | */ 22 | class ResetServicesListener implements EventSubscriberInterface 23 | { 24 | public function __construct( 25 | private ServicesResetter $servicesResetter, 26 | ) { 27 | } 28 | 29 | public function resetServices(WorkerRunningEvent $event): void 30 | { 31 | if (!$event->isWorkerIdle()) { 32 | $this->servicesResetter->reset(); 33 | } 34 | } 35 | 36 | public function resetServicesAtStop(WorkerStoppedEvent $event): void 37 | { 38 | $this->servicesResetter->reset(); 39 | } 40 | 41 | public static function getSubscribedEvents(): array 42 | { 43 | return [ 44 | WorkerRunningEvent::class => ['resetServices', -1024], 45 | WorkerStoppedEvent::class => ['resetServicesAtStop', -1024], 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /EventListener/SendFailedMessageToFailureTransportListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\EventListener; 13 | 14 | use Psr\Container\ContainerInterface; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 17 | use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; 18 | use Symfony\Component\Messenger\Event\WorkerMessageSkipEvent; 19 | use Symfony\Component\Messenger\Stamp\DelayStamp; 20 | use Symfony\Component\Messenger\Stamp\RedeliveryStamp; 21 | use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp; 22 | 23 | /** 24 | * Sends a rejected message to a "failure transport". 25 | * 26 | * @author Ryan Weaver 27 | */ 28 | class SendFailedMessageToFailureTransportListener implements EventSubscriberInterface 29 | { 30 | public function __construct( 31 | private ContainerInterface $failureSenders, 32 | private ?LoggerInterface $logger = null, 33 | ) { 34 | } 35 | 36 | public function onMessageFailed(WorkerMessageFailedEvent $event): void 37 | { 38 | if ($event->willRetry()) { 39 | return; 40 | } 41 | 42 | if (!$this->failureSenders->has($event->getReceiverName())) { 43 | return; 44 | } 45 | 46 | $failureSender = $this->failureSenders->get($event->getReceiverName()); 47 | 48 | $envelope = $event->getEnvelope(); 49 | 50 | // avoid re-sending to the failed sender 51 | if (null !== $envelope->last(SentToFailureTransportStamp::class)) { 52 | return; 53 | } 54 | 55 | $envelope = $envelope->with( 56 | new SentToFailureTransportStamp($event->getReceiverName()), 57 | new DelayStamp(0), 58 | new RedeliveryStamp(0) 59 | ); 60 | 61 | $this->logger?->info('Rejected message {class} will be sent to the failure transport {transport}.', [ 62 | 'class' => $envelope->getMessage()::class, 63 | 'transport' => $failureSender::class, 64 | ]); 65 | 66 | $failureSender->send($envelope); 67 | } 68 | 69 | public function onMessageSkip(WorkerMessageSkipEvent $event): void 70 | { 71 | if (!$this->failureSenders->has($event->getReceiverName())) { 72 | return; 73 | } 74 | 75 | $failureSender = $this->failureSenders->get($event->getReceiverName()); 76 | $envelope = $event->getEnvelope()->with( 77 | new SentToFailureTransportStamp($event->getReceiverName()), 78 | new DelayStamp(0), 79 | ); 80 | 81 | $failureSender->send($envelope); 82 | } 83 | 84 | public static function getSubscribedEvents(): array 85 | { 86 | return [ 87 | WorkerMessageFailedEvent::class => ['onMessageFailed', -100], 88 | WorkerMessageSkipEvent::class => ['onMessageSkip', -100], 89 | ]; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /EventListener/StopWorkerOnCustomStopExceptionListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\EventListener; 13 | 14 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 15 | use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; 16 | use Symfony\Component\Messenger\Event\WorkerRunningEvent; 17 | use Symfony\Component\Messenger\Exception\HandlerFailedException; 18 | use Symfony\Component\Messenger\Exception\StopWorkerExceptionInterface; 19 | 20 | /** 21 | * @author Grégoire Pineau 22 | */ 23 | class StopWorkerOnCustomStopExceptionListener implements EventSubscriberInterface 24 | { 25 | private bool $stop = false; 26 | 27 | public function onMessageFailed(WorkerMessageFailedEvent $event): void 28 | { 29 | $th = $event->getThrowable(); 30 | if ($th instanceof StopWorkerExceptionInterface) { 31 | $this->stop = true; 32 | } 33 | if ($th instanceof HandlerFailedException) { 34 | foreach ($th->getWrappedExceptions() as $e) { 35 | if ($e instanceof StopWorkerExceptionInterface) { 36 | $this->stop = true; 37 | break; 38 | } 39 | } 40 | } 41 | } 42 | 43 | public function onWorkerRunning(WorkerRunningEvent $event): void 44 | { 45 | if ($this->stop) { 46 | $event->getWorker()->stop(); 47 | } 48 | } 49 | 50 | public static function getSubscribedEvents(): array 51 | { 52 | return [ 53 | WorkerMessageFailedEvent::class => 'onMessageFailed', 54 | WorkerRunningEvent::class => 'onWorkerRunning', 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /EventListener/StopWorkerOnFailureLimitListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\EventListener; 13 | 14 | use Psr\Log\LoggerInterface; 15 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 16 | use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; 17 | use Symfony\Component\Messenger\Event\WorkerRunningEvent; 18 | use Symfony\Component\Messenger\Exception\InvalidArgumentException; 19 | 20 | /** 21 | * @author Michel Hunziker 22 | */ 23 | class StopWorkerOnFailureLimitListener implements EventSubscriberInterface 24 | { 25 | private int $failedMessages = 0; 26 | 27 | public function __construct( 28 | private int $maximumNumberOfFailures, 29 | private ?LoggerInterface $logger = null, 30 | ) { 31 | if ($maximumNumberOfFailures <= 0) { 32 | throw new InvalidArgumentException('Failure limit must be greater than zero.'); 33 | } 34 | } 35 | 36 | public function onMessageFailed(WorkerMessageFailedEvent $event): void 37 | { 38 | ++$this->failedMessages; 39 | } 40 | 41 | public function onWorkerRunning(WorkerRunningEvent $event): void 42 | { 43 | if (!$event->isWorkerIdle() && $this->failedMessages >= $this->maximumNumberOfFailures) { 44 | $this->failedMessages = 0; 45 | $event->getWorker()->stop(); 46 | 47 | $this->logger?->info('Worker stopped due to limit of {count} failed message(s) is reached', ['count' => $this->maximumNumberOfFailures]); 48 | } 49 | } 50 | 51 | public static function getSubscribedEvents(): array 52 | { 53 | return [ 54 | WorkerMessageFailedEvent::class => 'onMessageFailed', 55 | WorkerRunningEvent::class => 'onWorkerRunning', 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /EventListener/StopWorkerOnMemoryLimitListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\EventListener; 13 | 14 | use Psr\Log\LoggerInterface; 15 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 16 | use Symfony\Component\Messenger\Event\WorkerRunningEvent; 17 | 18 | /** 19 | * @author Simon Delicata 20 | * @author Tobias Schultze 21 | */ 22 | class StopWorkerOnMemoryLimitListener implements EventSubscriberInterface 23 | { 24 | private \Closure $memoryResolver; 25 | 26 | public function __construct( 27 | private int $memoryLimit, 28 | private ?LoggerInterface $logger = null, 29 | ?callable $memoryResolver = null, 30 | ) { 31 | $memoryResolver ??= static fn () => memory_get_usage(true); 32 | $this->memoryResolver = $memoryResolver(...); 33 | } 34 | 35 | public function onWorkerRunning(WorkerRunningEvent $event): void 36 | { 37 | $memoryResolver = $this->memoryResolver; 38 | $usedMemory = $memoryResolver(); 39 | if ($usedMemory > $this->memoryLimit) { 40 | $event->getWorker()->stop(); 41 | $this->logger?->info('Worker stopped due to memory limit of {limit} bytes exceeded ({memory} bytes used)', ['limit' => $this->memoryLimit, 'memory' => $usedMemory]); 42 | } 43 | } 44 | 45 | public static function getSubscribedEvents(): array 46 | { 47 | return [ 48 | WorkerRunningEvent::class => 'onWorkerRunning', 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /EventListener/StopWorkerOnMessageLimitListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\EventListener; 13 | 14 | use Psr\Log\LoggerInterface; 15 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 16 | use Symfony\Component\Messenger\Event\WorkerRunningEvent; 17 | use Symfony\Component\Messenger\Exception\InvalidArgumentException; 18 | 19 | /** 20 | * @author Samuel Roze 21 | * @author Tobias Schultze 22 | */ 23 | class StopWorkerOnMessageLimitListener implements EventSubscriberInterface 24 | { 25 | private int $receivedMessages = 0; 26 | 27 | public function __construct( 28 | private int $maximumNumberOfMessages, 29 | private ?LoggerInterface $logger = null, 30 | ) { 31 | if ($maximumNumberOfMessages <= 0) { 32 | throw new InvalidArgumentException('Message limit must be greater than zero.'); 33 | } 34 | } 35 | 36 | public function onWorkerRunning(WorkerRunningEvent $event): void 37 | { 38 | if (!$event->isWorkerIdle() && ++$this->receivedMessages >= $this->maximumNumberOfMessages) { 39 | $this->receivedMessages = 0; 40 | $event->getWorker()->stop(); 41 | 42 | $this->logger?->info('Worker stopped due to maximum count of {count} messages processed', ['count' => $this->maximumNumberOfMessages]); 43 | } 44 | } 45 | 46 | public static function getSubscribedEvents(): array 47 | { 48 | return [ 49 | WorkerRunningEvent::class => 'onWorkerRunning', 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /EventListener/StopWorkerOnRestartSignalListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\EventListener; 13 | 14 | use Psr\Cache\CacheItemPoolInterface; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 17 | use Symfony\Component\Messenger\Event\WorkerRunningEvent; 18 | use Symfony\Component\Messenger\Event\WorkerStartedEvent; 19 | 20 | /** 21 | * @author Ryan Weaver 22 | */ 23 | class StopWorkerOnRestartSignalListener implements EventSubscriberInterface 24 | { 25 | public const RESTART_REQUESTED_TIMESTAMP_KEY = 'workers.restart_requested_timestamp'; 26 | 27 | private float $workerStartedAt = 0; 28 | 29 | public function __construct( 30 | private CacheItemPoolInterface $cachePool, 31 | private ?LoggerInterface $logger = null, 32 | ) { 33 | } 34 | 35 | public function onWorkerStarted(): void 36 | { 37 | $this->workerStartedAt = microtime(true); 38 | } 39 | 40 | public function onWorkerRunning(WorkerRunningEvent $event): void 41 | { 42 | if ($this->shouldRestart()) { 43 | $event->getWorker()->stop(); 44 | $this->logger?->info('Worker stopped because a restart was requested.'); 45 | } 46 | } 47 | 48 | public static function getSubscribedEvents(): array 49 | { 50 | return [ 51 | WorkerStartedEvent::class => 'onWorkerStarted', 52 | WorkerRunningEvent::class => 'onWorkerRunning', 53 | ]; 54 | } 55 | 56 | private function shouldRestart(): bool 57 | { 58 | $cacheItem = $this->cachePool->getItem(self::RESTART_REQUESTED_TIMESTAMP_KEY); 59 | 60 | if (!$cacheItem->isHit()) { 61 | // no restart has ever been scheduled 62 | return false; 63 | } 64 | 65 | return $this->workerStartedAt < $cacheItem->get(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /EventListener/StopWorkerOnTimeLimitListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\EventListener; 13 | 14 | use Psr\Log\LoggerInterface; 15 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 16 | use Symfony\Component\Messenger\Event\WorkerRunningEvent; 17 | use Symfony\Component\Messenger\Event\WorkerStartedEvent; 18 | use Symfony\Component\Messenger\Exception\InvalidArgumentException; 19 | 20 | /** 21 | * @author Simon Delicata 22 | * @author Tobias Schultze 23 | */ 24 | class StopWorkerOnTimeLimitListener implements EventSubscriberInterface 25 | { 26 | private float $endTime = 0; 27 | 28 | public function __construct( 29 | private int $timeLimitInSeconds, 30 | private ?LoggerInterface $logger = null, 31 | ) { 32 | if ($timeLimitInSeconds <= 0) { 33 | throw new InvalidArgumentException('Time limit must be greater than zero.'); 34 | } 35 | } 36 | 37 | public function onWorkerStarted(): void 38 | { 39 | $startTime = microtime(true); 40 | $this->endTime = $startTime + $this->timeLimitInSeconds; 41 | } 42 | 43 | public function onWorkerRunning(WorkerRunningEvent $event): void 44 | { 45 | if ($this->endTime < microtime(true)) { 46 | $event->getWorker()->stop(); 47 | $this->logger?->info('Worker stopped due to time limit of {timeLimit}s exceeded', ['timeLimit' => $this->timeLimitInSeconds]); 48 | } 49 | } 50 | 51 | public static function getSubscribedEvents(): array 52 | { 53 | return [ 54 | WorkerStartedEvent::class => 'onWorkerStarted', 55 | WorkerRunningEvent::class => 'onWorkerRunning', 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Exception/DelayedMessageHandlingException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | /** 17 | * When handling queued messages from {@link DispatchAfterCurrentBusMiddleware}, 18 | * some handlers caused an exception. This exception contains all those handler exceptions. 19 | * 20 | * @author Tobias Nyholm 21 | */ 22 | class DelayedMessageHandlingException extends RuntimeException implements WrappedExceptionsInterface, EnvelopeAwareExceptionInterface 23 | { 24 | use EnvelopeAwareExceptionTrait; 25 | use WrappedExceptionsTrait; 26 | 27 | public function __construct( 28 | private array $exceptions, 29 | ?Envelope $envelope = null, 30 | ) { 31 | $this->envelope = $envelope; 32 | 33 | $exceptionMessages = implode(", \n", array_map( 34 | fn (\Throwable $e) => $e::class.': '.$e->getMessage(), 35 | $exceptions 36 | )); 37 | 38 | if (1 === \count($exceptions)) { 39 | $message = \sprintf("A delayed message handler threw an exception: \n\n%s", $exceptionMessages); 40 | } else { 41 | $message = \sprintf("Some delayed message handlers threw an exception: \n\n%s", $exceptionMessages); 42 | } 43 | 44 | parent::__construct($message, 0, $exceptions[array_key_first($exceptions)]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Exception/EnvelopeAwareExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | interface EnvelopeAwareExceptionInterface 17 | { 18 | public function getEnvelope(): ?Envelope; 19 | } 20 | -------------------------------------------------------------------------------- /Exception/EnvelopeAwareExceptionTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | /** 17 | * @internal 18 | */ 19 | trait EnvelopeAwareExceptionTrait 20 | { 21 | private ?Envelope $envelope = null; 22 | 23 | public function getEnvelope(): ?Envelope 24 | { 25 | return $this->envelope; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * Base Messenger component's exception. 16 | * 17 | * @author Samuel Roze 18 | */ 19 | interface ExceptionInterface extends \Throwable 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/HandlerFailedException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | class HandlerFailedException extends RuntimeException implements WrappedExceptionsInterface, EnvelopeAwareExceptionInterface 17 | { 18 | use WrappedExceptionsTrait; 19 | 20 | /** 21 | * @param \Throwable[] $exceptions The name of the handler should be given as key 22 | */ 23 | public function __construct( 24 | private Envelope $envelope, 25 | array $exceptions, 26 | ) { 27 | $firstFailure = current($exceptions); 28 | 29 | $message = \sprintf('Handling "%s" failed: ', $envelope->getMessage()::class); 30 | 31 | parent::__construct( 32 | $message.(1 === \count($exceptions) 33 | ? $firstFailure->getMessage() 34 | : \sprintf('%d handlers failed. First failure is: %s', \count($exceptions), $firstFailure->getMessage()) 35 | ), 36 | (int) $firstFailure->getCode(), 37 | $firstFailure 38 | ); 39 | 40 | $this->exceptions = $exceptions; 41 | } 42 | 43 | public function getEnvelope(): Envelope 44 | { 45 | return $this->envelope; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * @author Yonel Ceruto 16 | */ 17 | class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * @author Roland Franssen 16 | */ 17 | class LogicException extends \LogicException implements ExceptionInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/MessageDecodingFailedException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * Thrown when a message cannot be decoded in a serializer. 16 | */ 17 | class MessageDecodingFailedException extends InvalidArgumentException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/NoHandlerForMessageException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * @author Samuel Roze 16 | */ 17 | class NoHandlerForMessageException extends LogicException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/NoSenderForMessageException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * @author Jérémy Reynaud 16 | */ 17 | class NoSenderForMessageException extends LogicException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/RecoverableExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * Marker interface for exceptions to indicate that handling a message should have worked. 16 | * 17 | * If something goes wrong while handling a message that's received from a transport 18 | * and the message should be retried, a handler can throw such an exception. 19 | * 20 | * @author Jérémy Derussé 21 | * 22 | * @method int|null getRetryDelay() The time to wait in milliseconds 23 | */ 24 | interface RecoverableExceptionInterface extends \Throwable 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /Exception/RecoverableMessageHandlingException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * A concrete implementation of RecoverableExceptionInterface that can be used directly. 16 | * 17 | * @author Frederic Bouchery 18 | */ 19 | class RecoverableMessageHandlingException extends RuntimeException implements RecoverableExceptionInterface 20 | { 21 | public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, private readonly ?int $retryDelay = null) 22 | { 23 | parent::__construct($message, $code, $previous); 24 | } 25 | 26 | public function getRetryDelay(): ?int 27 | { 28 | return $this->retryDelay; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Exception/RejectRedeliveredMessageException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * @author Tobias Schultze 16 | */ 17 | class RejectRedeliveredMessageException extends RuntimeException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * @author Fabien Potencier 16 | */ 17 | class RuntimeException extends \RuntimeException implements ExceptionInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/StopWorkerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * @author Grégoire Pineau 16 | */ 17 | class StopWorkerException extends RuntimeException implements StopWorkerExceptionInterface 18 | { 19 | public function __construct(string $message = 'Worker should stop.', ?\Throwable $previous = null) 20 | { 21 | parent::__construct($message, 0, $previous); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Exception/StopWorkerExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * @author Grégoire Pineau 16 | */ 17 | interface StopWorkerExceptionInterface extends \Throwable 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/TransportException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * @author Eric Masoero 16 | */ 17 | class TransportException extends RuntimeException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/UnrecoverableExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * Marker interface for exceptions to indicate that handling a message will continue to fail. 16 | * 17 | * If something goes wrong while handling a message that's received from a transport 18 | * and the message should not be retried, a handler can throw such an exception. 19 | * 20 | * @author Tobias Schultze 21 | */ 22 | interface UnrecoverableExceptionInterface extends \Throwable 23 | { 24 | } 25 | -------------------------------------------------------------------------------- /Exception/UnrecoverableMessageHandlingException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * A concrete implementation of UnrecoverableExceptionInterface that can be used directly. 16 | * 17 | * @author Frederic Bouchery 18 | */ 19 | class UnrecoverableMessageHandlingException extends RuntimeException implements UnrecoverableExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/ValidationFailedException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Validator\ConstraintViolationListInterface; 16 | 17 | /** 18 | * @author Tobias Nyholm 19 | */ 20 | class ValidationFailedException extends RuntimeException implements EnvelopeAwareExceptionInterface 21 | { 22 | use EnvelopeAwareExceptionTrait; 23 | 24 | public function __construct( 25 | private object $violatingMessage, 26 | private ConstraintViolationListInterface $violations, 27 | ?Envelope $envelope = null, 28 | ) { 29 | $this->envelope = $envelope; 30 | 31 | parent::__construct(\sprintf('Message of type "%s" failed validation.', $this->violatingMessage::class)); 32 | } 33 | 34 | public function getViolatingMessage(): object 35 | { 36 | return $this->violatingMessage; 37 | } 38 | 39 | public function getViolations(): ConstraintViolationListInterface 40 | { 41 | return $this->violations; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Exception/WrappedExceptionsInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * Exception that holds multiple exceptions thrown by one or more handlers and/or messages. 16 | * 17 | * @author Jeroen 18 | */ 19 | interface WrappedExceptionsInterface extends \Throwable 20 | { 21 | /** 22 | * @template TException of \Throwable 23 | * 24 | * @param class-string|null $class 25 | * 26 | * @return \Throwable[] 27 | * 28 | * @psalm-return ($class is null ? \Throwable[] : TException[]) 29 | */ 30 | public function getWrappedExceptions(?string $class = null, bool $recursive = false): array; 31 | } 32 | -------------------------------------------------------------------------------- /Exception/WrappedExceptionsTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Exception; 13 | 14 | /** 15 | * @author Jeroen 16 | * 17 | * @internal 18 | */ 19 | trait WrappedExceptionsTrait 20 | { 21 | private array $exceptions; 22 | 23 | /** 24 | * @template TException of \Throwable 25 | * 26 | * @param class-string|null $class 27 | * 28 | * @return \Throwable[] 29 | * 30 | * @psalm-return ($class is null ? \Throwable[] : TException[]) 31 | */ 32 | public function getWrappedExceptions(?string $class = null, bool $recursive = false): array 33 | { 34 | return $this->getWrappedExceptionsRecursively($class, $recursive, $this->exceptions); 35 | } 36 | 37 | /** 38 | * @param class-string<\Throwable>|null $class 39 | * @param iterable<\Throwable> $exceptions 40 | * 41 | * @return \Throwable[] 42 | */ 43 | private function getWrappedExceptionsRecursively(?string $class, bool $recursive, iterable $exceptions): array 44 | { 45 | $unwrapped = []; 46 | foreach ($exceptions as $key => $exception) { 47 | if ($recursive && $exception instanceof WrappedExceptionsInterface) { 48 | $unwrapped[] = $this->getWrappedExceptionsRecursively($class, $recursive, $exception->getWrappedExceptions()); 49 | 50 | continue; 51 | } 52 | 53 | if ($class && !is_a($exception, $class)) { 54 | continue; 55 | } 56 | 57 | $unwrapped[] = [$key => $exception]; 58 | } 59 | 60 | return array_merge(...$unwrapped); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /HandleTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger; 13 | 14 | use Symfony\Component\Messenger\Exception\LogicException; 15 | use Symfony\Component\Messenger\Stamp\HandledStamp; 16 | use Symfony\Component\Messenger\Stamp\StampInterface; 17 | 18 | /** 19 | * Leverages a message bus to expect a single, synchronous message handling and return its result. 20 | * 21 | * @author Maxime Steinhausser 22 | */ 23 | trait HandleTrait 24 | { 25 | private MessageBusInterface $messageBus; 26 | 27 | /** 28 | * Dispatches the given message, expecting to be handled by a single handler 29 | * and returns the result from the handler returned value. 30 | * This behavior is useful for both synchronous command & query buses, 31 | * the last one usually returning the handler result. 32 | * 33 | * @param object|Envelope $message The message or the message pre-wrapped in an envelope 34 | * @param StampInterface[] $stamps Stamps to be set on the Envelope which are used to control middleware behavior 35 | */ 36 | private function handle(object $message, array $stamps = []): mixed 37 | { 38 | if (!isset($this->messageBus)) { 39 | 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)); 40 | } 41 | 42 | $envelope = $this->messageBus->dispatch($message, $stamps); 43 | /** @var HandledStamp[] $handledStamps */ 44 | $handledStamps = $envelope->all(HandledStamp::class); 45 | 46 | if (!$handledStamps) { 47 | 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__)); 48 | } 49 | 50 | if (\count($handledStamps) > 1) { 51 | $handlers = implode(', ', array_map(fn (HandledStamp $stamp): string => \sprintf('"%s"', $stamp->getHandlerName()), $handledStamps)); 52 | 53 | 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)); 54 | } 55 | 56 | return $handledStamps[0]->getResult(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Handler/Acknowledger.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Handler; 13 | 14 | use Symfony\Component\Messenger\Exception\LogicException; 15 | 16 | /** 17 | * @author Nicolas Grekas 18 | */ 19 | class Acknowledger 20 | { 21 | private ?\Closure $ack; 22 | private ?\Throwable $error = null; 23 | private mixed $result = null; 24 | 25 | /** 26 | * @param \Closure(\Throwable|null, mixed):void|null $ack 27 | */ 28 | public function __construct( 29 | private string $handlerClass, 30 | ?\Closure $ack = null, 31 | ) { 32 | $this->ack = $ack ?? static function () {}; 33 | } 34 | 35 | /** 36 | * @param mixed $result 37 | */ 38 | public function ack($result = null): void 39 | { 40 | $this->doAck(null, $result); 41 | } 42 | 43 | public function nack(\Throwable $error): void 44 | { 45 | $this->doAck($error); 46 | } 47 | 48 | public function getError(): ?\Throwable 49 | { 50 | return $this->error; 51 | } 52 | 53 | public function getResult(): mixed 54 | { 55 | return $this->result; 56 | } 57 | 58 | public function isAcknowledged(): bool 59 | { 60 | return null === $this->ack; 61 | } 62 | 63 | public function __destruct() 64 | { 65 | if (null !== $this->ack) { 66 | throw new LogicException(\sprintf('The acknowledger was not called by the "%s" batch handler.', $this->handlerClass)); 67 | } 68 | } 69 | 70 | private function doAck(?\Throwable $e = null, mixed $result = null): void 71 | { 72 | if (!$ack = $this->ack) { 73 | throw new LogicException(\sprintf('The acknowledger cannot be called twice by the "%s" batch handler.', $this->handlerClass)); 74 | } 75 | $this->ack = null; 76 | $this->error = $e; 77 | $this->result = $result; 78 | $ack($e, $result); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Handler/BatchHandlerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Handler; 13 | 14 | /** 15 | * @author Nicolas Grekas 16 | */ 17 | interface BatchHandlerInterface 18 | { 19 | /** 20 | * @param Acknowledger|null $ack The function to call to ack/nack the $message. 21 | * The message should be handled synchronously when null. 22 | * 23 | * @return mixed The number of pending messages in the batch if $ack is not null, 24 | * the result from handling the message otherwise 25 | */ 26 | // public function __invoke(object $message, ?Acknowledger $ack = null): mixed; 27 | 28 | /** 29 | * Flushes any pending buffers. 30 | * 31 | * @param bool $force Whether flushing is required; it can be skipped if not 32 | */ 33 | public function flush(bool $force): void; 34 | } 35 | -------------------------------------------------------------------------------- /Handler/BatchHandlerTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Handler; 13 | 14 | /** 15 | * @author Nicolas Grekas 16 | */ 17 | trait BatchHandlerTrait 18 | { 19 | private array $jobs = []; 20 | 21 | public function flush(bool $force): void 22 | { 23 | if ($jobs = $this->jobs) { 24 | $this->jobs = []; 25 | $this->process($jobs); 26 | } 27 | } 28 | 29 | /** 30 | * @param Acknowledger|null $ack The function to call to ack/nack the $message. 31 | * The message should be handled synchronously when null. 32 | * 33 | * @return mixed The number of pending messages in the batch if $ack is not null, 34 | * the result from handling the message otherwise 35 | */ 36 | private function handle(object $message, ?Acknowledger $ack): mixed 37 | { 38 | if (null === $ack) { 39 | $ack = new Acknowledger(get_debug_type($this)); 40 | $this->jobs[] = [$message, $ack]; 41 | $this->flush(true); 42 | 43 | return $ack->getResult(); 44 | } 45 | 46 | $this->jobs[] = [$message, $ack]; 47 | if (!$this->shouldFlush()) { 48 | return \count($this->jobs); 49 | } 50 | 51 | $this->flush(true); 52 | 53 | return 0; 54 | } 55 | 56 | private function shouldFlush(): bool 57 | { 58 | return $this->getBatchSize() <= \count($this->jobs); 59 | } 60 | 61 | /** 62 | * Completes the jobs in the list. 63 | * 64 | * @param list $jobs A list of pairs of messages and their corresponding acknowledgers 65 | */ 66 | abstract private function process(array $jobs): void; 67 | 68 | private function getBatchSize(): int 69 | { 70 | return 10; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Handler/HandlerDescriptor.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Handler; 13 | 14 | /** 15 | * Describes a handler and the possible associated options, such as `from_transport`, `bus`, etc. 16 | * 17 | * @author Samuel Roze 18 | */ 19 | final class HandlerDescriptor 20 | { 21 | private \Closure $handler; 22 | private string $name; 23 | private ?BatchHandlerInterface $batchHandler = null; 24 | 25 | public function __construct( 26 | callable $handler, 27 | private array $options = [], 28 | ) { 29 | $handler = $handler(...); 30 | 31 | $this->handler = $handler; 32 | 33 | $r = new \ReflectionFunction($handler); 34 | 35 | if ($r->isAnonymous()) { 36 | $this->name = 'Closure'; 37 | } elseif (!$handler = $r->getClosureThis()) { 38 | $class = $r->getClosureCalledClass(); 39 | 40 | $this->name = ($class ? $class->name.'::' : '').$r->name; 41 | } else { 42 | if ($handler instanceof BatchHandlerInterface) { 43 | $this->batchHandler = $handler; 44 | } 45 | 46 | $this->name = $handler::class.'::'.$r->name; 47 | } 48 | } 49 | 50 | public function getHandler(): \Closure 51 | { 52 | return $this->handler; 53 | } 54 | 55 | public function getName(): string 56 | { 57 | $name = $this->name; 58 | $alias = $this->options['alias'] ?? null; 59 | 60 | if (null !== $alias) { 61 | $name .= '@'.$alias; 62 | } 63 | 64 | return $name; 65 | } 66 | 67 | public function getBatchHandler(): ?BatchHandlerInterface 68 | { 69 | return $this->batchHandler; 70 | } 71 | 72 | public function getOption(string $option): mixed 73 | { 74 | return $this->options[$option] ?? null; 75 | } 76 | 77 | public function getOptions(): array 78 | { 79 | return $this->options; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Handler/HandlersLocator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Handler; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Stamp\ReceivedStamp; 16 | 17 | /** 18 | * Maps a message to a list of handlers. 19 | * 20 | * @author Nicolas Grekas 21 | * @author Samuel Roze 22 | */ 23 | class HandlersLocator implements HandlersLocatorInterface 24 | { 25 | /** 26 | * @param HandlerDescriptor[][]|callable[][] $handlers 27 | */ 28 | public function __construct( 29 | private array $handlers, 30 | ) { 31 | } 32 | 33 | public function getHandlers(Envelope $envelope): iterable 34 | { 35 | $seen = []; 36 | 37 | foreach (self::listTypes($envelope) as $type) { 38 | foreach ($this->handlers[$type] ?? [] as $handlerDescriptor) { 39 | if (\is_callable($handlerDescriptor)) { 40 | $handlerDescriptor = new HandlerDescriptor($handlerDescriptor); 41 | } 42 | 43 | if (!$this->shouldHandle($envelope, $handlerDescriptor)) { 44 | continue; 45 | } 46 | 47 | $name = $handlerDescriptor->getName(); 48 | if (\in_array($name, $seen, true)) { 49 | continue; 50 | } 51 | 52 | $seen[] = $name; 53 | 54 | yield $handlerDescriptor; 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * @internal 61 | */ 62 | public static function listTypes(Envelope $envelope): array 63 | { 64 | $class = $envelope->getMessage()::class; 65 | 66 | return [$class => $class] 67 | + class_parents($class) 68 | + class_implements($class) 69 | + self::listWildcards($class) 70 | + ['*' => '*']; 71 | } 72 | 73 | private static function listWildcards(string $type): array 74 | { 75 | $type .= '\*'; 76 | $wildcards = []; 77 | while ($i = strrpos($type, '\\', -3)) { 78 | $type = substr_replace($type, '\*', $i); 79 | $wildcards[$type] = $type; 80 | } 81 | 82 | return $wildcards; 83 | } 84 | 85 | private function shouldHandle(Envelope $envelope, HandlerDescriptor $handlerDescriptor): bool 86 | { 87 | if (null === $received = $envelope->last(ReceivedStamp::class)) { 88 | return true; 89 | } 90 | 91 | if (null === $expectedTransport = $handlerDescriptor->getOption('from_transport')) { 92 | return true; 93 | } 94 | 95 | return $received->getTransportName() === $expectedTransport; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Handler/HandlersLocatorInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Handler; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | /** 17 | * Maps a message to a list of handlers. 18 | * 19 | * @author Nicolas Grekas 20 | */ 21 | interface HandlersLocatorInterface 22 | { 23 | /** 24 | * Returns the handlers for the given message name. 25 | * 26 | * @return iterable 27 | */ 28 | public function getHandlers(Envelope $envelope): iterable; 29 | } 30 | -------------------------------------------------------------------------------- /Handler/RedispatchMessageHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Handler; 13 | 14 | use Symfony\Component\Messenger\Message\RedispatchMessage; 15 | use Symfony\Component\Messenger\MessageBusInterface; 16 | use Symfony\Component\Messenger\Stamp\HandledStamp; 17 | use Symfony\Component\Messenger\Stamp\TransportNamesStamp; 18 | 19 | final class RedispatchMessageHandler 20 | { 21 | public function __construct( 22 | private MessageBusInterface $bus, 23 | ) { 24 | } 25 | 26 | public function __invoke(RedispatchMessage $message): mixed 27 | { 28 | $envelope = $this->bus->dispatch($message->envelope, [new TransportNamesStamp($message->transportNames)]); 29 | 30 | return $envelope->last(HandledStamp::class)?->getResult(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Message/RedispatchMessage.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Message; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | final class RedispatchMessage implements \Stringable 17 | { 18 | /** 19 | * @param object|Envelope $envelope The message or the message pre-wrapped in an envelope 20 | * @param string[]|string $transportNames Transport names to be used for the message 21 | */ 22 | public function __construct( 23 | public readonly object $envelope, 24 | public readonly array|string $transportNames = [], 25 | ) { 26 | } 27 | 28 | public function __toString(): string 29 | { 30 | $message = $this->envelope instanceof Envelope ? $this->envelope->getMessage() : $this->envelope; 31 | 32 | return \sprintf('%s via %s', $message instanceof \Stringable ? (string) $message : $message::class, implode(', ', (array) $this->transportNames)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MessageBus.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger; 13 | 14 | use Symfony\Component\Messenger\Middleware\MiddlewareInterface; 15 | use Symfony\Component\Messenger\Middleware\StackMiddleware; 16 | 17 | /** 18 | * @author Samuel Roze 19 | * @author Matthias Noback 20 | * @author Nicolas Grekas 21 | */ 22 | class MessageBus implements MessageBusInterface 23 | { 24 | private \IteratorAggregate $middlewareAggregate; 25 | 26 | /** 27 | * @param iterable $middlewareHandlers 28 | */ 29 | public function __construct(iterable $middlewareHandlers = []) 30 | { 31 | if ($middlewareHandlers instanceof \IteratorAggregate) { 32 | $this->middlewareAggregate = $middlewareHandlers; 33 | } elseif (\is_array($middlewareHandlers)) { 34 | $this->middlewareAggregate = new \ArrayObject($middlewareHandlers); 35 | } else { 36 | // $this->middlewareAggregate should be an instance of IteratorAggregate. 37 | // When $middlewareHandlers is an Iterator, we wrap it to ensure it is lazy-loaded and can be rewound. 38 | $this->middlewareAggregate = new class($middlewareHandlers) implements \IteratorAggregate { 39 | private \ArrayObject $cachedIterator; 40 | 41 | public function __construct( 42 | private \Traversable $middlewareHandlers, 43 | ) { 44 | } 45 | 46 | public function getIterator(): \Traversable 47 | { 48 | return $this->cachedIterator ??= new \ArrayObject(iterator_to_array($this->middlewareHandlers, false)); 49 | } 50 | }; 51 | } 52 | } 53 | 54 | public function dispatch(object $message, array $stamps = []): Envelope 55 | { 56 | $envelope = Envelope::wrap($message, $stamps); 57 | $middlewareIterator = $this->middlewareAggregate->getIterator(); 58 | 59 | while ($middlewareIterator instanceof \IteratorAggregate) { 60 | $middlewareIterator = $middlewareIterator->getIterator(); 61 | } 62 | $middlewareIterator->rewind(); 63 | 64 | if (!$middlewareIterator->valid()) { 65 | return $envelope; 66 | } 67 | $stack = new StackMiddleware($middlewareIterator); 68 | 69 | return $middlewareIterator->current()->handle($envelope, $stack); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /MessageBusInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger; 13 | 14 | use Symfony\Component\Messenger\Exception\ExceptionInterface; 15 | use Symfony\Component\Messenger\Stamp\StampInterface; 16 | 17 | /** 18 | * @author Samuel Roze 19 | */ 20 | interface MessageBusInterface 21 | { 22 | /** 23 | * Dispatches the given message. 24 | * 25 | * @param object|Envelope $message The message or the message pre-wrapped in an envelope 26 | * @param StampInterface[] $stamps Stamps set on the Envelope which are used to control middleware behavior 27 | * 28 | * @throws ExceptionInterface 29 | */ 30 | public function dispatch(object $message, array $stamps = []): Envelope; 31 | } 32 | -------------------------------------------------------------------------------- /Middleware/ActivationMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | /** 17 | * Execute the inner middleware according to an activation strategy. 18 | * 19 | * @author Maxime Steinhausser 20 | */ 21 | class ActivationMiddleware implements MiddlewareInterface 22 | { 23 | private \Closure|bool $activated; 24 | 25 | public function __construct( 26 | private MiddlewareInterface $inner, 27 | bool|callable $activated, 28 | ) { 29 | $this->activated = \is_bool($activated) ? $activated : $activated(...); 30 | } 31 | 32 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 33 | { 34 | if (\is_callable($this->activated) ? ($this->activated)($envelope) : $this->activated) { 35 | return $this->inner->handle($envelope, $stack); 36 | } 37 | 38 | return $stack->next()->handle($envelope, $stack); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Middleware/AddBusNameStampMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Stamp\BusNameStamp; 16 | 17 | /** 18 | * Adds the BusNameStamp to the bus. 19 | * 20 | * @author Ryan Weaver 21 | */ 22 | class AddBusNameStampMiddleware implements MiddlewareInterface 23 | { 24 | public function __construct( 25 | private string $busName, 26 | ) { 27 | } 28 | 29 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 30 | { 31 | if (null === $envelope->last(BusNameStamp::class)) { 32 | $envelope = $envelope->with(new BusNameStamp($this->busName)); 33 | } 34 | 35 | return $stack->next()->handle($envelope, $stack); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Middleware/DeduplicateMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | use Symfony\Component\Lock\LockFactory; 15 | use Symfony\Component\Messenger\Envelope; 16 | use Symfony\Component\Messenger\Stamp\DeduplicateStamp; 17 | use Symfony\Component\Messenger\Stamp\ReceivedStamp; 18 | 19 | final class DeduplicateMiddleware implements MiddlewareInterface 20 | { 21 | public function __construct(private LockFactory $lockFactory) 22 | { 23 | } 24 | 25 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 26 | { 27 | if (!$stamp = $envelope->last(DeduplicateStamp::class)) { 28 | return $stack->next()->handle($envelope, $stack); 29 | } 30 | 31 | if (!$envelope->last(ReceivedStamp::class)) { 32 | $lock = $this->lockFactory->createLockFromKey($stamp->getKey(), $stamp->getTtl(), false); 33 | 34 | if (!$lock->acquire()) { 35 | return $envelope; 36 | } 37 | } elseif ($stamp->onlyDeduplicateInQueue()) { 38 | $this->lockFactory->createLockFromKey($stamp->getKey())->release(); 39 | } 40 | 41 | try { 42 | $envelope = $stack->next()->handle($envelope, $stack); 43 | } finally { 44 | if ($envelope->last(ReceivedStamp::class) && !$stamp->onlyDeduplicateInQueue()) { 45 | $this->lockFactory->createLockFromKey($stamp->getKey())->release(); 46 | } 47 | } 48 | 49 | return $envelope; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Middleware/DispatchAfterCurrentBusMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Exception\DelayedMessageHandlingException; 16 | use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp; 17 | 18 | /** 19 | * Allow to configure messages to be handled after the current bus is finished. 20 | * 21 | * I.e, messages dispatched from a handler with a DispatchAfterCurrentBus stamp 22 | * will actually be handled once the current message being dispatched is fully 23 | * handled. 24 | * 25 | * For instance, using this middleware before the DoctrineTransactionMiddleware 26 | * means sub-dispatched messages with a DispatchAfterCurrentBus stamp would be 27 | * handled after the Doctrine transaction has been committed. 28 | * 29 | * @author Tobias Nyholm 30 | */ 31 | class DispatchAfterCurrentBusMiddleware implements MiddlewareInterface 32 | { 33 | /** 34 | * @var QueuedEnvelope[] A queue of messages and next middleware 35 | */ 36 | private array $queue = []; 37 | 38 | /** 39 | * @var bool this property is used to signal if we are inside a the first/root call to 40 | * MessageBusInterface::dispatch() or if dispatch has been called inside a message handler 41 | */ 42 | private bool $isRootDispatchCallRunning = false; 43 | 44 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 45 | { 46 | if (null !== $envelope->last(DispatchAfterCurrentBusStamp::class)) { 47 | if ($this->isRootDispatchCallRunning) { 48 | $this->queue[] = new QueuedEnvelope($envelope, $stack); 49 | 50 | return $envelope; 51 | } 52 | 53 | $envelope = $envelope->withoutAll(DispatchAfterCurrentBusStamp::class); 54 | } 55 | 56 | if ($this->isRootDispatchCallRunning) { 57 | /* 58 | * A call to MessageBusInterface::dispatch() was made from inside the main bus handling, 59 | * but the message does not have the stamp. So, process it like normal. 60 | */ 61 | return $stack->next()->handle($envelope, $stack); 62 | } 63 | 64 | // First time we get here, mark as inside a "root dispatch" call: 65 | $this->isRootDispatchCallRunning = true; 66 | try { 67 | // Execute the whole middleware stack & message handling for main dispatch: 68 | $returnedEnvelope = $stack->next()->handle($envelope, $stack); 69 | } catch (\Throwable $exception) { 70 | /* 71 | * Whenever an exception occurs while handling a message that has 72 | * queued other messages, we drop the queued ones. 73 | * This is intentional since the queued commands were likely dependent 74 | * on the preceding command. 75 | */ 76 | $this->queue = []; 77 | $this->isRootDispatchCallRunning = false; 78 | 79 | throw $exception; 80 | } 81 | 82 | // "Root dispatch" call is finished, dispatch stored messages. 83 | $exceptions = []; 84 | while (null !== $queueItem = array_shift($this->queue)) { 85 | // Save how many messages are left in queue before handling the message 86 | $queueLengthBefore = \count($this->queue); 87 | try { 88 | // Execute the stored messages 89 | $queueItem->getStack()->next()->handle($queueItem->getEnvelope(), $queueItem->getStack()); 90 | } catch (\Exception $exception) { 91 | // Gather all exceptions 92 | $exceptions[] = $exception; 93 | // Restore queue to previous state 94 | $this->queue = \array_slice($this->queue, 0, $queueLengthBefore); 95 | } 96 | } 97 | 98 | $this->isRootDispatchCallRunning = false; 99 | if (\count($exceptions) > 0) { 100 | throw new DelayedMessageHandlingException($exceptions, $returnedEnvelope); 101 | } 102 | 103 | return $returnedEnvelope; 104 | } 105 | } 106 | 107 | /** 108 | * @internal 109 | */ 110 | final class QueuedEnvelope 111 | { 112 | private Envelope $envelope; 113 | 114 | public function __construct( 115 | Envelope $envelope, 116 | private StackInterface $stack, 117 | ) { 118 | $this->envelope = $envelope->withoutAll(DispatchAfterCurrentBusStamp::class); 119 | } 120 | 121 | public function getEnvelope(): Envelope 122 | { 123 | return $this->envelope; 124 | } 125 | 126 | public function getStack(): StackInterface 127 | { 128 | return $this->stack; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Middleware/FailedMessageProcessingMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Stamp\ReceivedStamp; 16 | use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp; 17 | 18 | /** 19 | * @author Ryan Weaver 20 | */ 21 | class FailedMessageProcessingMiddleware implements MiddlewareInterface 22 | { 23 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 24 | { 25 | // look for "received" messages decorated with the SentToFailureTransportStamp 26 | /** @var SentToFailureTransportStamp|null $sentToFailureStamp */ 27 | $sentToFailureStamp = $envelope->last(SentToFailureTransportStamp::class); 28 | if (null !== $sentToFailureStamp && null !== $envelope->last(ReceivedStamp::class)) { 29 | // mark the message as "received" from the original transport 30 | // this guarantees the same behavior as when originally received 31 | $envelope = $envelope->with(new ReceivedStamp($sentToFailureStamp->getOriginalReceiverName())); 32 | } 33 | 34 | return $stack->next()->handle($envelope, $stack); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Middleware/HandleMessageMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | use Psr\Log\LoggerAwareTrait; 15 | use Symfony\Component\Messenger\Envelope; 16 | use Symfony\Component\Messenger\Exception\HandlerFailedException; 17 | use Symfony\Component\Messenger\Exception\LogicException; 18 | use Symfony\Component\Messenger\Exception\NoHandlerForMessageException; 19 | use Symfony\Component\Messenger\Handler\Acknowledger; 20 | use Symfony\Component\Messenger\Handler\HandlerDescriptor; 21 | use Symfony\Component\Messenger\Handler\HandlersLocatorInterface; 22 | use Symfony\Component\Messenger\Stamp\AckStamp; 23 | use Symfony\Component\Messenger\Stamp\FlushBatchHandlersStamp; 24 | use Symfony\Component\Messenger\Stamp\HandledStamp; 25 | use Symfony\Component\Messenger\Stamp\HandlerArgumentsStamp; 26 | use Symfony\Component\Messenger\Stamp\NoAutoAckStamp; 27 | 28 | /** 29 | * @author Samuel Roze 30 | */ 31 | class HandleMessageMiddleware implements MiddlewareInterface 32 | { 33 | use LoggerAwareTrait; 34 | 35 | public function __construct( 36 | private HandlersLocatorInterface $handlersLocator, 37 | private bool $allowNoHandlers = false, 38 | ) { 39 | } 40 | 41 | /** 42 | * @throws NoHandlerForMessageException When no handler is found and $allowNoHandlers is false 43 | */ 44 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 45 | { 46 | $handler = null; 47 | $message = $envelope->getMessage(); 48 | 49 | $context = [ 50 | 'class' => $message::class, 51 | ]; 52 | 53 | $exceptions = []; 54 | $alreadyHandled = false; 55 | foreach ($this->handlersLocator->getHandlers($envelope) as $handlerDescriptor) { 56 | if ($this->messageHasAlreadyBeenHandled($envelope, $handlerDescriptor)) { 57 | $alreadyHandled = true; 58 | continue; 59 | } 60 | 61 | try { 62 | $handler = $handlerDescriptor->getHandler(); 63 | $batchHandler = $handlerDescriptor->getBatchHandler(); 64 | 65 | /** @var AckStamp $ackStamp */ 66 | if ($batchHandler && $ackStamp = $envelope->last(AckStamp::class)) { 67 | $ack = new Acknowledger(get_debug_type($batchHandler), static function (?\Throwable $e = null, $result = null) use ($envelope, $ackStamp, $handlerDescriptor) { 68 | if (null !== $e) { 69 | $e = new HandlerFailedException($envelope, [$handlerDescriptor->getName() => $e]); 70 | } else { 71 | $envelope = $envelope->with(HandledStamp::fromDescriptor($handlerDescriptor, $result)); 72 | } 73 | 74 | $ackStamp->ack($envelope, $e); 75 | }); 76 | 77 | $result = $this->callHandler($handler, $message, $ack, $envelope->last(HandlerArgumentsStamp::class)); 78 | 79 | if (!\is_int($result) || 0 > $result) { 80 | 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))); 81 | } 82 | 83 | if (!$ack->isAcknowledged()) { 84 | $envelope = $envelope->with(new NoAutoAckStamp($handlerDescriptor)); 85 | } elseif ($ack->getError()) { 86 | throw $ack->getError(); 87 | } else { 88 | $result = $ack->getResult(); 89 | } 90 | } else { 91 | $result = $this->callHandler($handler, $message, null, $envelope->last(HandlerArgumentsStamp::class)); 92 | } 93 | 94 | $handledStamp = HandledStamp::fromDescriptor($handlerDescriptor, $result); 95 | $envelope = $envelope->with($handledStamp); 96 | $this->logger?->info('Message {class} handled by {handler}', $context + ['handler' => $handledStamp->getHandlerName()]); 97 | } catch (\Throwable $e) { 98 | $exceptions[$handlerDescriptor->getName()] = $e; 99 | } 100 | } 101 | 102 | /** @var FlushBatchHandlersStamp $flushStamp */ 103 | if ($flushStamp = $envelope->last(FlushBatchHandlersStamp::class)) { 104 | /** @var NoAutoAckStamp $stamp */ 105 | foreach ($envelope->all(NoAutoAckStamp::class) as $stamp) { 106 | try { 107 | $handler = $stamp->getHandlerDescriptor()->getBatchHandler(); 108 | $handler->flush($flushStamp->force()); 109 | } catch (\Throwable $e) { 110 | $exceptions[$stamp->getHandlerDescriptor()->getName()] = $e; 111 | } 112 | } 113 | } 114 | 115 | if (null === $handler && !$alreadyHandled) { 116 | if (!$this->allowNoHandlers) { 117 | throw new NoHandlerForMessageException(\sprintf('No handler for message "%s".', $context['class'])); 118 | } 119 | 120 | $this->logger?->info('No handler for message {class}', $context); 121 | } 122 | 123 | if (\count($exceptions)) { 124 | throw new HandlerFailedException($envelope, $exceptions); 125 | } 126 | 127 | return $stack->next()->handle($envelope, $stack); 128 | } 129 | 130 | private function messageHasAlreadyBeenHandled(Envelope $envelope, HandlerDescriptor $handlerDescriptor): bool 131 | { 132 | /** @var HandledStamp $stamp */ 133 | foreach ($envelope->all(HandledStamp::class) as $stamp) { 134 | if ($stamp->getHandlerName() === $handlerDescriptor->getName()) { 135 | return true; 136 | } 137 | } 138 | 139 | return false; 140 | } 141 | 142 | private function callHandler(\Closure $handler, object $message, ?Acknowledger $ack, ?HandlerArgumentsStamp $handlerArgumentsStamp): mixed 143 | { 144 | $arguments = [$message]; 145 | if (null !== $ack) { 146 | $arguments[] = $ack; 147 | } 148 | if (null !== $handlerArgumentsStamp) { 149 | $arguments = [...$arguments, ...$handlerArgumentsStamp->getAdditionalArguments()]; 150 | } 151 | 152 | return $handler(...$arguments); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /Middleware/MiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Exception\ExceptionInterface; 16 | 17 | /** 18 | * @author Samuel Roze 19 | */ 20 | interface MiddlewareInterface 21 | { 22 | /** 23 | * @throws ExceptionInterface 24 | */ 25 | public function handle(Envelope $envelope, StackInterface $stack): Envelope; 26 | } 27 | -------------------------------------------------------------------------------- /Middleware/RejectRedeliveredMessageMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceivedStamp; 15 | use Symfony\Component\Messenger\Envelope; 16 | use Symfony\Component\Messenger\Exception\RejectRedeliveredMessageException; 17 | 18 | /** 19 | * Middleware that throws a RejectRedeliveredMessageException when a message is detected that has been redelivered by AMQP. 20 | * 21 | * The middleware runs before the HandleMessageMiddleware and prevents redelivered messages from being handled directly. 22 | * The thrown exception is caught by the worker and will trigger the retry logic according to the retry strategy. 23 | * 24 | * AMQP redelivers messages when they do not get acknowledged or rejected. This can happen when the connection times out 25 | * or an exception is thrown before acknowledging or rejecting. When such errors happen again while handling the 26 | * redelivered message, the message would get redelivered again and again. The purpose of this middleware is to prevent 27 | * infinite redelivery loops and to unblock the queue by republishing the redelivered messages as retries with a retry 28 | * limit and potential delay. 29 | * 30 | * @author Tobias Schultze 31 | */ 32 | class RejectRedeliveredMessageMiddleware implements MiddlewareInterface 33 | { 34 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 35 | { 36 | $amqpReceivedStamp = $envelope->last(AmqpReceivedStamp::class); 37 | if ($amqpReceivedStamp instanceof AmqpReceivedStamp && $amqpReceivedStamp->getAmqpEnvelope()->isRedelivery()) { 38 | throw new RejectRedeliveredMessageException('Redelivered message from AMQP detected that will be rejected and trigger the retry logic.'); 39 | } 40 | 41 | return $stack->next()->handle($envelope, $stack); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Middleware/RouterContextMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp; 16 | use Symfony\Component\Messenger\Stamp\RouterContextStamp; 17 | use Symfony\Component\Routing\RequestContextAwareInterface; 18 | 19 | /** 20 | * Restore the Router context when processing the message. 21 | * 22 | * @author Jérémy Derussé 23 | */ 24 | class RouterContextMiddleware implements MiddlewareInterface 25 | { 26 | public function __construct( 27 | private RequestContextAwareInterface $router, 28 | ) { 29 | } 30 | 31 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 32 | { 33 | $context = $this->router->getContext(); 34 | 35 | if (!$envelope->last(ConsumedByWorkerStamp::class) || !$contextStamp = $envelope->last(RouterContextStamp::class)) { 36 | $envelope = $envelope->with(new RouterContextStamp( 37 | $context->getBaseUrl(), 38 | $context->getMethod(), 39 | $context->getHost(), 40 | $context->getScheme(), 41 | $context->getHttpPort(), 42 | $context->getHttpsPort(), 43 | $context->getPathInfo(), 44 | $context->getQueryString() 45 | )); 46 | 47 | return $stack->next()->handle($envelope, $stack); 48 | } 49 | 50 | $currentBaseUrl = $context->getBaseUrl(); 51 | $currentMethod = $context->getMethod(); 52 | $currentHost = $context->getHost(); 53 | $currentScheme = $context->getScheme(); 54 | $currentHttpPort = $context->getHttpPort(); 55 | $currentHttpsPort = $context->getHttpsPort(); 56 | $currentPathInfo = $context->getPathInfo(); 57 | $currentQueryString = $context->getQueryString(); 58 | 59 | /* @var RouterContextStamp $contextStamp */ 60 | $context 61 | ->setBaseUrl($contextStamp->getBaseUrl()) 62 | ->setMethod($contextStamp->getMethod()) 63 | ->setHost($contextStamp->getHost()) 64 | ->setScheme($contextStamp->getScheme()) 65 | ->setHttpPort($contextStamp->getHttpPort()) 66 | ->setHttpsPort($contextStamp->getHttpsPort()) 67 | ->setPathInfo($contextStamp->getPathInfo()) 68 | ->setQueryString($contextStamp->getQueryString()) 69 | ; 70 | 71 | try { 72 | return $stack->next()->handle($envelope, $stack); 73 | } finally { 74 | $context 75 | ->setBaseUrl($currentBaseUrl) 76 | ->setMethod($currentMethod) 77 | ->setHost($currentHost) 78 | ->setScheme($currentScheme) 79 | ->setHttpPort($currentHttpPort) 80 | ->setHttpsPort($currentHttpsPort) 81 | ->setPathInfo($currentPathInfo) 82 | ->setQueryString($currentQueryString) 83 | ; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Middleware/SendMessageMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | use Psr\EventDispatcher\EventDispatcherInterface; 15 | use Psr\Log\LoggerAwareTrait; 16 | use Symfony\Component\Messenger\Envelope; 17 | use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent; 18 | use Symfony\Component\Messenger\Exception\NoSenderForMessageException; 19 | use Symfony\Component\Messenger\Stamp\ReceivedStamp; 20 | use Symfony\Component\Messenger\Stamp\SentStamp; 21 | use Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface; 22 | 23 | /** 24 | * @author Samuel Roze 25 | * @author Tobias Schultze 26 | */ 27 | class SendMessageMiddleware implements MiddlewareInterface 28 | { 29 | use LoggerAwareTrait; 30 | 31 | public function __construct( 32 | private SendersLocatorInterface $sendersLocator, 33 | private ?EventDispatcherInterface $eventDispatcher = null, 34 | private bool $allowNoSenders = true, 35 | ) { 36 | } 37 | 38 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 39 | { 40 | $context = [ 41 | 'class' => $envelope->getMessage()::class, 42 | ]; 43 | 44 | $sender = null; 45 | 46 | if ($envelope->all(ReceivedStamp::class)) { 47 | // it's a received message, do not send it back 48 | $this->logger?->info('Received message {class}', $context); 49 | } else { 50 | $shouldDispatchEvent = true; 51 | $senders = $this->sendersLocator->getSenders($envelope); 52 | $senders = \is_array($senders) ? $senders : iterator_to_array($senders); 53 | foreach ($senders as $alias => $sender) { 54 | if (null !== $this->eventDispatcher && $shouldDispatchEvent) { 55 | $event = new SendMessageToTransportsEvent($envelope, $senders); 56 | $this->eventDispatcher->dispatch($event); 57 | $envelope = $event->getEnvelope(); 58 | $shouldDispatchEvent = false; 59 | } 60 | 61 | $this->logger?->info('Sending message {class} with {alias} sender using {sender}', $context + ['alias' => $alias, 'sender' => $sender::class]); 62 | $envelope = $sender->send($envelope->with(new SentStamp($sender::class, \is_string($alias) ? $alias : null))); 63 | } 64 | 65 | if (!$this->allowNoSenders && !$sender) { 66 | throw new NoSenderForMessageException(\sprintf('No sender for message "%s".', $context['class'])); 67 | } 68 | } 69 | 70 | if (null === $sender) { 71 | return $stack->next()->handle($envelope, $stack); 72 | } 73 | 74 | // message should only be sent and not be handled by the next middleware 75 | return $envelope; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Middleware/StackInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | /** 15 | * @author Nicolas Grekas 16 | * 17 | * Implementations must be cloneable, and each clone must unstack the stack independently. 18 | */ 19 | interface StackInterface 20 | { 21 | /** 22 | * Returns the next middleware to process a message. 23 | */ 24 | public function next(): MiddlewareInterface; 25 | } 26 | -------------------------------------------------------------------------------- /Middleware/StackMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | /** 17 | * @author Nicolas Grekas 18 | */ 19 | class StackMiddleware implements MiddlewareInterface, StackInterface 20 | { 21 | private MiddlewareStack $stack; 22 | private int $offset = 0; 23 | 24 | /** 25 | * @param iterable|MiddlewareInterface|null $middlewareIterator 26 | */ 27 | public function __construct(iterable|MiddlewareInterface|null $middlewareIterator = null) 28 | { 29 | $this->stack = new MiddlewareStack(); 30 | 31 | if (null === $middlewareIterator) { 32 | return; 33 | } 34 | 35 | if ($middlewareIterator instanceof \Iterator) { 36 | $this->stack->iterator = $middlewareIterator; 37 | } elseif ($middlewareIterator instanceof MiddlewareInterface) { 38 | $this->stack->stack[] = $middlewareIterator; 39 | } else { 40 | $this->stack->iterator = (function () use ($middlewareIterator) { 41 | yield from $middlewareIterator; 42 | })(); 43 | } 44 | } 45 | 46 | public function next(): MiddlewareInterface 47 | { 48 | if (null === $next = $this->stack->next($this->offset)) { 49 | return $this; 50 | } 51 | 52 | ++$this->offset; 53 | 54 | return $next; 55 | } 56 | 57 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 58 | { 59 | return $envelope; 60 | } 61 | } 62 | 63 | /** 64 | * @internal 65 | */ 66 | class MiddlewareStack 67 | { 68 | /** @var \Iterator|null */ 69 | public ?\Iterator $iterator = null; 70 | public array $stack = []; 71 | 72 | public function next(int $offset): ?MiddlewareInterface 73 | { 74 | if (isset($this->stack[$offset])) { 75 | return $this->stack[$offset]; 76 | } 77 | 78 | if (null === $this->iterator) { 79 | return null; 80 | } 81 | 82 | $this->iterator->next(); 83 | 84 | if (!$this->iterator->valid()) { 85 | return $this->iterator = null; 86 | } 87 | 88 | return $this->stack[] = $this->iterator->current(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Middleware/TraceableMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Stopwatch\Stopwatch; 16 | 17 | /** 18 | * Collects some data about a middleware. 19 | * 20 | * @author Maxime Steinhausser 21 | */ 22 | class TraceableMiddleware implements MiddlewareInterface 23 | { 24 | public function __construct( 25 | private Stopwatch $stopwatch, 26 | private string $busName, 27 | private string $eventCategory = 'messenger.middleware', 28 | ) { 29 | } 30 | 31 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 32 | { 33 | $stack = new TraceableStack($stack, $this->stopwatch, $this->busName, $this->eventCategory); 34 | 35 | try { 36 | return $stack->next()->handle($envelope, $stack); 37 | } finally { 38 | $stack->stop(); 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * @internal 45 | */ 46 | class TraceableStack implements StackInterface 47 | { 48 | private ?string $currentEvent = null; 49 | 50 | public function __construct( 51 | private StackInterface $stack, 52 | private Stopwatch $stopwatch, 53 | private string $busName, 54 | private string $eventCategory, 55 | ) { 56 | } 57 | 58 | public function next(): MiddlewareInterface 59 | { 60 | if (null !== $this->currentEvent && $this->stopwatch->isStarted($this->currentEvent)) { 61 | $this->stopwatch->stop($this->currentEvent); 62 | } 63 | 64 | if ($this->stack === $nextMiddleware = $this->stack->next()) { 65 | $this->currentEvent = 'Tail'; 66 | } else { 67 | $this->currentEvent = \sprintf('"%s"', get_debug_type($nextMiddleware)); 68 | } 69 | $this->currentEvent .= \sprintf(' on "%s"', $this->busName); 70 | 71 | $this->stopwatch->start($this->currentEvent, $this->eventCategory); 72 | 73 | return $nextMiddleware; 74 | } 75 | 76 | public function stop(): void 77 | { 78 | if (null !== $this->currentEvent && $this->stopwatch->isStarted($this->currentEvent)) { 79 | $this->stopwatch->stop($this->currentEvent); 80 | } 81 | $this->currentEvent = null; 82 | } 83 | 84 | public function __clone() 85 | { 86 | $this->stack = clone $this->stack; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Middleware/ValidationMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Middleware; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Exception\ValidationFailedException; 16 | use Symfony\Component\Messenger\Stamp\ValidationStamp; 17 | use Symfony\Component\Validator\Validator\ValidatorInterface; 18 | 19 | /** 20 | * @author Tobias Nyholm 21 | */ 22 | class ValidationMiddleware implements MiddlewareInterface 23 | { 24 | public function __construct( 25 | private ValidatorInterface $validator, 26 | ) { 27 | } 28 | 29 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 30 | { 31 | $message = $envelope->getMessage(); 32 | $groups = null; 33 | /** @var ValidationStamp|null $validationStamp */ 34 | if ($validationStamp = $envelope->last(ValidationStamp::class)) { 35 | $groups = $validationStamp->getGroups(); 36 | } 37 | 38 | $violations = $this->validator->validate($message, null, $groups); 39 | if (\count($violations)) { 40 | throw new ValidationFailedException($message, $violations, $envelope); 41 | } 42 | 43 | return $stack->next()->handle($envelope, $stack); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Messenger Component 2 | =================== 3 | 4 | The Messenger component helps applications send and receive messages to/from 5 | other applications or via message queues. 6 | 7 | Sponsor 8 | ------- 9 | 10 | Help Symfony by [sponsoring][1] its development! 11 | 12 | Resources 13 | --------- 14 | 15 | * [Documentation](https://symfony.com/doc/current/components/messenger.html) 16 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 17 | * [Report issues](https://github.com/symfony/symfony/issues) and 18 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 19 | in the [main Symfony repository](https://github.com/symfony/symfony) 20 | 21 | [1]: https://symfony.com/sponsor 22 | -------------------------------------------------------------------------------- /Retry/MultiplierRetryStrategy.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Retry; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Exception\InvalidArgumentException; 16 | use Symfony\Component\Messenger\Stamp\RedeliveryStamp; 17 | 18 | /** 19 | * A retry strategy with a constant or exponential retry delay. 20 | * 21 | * For example, if $delayMilliseconds=10000 & $multiplier=1 (default), 22 | * each retry will wait exactly 10 seconds. 23 | * 24 | * But if $delayMilliseconds=10000 & $multiplier=2: 25 | * * Retry 1: 10 second delay 26 | * * Retry 2: 20 second delay (10000 * 2 = 20000) 27 | * * Retry 3: 40 second delay (20000 * 2 = 40000) 28 | * 29 | * @author Ryan Weaver 30 | * 31 | * @final 32 | */ 33 | class MultiplierRetryStrategy implements RetryStrategyInterface 34 | { 35 | /** 36 | * @param int $maxRetries The maximum number of times to retry 37 | * @param int $delayMilliseconds Amount of time to delay (or the initial value when multiplier is used) 38 | * @param float $multiplier Multiplier to apply to the delay each time a retry occurs 39 | * @param int $maxDelayMilliseconds Maximum delay to allow (0 means no maximum) 40 | * @param float $jitter Randomness to apply to the delay (between 0 and 1) 41 | */ 42 | public function __construct( 43 | private int $maxRetries = 3, 44 | private int $delayMilliseconds = 1000, 45 | private float $multiplier = 1, 46 | private int $maxDelayMilliseconds = 0, 47 | private float $jitter = 0.1, 48 | ) { 49 | if ($delayMilliseconds < 0) { 50 | throw new InvalidArgumentException(\sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMilliseconds)); 51 | } 52 | 53 | if ($multiplier < 1) { 54 | throw new InvalidArgumentException(\sprintf('Multiplier must be greater than zero: "%s" given.', $multiplier)); 55 | } 56 | 57 | if ($maxDelayMilliseconds < 0) { 58 | throw new InvalidArgumentException(\sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMilliseconds)); 59 | } 60 | 61 | if ($jitter < 0 || $jitter > 1) { 62 | throw new InvalidArgumentException(\sprintf('Jitter must be between 0 and 1: "%s" given.', $jitter)); 63 | } 64 | } 65 | 66 | /** 67 | * @param \Throwable|null $throwable The cause of the failed handling 68 | */ 69 | public function isRetryable(Envelope $message, ?\Throwable $throwable = null): bool 70 | { 71 | $retries = RedeliveryStamp::getRetryCountFromEnvelope($message); 72 | 73 | return $retries < $this->maxRetries; 74 | } 75 | 76 | /** 77 | * @param \Throwable|null $throwable The cause of the failed handling 78 | */ 79 | public function getWaitingTime(Envelope $message, ?\Throwable $throwable = null): int 80 | { 81 | $retries = RedeliveryStamp::getRetryCountFromEnvelope($message); 82 | 83 | $delay = $this->delayMilliseconds * $this->multiplier ** $retries; 84 | 85 | if ($this->jitter > 0) { 86 | $randomness = (int) min(\PHP_INT_MAX, $delay * $this->jitter); 87 | $delay += random_int(-$randomness, +$randomness); 88 | } 89 | 90 | if ($delay > $this->maxDelayMilliseconds && 0 !== $this->maxDelayMilliseconds) { 91 | return $this->maxDelayMilliseconds; 92 | } 93 | 94 | return (int) min(\PHP_INT_MAX, ceil($delay)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Retry/RetryStrategyInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Retry; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | /** 17 | * @author Fabien Potencier 18 | * @author Grégoire Pineau 19 | * @author Ryan Weaver 20 | */ 21 | interface RetryStrategyInterface 22 | { 23 | /** 24 | * @param \Throwable|null $throwable The cause of the failed handling 25 | */ 26 | public function isRetryable(Envelope $message, ?\Throwable $throwable = null): bool; 27 | 28 | /** 29 | * @param \Throwable|null $throwable The cause of the failed handling 30 | * 31 | * @return int The time to delay/wait in milliseconds 32 | */ 33 | public function getWaitingTime(Envelope $message, ?\Throwable $throwable = null): int; 34 | } 35 | -------------------------------------------------------------------------------- /RoutableMessageBus.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger; 13 | 14 | use Psr\Container\ContainerInterface; 15 | use Symfony\Component\Messenger\Exception\InvalidArgumentException; 16 | use Symfony\Component\Messenger\Stamp\BusNameStamp; 17 | 18 | /** 19 | * Bus of buses that is routable using a BusNameStamp. 20 | * 21 | * This is useful when passed to Worker: messages received 22 | * from the transport can be sent to the correct bus. 23 | * 24 | * @author Ryan Weaver 25 | */ 26 | class RoutableMessageBus implements MessageBusInterface 27 | { 28 | public function __construct( 29 | private ContainerInterface $busLocator, 30 | private ?MessageBusInterface $fallbackBus = null, 31 | ) { 32 | } 33 | 34 | public function dispatch(object $envelope, array $stamps = []): Envelope 35 | { 36 | if (!$envelope instanceof Envelope) { 37 | throw new InvalidArgumentException('Messages passed to RoutableMessageBus::dispatch() must be inside an Envelope.'); 38 | } 39 | 40 | /** @var BusNameStamp|null $busNameStamp */ 41 | $busNameStamp = $envelope->last(BusNameStamp::class); 42 | 43 | if (null === $busNameStamp) { 44 | if (null === $this->fallbackBus) { 45 | throw new InvalidArgumentException('Envelope is missing a BusNameStamp and no fallback message bus is configured on RoutableMessageBus.'); 46 | } 47 | 48 | return $this->fallbackBus->dispatch($envelope, $stamps); 49 | } 50 | 51 | return $this->getMessageBus($busNameStamp->getBusName())->dispatch($envelope, $stamps); 52 | } 53 | 54 | /** 55 | * @internal 56 | */ 57 | public function getMessageBus(string $busName): MessageBusInterface 58 | { 59 | if (!$this->busLocator->has($busName)) { 60 | throw new InvalidArgumentException(\sprintf('Bus named "%s" does not exist.', $busName)); 61 | } 62 | 63 | return $this->busLocator->get($busName); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Stamp/AckStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | /** 17 | * Marker stamp for messages that can be ack/nack'ed. 18 | */ 19 | final class AckStamp implements NonSendableStampInterface 20 | { 21 | /** 22 | * @param \Closure(Envelope, \Throwable|null) $ack 23 | */ 24 | public function __construct( 25 | private readonly \Closure $ack, 26 | ) { 27 | } 28 | 29 | public function ack(Envelope $envelope, ?\Throwable $e = null): void 30 | { 31 | ($this->ack)($envelope, $e); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Stamp/BusNameStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * Stamp used to identify which bus it was passed to. 16 | * 17 | * @author Ryan Weaver 18 | */ 19 | final class BusNameStamp implements StampInterface 20 | { 21 | public function __construct( 22 | private string $busName, 23 | ) { 24 | } 25 | 26 | public function getBusName(): string 27 | { 28 | return $this->busName; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Stamp/ConsumedByWorkerStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * A marker that this message was consumed by a worker process. 16 | */ 17 | class ConsumedByWorkerStamp implements NonSendableStampInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Stamp/DeduplicateStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | use Symfony\Component\Lock\Key; 15 | use Symfony\Component\Messenger\Exception\LogicException; 16 | 17 | final class DeduplicateStamp implements StampInterface 18 | { 19 | private Key $key; 20 | 21 | public function __construct( 22 | string $key, 23 | private ?float $ttl = 300.0, 24 | private bool $onlyDeduplicateInQueue = false, 25 | ) { 26 | if (!class_exists(Key::class)) { 27 | throw new LogicException(\sprintf('You cannot use the "%s" as the Lock component is not installed. Try running "composer require symfony/lock".', self::class)); 28 | } 29 | 30 | $this->key = new Key($key); 31 | } 32 | 33 | public function onlyDeduplicateInQueue(): bool 34 | { 35 | return $this->onlyDeduplicateInQueue; 36 | } 37 | 38 | public function getKey(): Key 39 | { 40 | return $this->key; 41 | } 42 | 43 | public function getTtl(): ?float 44 | { 45 | return $this->ttl; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Stamp/DelayStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * Apply this stamp to delay delivery of your message on a transport. 16 | */ 17 | final class DelayStamp implements StampInterface 18 | { 19 | /** 20 | * @param int $delay The delay in milliseconds 21 | */ 22 | public function __construct( 23 | private int $delay, 24 | ) { 25 | } 26 | 27 | public function getDelay(): int 28 | { 29 | return $this->delay; 30 | } 31 | 32 | public static function delayFor(\DateInterval $interval): self 33 | { 34 | $now = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); 35 | $end = $now->add($interval); 36 | 37 | return new self(($end->getTimestamp() - $now->getTimestamp()) * 1000); 38 | } 39 | 40 | public static function delayUntil(\DateTimeInterface $dateTime): self 41 | { 42 | return new self(($dateTime->getTimestamp() - time()) * 1000); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Stamp/DispatchAfterCurrentBusStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * Marker item to tell this message should be handled in after the current bus has finished. 16 | * 17 | * @see \Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware 18 | * 19 | * @author Tobias Nyholm 20 | */ 21 | final class DispatchAfterCurrentBusStamp implements NonSendableStampInterface 22 | { 23 | } 24 | -------------------------------------------------------------------------------- /Stamp/ErrorDetailsStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | use Symfony\Component\ErrorHandler\Exception\FlattenException; 15 | use Symfony\Component\Messenger\Exception\HandlerFailedException; 16 | 17 | /** 18 | * Stamp applied when a messages fails due to an exception in the handler. 19 | */ 20 | final class ErrorDetailsStamp implements StampInterface 21 | { 22 | public function __construct( 23 | private string $exceptionClass, 24 | private int|string $exceptionCode, 25 | private string $exceptionMessage, 26 | private ?FlattenException $flattenException = null, 27 | ) { 28 | } 29 | 30 | public static function create(\Throwable $throwable): self 31 | { 32 | if ($throwable instanceof HandlerFailedException) { 33 | $throwable = $throwable->getPrevious(); 34 | } 35 | 36 | $flattenException = null; 37 | if (class_exists(FlattenException::class)) { 38 | $flattenException = FlattenException::createFromThrowable($throwable); 39 | } 40 | 41 | return new self($throwable::class, $throwable->getCode(), $throwable->getMessage(), $flattenException); 42 | } 43 | 44 | public function getExceptionClass(): string 45 | { 46 | return $this->exceptionClass; 47 | } 48 | 49 | public function getExceptionCode(): int|string 50 | { 51 | return $this->exceptionCode; 52 | } 53 | 54 | public function getExceptionMessage(): string 55 | { 56 | return $this->exceptionMessage; 57 | } 58 | 59 | public function getFlattenException(): ?FlattenException 60 | { 61 | return $this->flattenException; 62 | } 63 | 64 | public function equals(?self $that): bool 65 | { 66 | if (null === $that) { 67 | return false; 68 | } 69 | 70 | if ($this->flattenException && $that->flattenException) { 71 | return $this->flattenException->getClass() === $that->flattenException->getClass() 72 | && $this->flattenException->getCode() === $that->flattenException->getCode() 73 | && $this->flattenException->getMessage() === $that->flattenException->getMessage(); 74 | } 75 | 76 | return $this->exceptionClass === $that->exceptionClass 77 | && $this->exceptionCode === $that->exceptionCode 78 | && $this->exceptionMessage === $that->exceptionMessage; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Stamp/FlushBatchHandlersStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * Marker telling that any batch handlers bound to the envelope should be flushed. 16 | */ 17 | final class FlushBatchHandlersStamp implements NonSendableStampInterface 18 | { 19 | public function __construct( 20 | private bool $force, 21 | ) { 22 | } 23 | 24 | public function force(): bool 25 | { 26 | return $this->force; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Stamp/HandledStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | use Symfony\Component\Messenger\Handler\HandlerDescriptor; 15 | 16 | /** 17 | * Stamp identifying a message handled by the `HandleMessageMiddleware` middleware 18 | * and storing the handler returned value. 19 | * 20 | * This is used by synchronous command buses expecting a return value and the retry logic 21 | * to only execute handlers that didn't succeed. 22 | * 23 | * @see \Symfony\Component\Messenger\Middleware\HandleMessageMiddleware 24 | * @see \Symfony\Component\Messenger\HandleTrait 25 | * 26 | * @author Maxime Steinhausser 27 | */ 28 | final class HandledStamp implements StampInterface 29 | { 30 | public function __construct( 31 | private mixed $result, 32 | private string $handlerName, 33 | ) { 34 | } 35 | 36 | public static function fromDescriptor(HandlerDescriptor $handler, mixed $result): self 37 | { 38 | return new self($result, $handler->getName()); 39 | } 40 | 41 | public function getResult(): mixed 42 | { 43 | return $this->result; 44 | } 45 | 46 | public function getHandlerName(): string 47 | { 48 | return $this->handlerName; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Stamp/HandlerArgumentsStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * @author Jáchym Toušek 16 | */ 17 | final class HandlerArgumentsStamp implements NonSendableStampInterface 18 | { 19 | public function __construct( 20 | private array $additionalArguments, 21 | ) { 22 | } 23 | 24 | public function getAdditionalArguments(): array 25 | { 26 | return $this->additionalArguments; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Stamp/MessageDecodingFailedStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * @author Grégoire Pineau 16 | */ 17 | class MessageDecodingFailedStamp implements StampInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Stamp/NoAutoAckStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | use Symfony\Component\Messenger\Handler\HandlerDescriptor; 15 | 16 | /** 17 | * Marker telling that ack should not be done automatically for this message. 18 | */ 19 | final class NoAutoAckStamp implements NonSendableStampInterface 20 | { 21 | public function __construct( 22 | private HandlerDescriptor $handlerDescriptor, 23 | ) { 24 | } 25 | 26 | public function getHandlerDescriptor(): HandlerDescriptor 27 | { 28 | return $this->handlerDescriptor; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Stamp/NonSendableStampInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * A stamp that should not be included with the Envelope if sent to a transport. 16 | * 17 | * @author Ryan Weaver 18 | */ 19 | interface NonSendableStampInterface extends StampInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Stamp/ReceivedStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | use Symfony\Component\Messenger\Middleware\SendMessageMiddleware; 15 | 16 | /** 17 | * Marker stamp for a received message. 18 | * 19 | * This is mainly used by the `SendMessageMiddleware` middleware to identify 20 | * a message should not be sent if it was just received. 21 | * 22 | * @see SendMessageMiddleware 23 | * 24 | * @author Samuel Roze 25 | */ 26 | final class ReceivedStamp implements NonSendableStampInterface 27 | { 28 | public function __construct( 29 | private string $transportName, 30 | ) { 31 | } 32 | 33 | public function getTransportName(): string 34 | { 35 | return $this->transportName; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Stamp/RedeliveryStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | /** 17 | * Stamp applied when a messages needs to be redelivered. 18 | */ 19 | final class RedeliveryStamp implements StampInterface 20 | { 21 | private \DateTimeInterface $redeliveredAt; 22 | 23 | public function __construct( 24 | private int $retryCount, 25 | ?\DateTimeInterface $redeliveredAt = null, 26 | ) { 27 | $this->redeliveredAt = $redeliveredAt ?? new \DateTimeImmutable(); 28 | } 29 | 30 | public static function getRetryCountFromEnvelope(Envelope $envelope): int 31 | { 32 | /** @var self|null $retryMessageStamp */ 33 | $retryMessageStamp = $envelope->last(self::class); 34 | 35 | return $retryMessageStamp ? $retryMessageStamp->getRetryCount() : 0; 36 | } 37 | 38 | public function getRetryCount(): int 39 | { 40 | return $this->retryCount; 41 | } 42 | 43 | public function getRedeliveredAt(): \DateTimeInterface 44 | { 45 | return $this->redeliveredAt; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Stamp/RouterContextStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * @author Jérémy Derussé 16 | */ 17 | class RouterContextStamp implements StampInterface 18 | { 19 | public function __construct( 20 | private string $baseUrl, 21 | private string $method, 22 | private string $host, 23 | private string $scheme, 24 | private int $httpPort, 25 | private int $httpsPort, 26 | private string $pathInfo, 27 | private string $queryString, 28 | ) { 29 | } 30 | 31 | public function getBaseUrl(): string 32 | { 33 | return $this->baseUrl; 34 | } 35 | 36 | public function getMethod(): string 37 | { 38 | return $this->method; 39 | } 40 | 41 | public function getHost(): string 42 | { 43 | return $this->host; 44 | } 45 | 46 | public function getScheme(): string 47 | { 48 | return $this->scheme; 49 | } 50 | 51 | public function getHttpPort(): int 52 | { 53 | return $this->httpPort; 54 | } 55 | 56 | public function getHttpsPort(): int 57 | { 58 | return $this->httpsPort; 59 | } 60 | 61 | public function getPathInfo(): string 62 | { 63 | return $this->pathInfo; 64 | } 65 | 66 | public function getQueryString(): string 67 | { 68 | return $this->queryString; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Stamp/SentForRetryStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * Stamp indicating whether a failed message has been sent for retry. 16 | */ 17 | final class SentForRetryStamp implements NonSendableStampInterface 18 | { 19 | public function __construct( 20 | public readonly bool $isSent, 21 | ) { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Stamp/SentStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * Marker stamp identifying a message sent by the `SendMessageMiddleware`. 16 | * 17 | * @see \Symfony\Component\Messenger\Middleware\SendMessageMiddleware 18 | * 19 | * @author Maxime Steinhausser 20 | */ 21 | final class SentStamp implements NonSendableStampInterface 22 | { 23 | public function __construct( 24 | private string $senderClass, 25 | private ?string $senderAlias = null, 26 | ) { 27 | } 28 | 29 | public function getSenderClass(): string 30 | { 31 | return $this->senderClass; 32 | } 33 | 34 | public function getSenderAlias(): ?string 35 | { 36 | return $this->senderAlias; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Stamp/SentToFailureTransportStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * Stamp applied when a message is sent to the failure transport. 16 | * 17 | * @author Ryan Weaver 18 | */ 19 | final class SentToFailureTransportStamp implements StampInterface 20 | { 21 | public function __construct( 22 | private string $originalReceiverName, 23 | ) { 24 | } 25 | 26 | public function getOriginalReceiverName(): string 27 | { 28 | return $this->originalReceiverName; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Stamp/SerializedMessageStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | final class SerializedMessageStamp implements NonSendableStampInterface 15 | { 16 | public function __construct(private string $serializedMessage) 17 | { 18 | } 19 | 20 | public function getSerializedMessage(): string 21 | { 22 | return $this->serializedMessage; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Stamp/SerializerStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * @author Maxime Steinhausser 16 | */ 17 | final class SerializerStamp implements StampInterface 18 | { 19 | public function __construct( 20 | private array $context, 21 | ) { 22 | } 23 | 24 | public function getContext(): array 25 | { 26 | return $this->context; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Stamp/StampInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * An envelope stamp related to a message. 16 | * 17 | * Stamps must be serializable value objects for transport. 18 | * 19 | * @author Maxime Steinhausser 20 | */ 21 | interface StampInterface 22 | { 23 | } 24 | -------------------------------------------------------------------------------- /Stamp/TransportMessageIdStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * Added by a sender or receiver to indicate the id of this message in that transport. 16 | * 17 | * @author Ryan Weaver 18 | */ 19 | final class TransportMessageIdStamp implements StampInterface 20 | { 21 | /** 22 | * @param mixed $id some "identifier" of the message in a transport 23 | */ 24 | public function __construct( 25 | private mixed $id, 26 | ) { 27 | } 28 | 29 | public function getId(): mixed 30 | { 31 | return $this->id; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Stamp/TransportNamesStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | /** 15 | * Stamp used to override the transport names specified in the Messenger routing configuration file. 16 | */ 17 | final class TransportNamesStamp implements StampInterface 18 | { 19 | private array $transportNames; 20 | 21 | /** 22 | * @param string[]|string $transportNames Transport names to be used for the message 23 | */ 24 | public function __construct(array|string $transportNames) 25 | { 26 | $this->transportNames = (array) $transportNames; 27 | } 28 | 29 | public function getTransportNames(): array 30 | { 31 | return $this->transportNames; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Stamp/ValidationStamp.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Stamp; 13 | 14 | use Symfony\Component\Validator\Constraints\GroupSequence; 15 | 16 | /** 17 | * @author Maxime Steinhausser 18 | */ 19 | final class ValidationStamp implements StampInterface 20 | { 21 | /** 22 | * @param string[]|GroupSequence $groups 23 | */ 24 | public function __construct( 25 | private array|GroupSequence $groups, 26 | ) { 27 | } 28 | 29 | /** @return string[]|GroupSequence */ 30 | public function getGroups(): array|GroupSequence 31 | { 32 | return $this->groups; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Test/Middleware/MiddlewareTestCase.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Test\Middleware; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | use Symfony\Component\Messenger\Envelope; 16 | use Symfony\Component\Messenger\Middleware\MiddlewareInterface; 17 | use Symfony\Component\Messenger\Middleware\StackInterface; 18 | use Symfony\Component\Messenger\Middleware\StackMiddleware; 19 | 20 | /** 21 | * @author Nicolas Grekas 22 | */ 23 | abstract class MiddlewareTestCase extends TestCase 24 | { 25 | protected function getStackMock(bool $nextIsCalled = true) 26 | { 27 | if (!$nextIsCalled) { 28 | $stack = $this->createMock(StackInterface::class); 29 | $stack 30 | ->expects($this->never()) 31 | ->method('next') 32 | ; 33 | 34 | return $stack; 35 | } 36 | 37 | $nextMiddleware = $this->createMock(MiddlewareInterface::class); 38 | $nextMiddleware 39 | ->expects($this->once()) 40 | ->method('handle') 41 | ->willReturnCallback(fn (Envelope $envelope, StackInterface $stack): Envelope => $envelope) 42 | ; 43 | 44 | return new StackMiddleware($nextMiddleware); 45 | } 46 | 47 | protected function getThrowingStackMock(?\Throwable $throwable = null) 48 | { 49 | $nextMiddleware = $this->createMock(MiddlewareInterface::class); 50 | $nextMiddleware 51 | ->expects($this->once()) 52 | ->method('handle') 53 | ->willThrowException($throwable ?? new \RuntimeException('Thrown from next middleware.')) 54 | ; 55 | 56 | return new StackMiddleware($nextMiddleware); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /TraceableMessageBus.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger; 13 | 14 | /** 15 | * @author Samuel Roze 16 | */ 17 | class TraceableMessageBus implements MessageBusInterface 18 | { 19 | private array $dispatchedMessages = []; 20 | 21 | public function __construct( 22 | private MessageBusInterface $decoratedBus, 23 | protected readonly ?\Closure $disabled = null, 24 | ) { 25 | } 26 | 27 | public function dispatch(object $message, array $stamps = []): Envelope 28 | { 29 | if ($this->disabled?->__invoke()) { 30 | return $this->decoratedBus->dispatch($message, $stamps); 31 | } 32 | 33 | $envelope = Envelope::wrap($message, $stamps); 34 | $context = [ 35 | 'stamps' => array_merge([], ...array_values($envelope->all())), 36 | 'message' => $envelope->getMessage(), 37 | 'caller' => $this->getCaller(), 38 | 'callTime' => microtime(true), 39 | ]; 40 | 41 | try { 42 | return $envelope = $this->decoratedBus->dispatch($message, $stamps); 43 | } catch (\Throwable $e) { 44 | $context['exception'] = $e; 45 | 46 | throw $e; 47 | } finally { 48 | $this->dispatchedMessages[] = $context + ['stamps_after_dispatch' => array_merge([], ...array_values($envelope->all()))]; 49 | } 50 | } 51 | 52 | public function getDispatchedMessages(): array 53 | { 54 | return $this->dispatchedMessages; 55 | } 56 | 57 | public function reset(): void 58 | { 59 | $this->dispatchedMessages = []; 60 | } 61 | 62 | private function getCaller(): array 63 | { 64 | $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 8); 65 | 66 | $file = $trace[1]['file'] ?? null; 67 | $line = $trace[1]['line'] ?? null; 68 | 69 | $handleTraitFile = (new \ReflectionClass(HandleTrait::class))->getFileName(); 70 | $found = false; 71 | for ($i = 1; $i < 8; ++$i) { 72 | if (isset($trace[$i]['file'], $trace[$i + 1]['file'], $trace[$i + 1]['line']) && $trace[$i]['file'] === $handleTraitFile) { 73 | $file = $trace[$i + 1]['file']; 74 | $line = $trace[$i + 1]['line']; 75 | $found = true; 76 | 77 | break; 78 | } 79 | } 80 | 81 | for ($i = 2; $i < 8 && !$found; ++$i) { 82 | if (isset($trace[$i]['class'], $trace[$i]['function']) 83 | && 'dispatch' === $trace[$i]['function'] 84 | && is_a($trace[$i]['class'], MessageBusInterface::class, true) 85 | ) { 86 | $file = $trace[$i]['file']; 87 | $line = $trace[$i]['line']; 88 | 89 | while (++$i < 8) { 90 | if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && !str_starts_with($trace[$i]['function'], 'call_user_func')) { 91 | $file = $trace[$i]['file']; 92 | $line = $trace[$i]['line']; 93 | 94 | break; 95 | } 96 | } 97 | break; 98 | } 99 | } 100 | 101 | $name = str_replace('\\', '/', (string) $file); 102 | 103 | return [ 104 | 'name' => substr($name, strrpos($name, '/') + 1), 105 | 'file' => $file, 106 | 'line' => $line, 107 | ]; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Transport/CloseableTransportInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport; 13 | 14 | interface CloseableTransportInterface 15 | { 16 | public function close(): void; 17 | } 18 | -------------------------------------------------------------------------------- /Transport/InMemory/InMemoryTransport.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\InMemory; 13 | 14 | use Psr\Clock\ClockInterface; 15 | use Symfony\Component\Messenger\Envelope; 16 | use Symfony\Component\Messenger\Exception\LogicException; 17 | use Symfony\Component\Messenger\Stamp\DelayStamp; 18 | use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp; 19 | use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; 20 | use Symfony\Component\Messenger\Transport\TransportInterface; 21 | use Symfony\Contracts\Service\ResetInterface; 22 | 23 | /** 24 | * Transport that stays in memory. Useful for testing purpose. 25 | * 26 | * @author Gary PEGEOT 27 | */ 28 | class InMemoryTransport implements TransportInterface, ResetInterface 29 | { 30 | /** 31 | * @var Envelope[] 32 | */ 33 | private array $sent = []; 34 | 35 | /** 36 | * @var Envelope[] 37 | */ 38 | private array $acknowledged = []; 39 | 40 | /** 41 | * @var Envelope[] 42 | */ 43 | private array $rejected = []; 44 | 45 | /** 46 | * @var Envelope[] 47 | */ 48 | private array $queue = []; 49 | 50 | private int $nextId = 1; 51 | private array $availableAt = []; 52 | 53 | public function __construct( 54 | private ?SerializerInterface $serializer = null, 55 | private ?ClockInterface $clock = null, 56 | ) { 57 | } 58 | 59 | public function get(): iterable 60 | { 61 | $envelopes = []; 62 | $now = $this->clock?->now() ?? new \DateTimeImmutable(); 63 | foreach ($this->decode($this->queue) as $id => $envelope) { 64 | if (!isset($this->availableAt[$id]) || $now > $this->availableAt[$id]) { 65 | $envelopes[] = $envelope; 66 | } 67 | } 68 | 69 | return $envelopes; 70 | } 71 | 72 | public function ack(Envelope $envelope): void 73 | { 74 | $this->acknowledged[] = $this->encode($envelope); 75 | 76 | if (!$transportMessageIdStamp = $envelope->last(TransportMessageIdStamp::class)) { 77 | throw new LogicException('No TransportMessageIdStamp found on the Envelope.'); 78 | } 79 | 80 | unset($this->queue[$id = $transportMessageIdStamp->getId()], $this->availableAt[$id]); 81 | } 82 | 83 | public function reject(Envelope $envelope): void 84 | { 85 | $this->rejected[] = $this->encode($envelope); 86 | 87 | if (!$transportMessageIdStamp = $envelope->last(TransportMessageIdStamp::class)) { 88 | throw new LogicException('No TransportMessageIdStamp found on the Envelope.'); 89 | } 90 | 91 | unset($this->queue[$id = $transportMessageIdStamp->getId()], $this->availableAt[$id]); 92 | } 93 | 94 | public function send(Envelope $envelope): Envelope 95 | { 96 | $id = $this->nextId++; 97 | $envelope = $envelope->with(new TransportMessageIdStamp($id)); 98 | $encodedEnvelope = $this->encode($envelope); 99 | $this->sent[] = $encodedEnvelope; 100 | $this->queue[$id] = $encodedEnvelope; 101 | 102 | /** @var DelayStamp|null $delayStamp */ 103 | if ($delayStamp = $envelope->last(DelayStamp::class)) { 104 | $now = $this->clock?->now() ?? new \DateTimeImmutable(); 105 | $this->availableAt[$id] = $now->modify(\sprintf('+%d seconds', $delayStamp->getDelay() / 1000)); 106 | } 107 | 108 | return $envelope; 109 | } 110 | 111 | public function reset(): void 112 | { 113 | $this->sent = $this->queue = $this->rejected = $this->acknowledged = []; 114 | } 115 | 116 | /** 117 | * @return Envelope[] 118 | */ 119 | public function getAcknowledged(): array 120 | { 121 | return $this->decode($this->acknowledged); 122 | } 123 | 124 | /** 125 | * @return Envelope[] 126 | */ 127 | public function getRejected(): array 128 | { 129 | return $this->decode($this->rejected); 130 | } 131 | 132 | /** 133 | * @return Envelope[] 134 | */ 135 | public function getSent(): array 136 | { 137 | return $this->decode($this->sent); 138 | } 139 | 140 | private function encode(Envelope $envelope): Envelope|array 141 | { 142 | if (null === $this->serializer) { 143 | return $envelope; 144 | } 145 | 146 | return $this->serializer->encode($envelope); 147 | } 148 | 149 | /** 150 | * @param array $messagesEncoded 151 | * 152 | * @return Envelope[] 153 | */ 154 | private function decode(array $messagesEncoded): array 155 | { 156 | if (null === $this->serializer) { 157 | return $messagesEncoded; 158 | } 159 | 160 | return array_map($this->serializer->decode(...), $messagesEncoded); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Transport/InMemory/InMemoryTransportFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\InMemory; 13 | 14 | use Psr\Clock\ClockInterface; 15 | use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; 16 | use Symfony\Component\Messenger\Transport\TransportFactoryInterface; 17 | use Symfony\Component\Messenger\Transport\TransportInterface; 18 | use Symfony\Contracts\Service\ResetInterface; 19 | 20 | /** 21 | * @author Gary PEGEOT 22 | * 23 | * @implements TransportFactoryInterface 24 | */ 25 | class InMemoryTransportFactory implements TransportFactoryInterface, ResetInterface 26 | { 27 | /** 28 | * @var InMemoryTransport[] 29 | */ 30 | private array $createdTransports = []; 31 | 32 | public function __construct( 33 | private readonly ?ClockInterface $clock = null, 34 | ) { 35 | } 36 | 37 | public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface 38 | { 39 | ['serialize' => $serialize] = $this->parseDsn($dsn); 40 | 41 | return $this->createdTransports[] = new InMemoryTransport($serialize ? $serializer : null, $this->clock); 42 | } 43 | 44 | public function supports(string $dsn, array $options): bool 45 | { 46 | return str_starts_with($dsn, 'in-memory://'); 47 | } 48 | 49 | public function reset(): void 50 | { 51 | foreach ($this->createdTransports as $transport) { 52 | $transport->reset(); 53 | } 54 | } 55 | 56 | private function parseDsn(string $dsn): array 57 | { 58 | $query = []; 59 | if ($queryAsString = strstr($dsn, '?')) { 60 | parse_str(ltrim($queryAsString, '?'), $query); 61 | } 62 | 63 | return [ 64 | 'serialize' => filter_var($query['serialize'] ?? false, \FILTER_VALIDATE_BOOL), 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Transport/Receiver/KeepaliveReceiverInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Receiver; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Exception\TransportException; 16 | 17 | interface KeepaliveReceiverInterface extends ReceiverInterface 18 | { 19 | /** 20 | * Informs the transport that the message is still being processed to avoid a timeout on the transport's side. 21 | * 22 | * @param int|null $seconds The minimum duration the message should be kept alive 23 | * 24 | * @throws TransportException If there is an issue communicating with the transport 25 | */ 26 | public function keepalive(Envelope $envelope, ?int $seconds = null): void; 27 | } 28 | -------------------------------------------------------------------------------- /Transport/Receiver/ListableReceiverInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Receiver; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | /** 17 | * Used when a receiver has the ability to list messages and find specific messages. 18 | * A receiver that implements this should add the TransportMessageIdStamp 19 | * to the Envelopes that it returns. 20 | * 21 | * @author Ryan Weaver 22 | */ 23 | interface ListableReceiverInterface extends ReceiverInterface 24 | { 25 | /** 26 | * Returns all the messages (up to the limit) in this receiver. 27 | * 28 | * Messages should be given the same stamps as when using ReceiverInterface::get(). 29 | * 30 | * @return Envelope[]|iterable 31 | */ 32 | public function all(?int $limit = null): iterable; 33 | 34 | /** 35 | * Returns the Envelope by id or none. 36 | * 37 | * Message should be given the same stamps as when using ReceiverInterface::get(). 38 | */ 39 | public function find(mixed $id): ?Envelope; 40 | } 41 | -------------------------------------------------------------------------------- /Transport/Receiver/MessageCountAwareInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Receiver; 13 | 14 | /** 15 | * @author Samuel Roze 16 | * @author Ryan Weaver 17 | */ 18 | interface MessageCountAwareInterface 19 | { 20 | /** 21 | * Returns the number of messages waiting to be handled. 22 | * 23 | * In some systems, this may be an approximate number. 24 | */ 25 | public function getMessageCount(): int; 26 | } 27 | -------------------------------------------------------------------------------- /Transport/Receiver/QueueReceiverInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Receiver; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | /** 17 | * Some transports may have multiple queues. This interface is used to read from only some queues. 18 | * 19 | * @author David Buchmann 20 | */ 21 | interface QueueReceiverInterface extends ReceiverInterface 22 | { 23 | /** 24 | * Get messages from the specified queue names instead of consuming from all queues. 25 | * 26 | * @param string[] $queueNames 27 | * 28 | * @return Envelope[] 29 | */ 30 | public function getFromQueues(array $queueNames): iterable; 31 | } 32 | -------------------------------------------------------------------------------- /Transport/Receiver/ReceiverInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Receiver; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Exception\TransportException; 16 | 17 | /** 18 | * @author Samuel Roze 19 | * @author Ryan Weaver 20 | */ 21 | interface ReceiverInterface 22 | { 23 | /** 24 | * Receives some messages. 25 | * 26 | * While this method could return an unlimited number of messages, 27 | * the intention is that it returns only one, or a "small number" 28 | * of messages each time. This gives the user more flexibility: 29 | * they can finish processing the one (or "small number") of messages 30 | * from this receiver and move on to check other receivers for messages. 31 | * If this method returns too many messages, it could cause a 32 | * blocking effect where handling the messages received from one 33 | * call to get() takes a long time, blocking other receivers from 34 | * being called. 35 | * 36 | * If applicable, the Envelope should contain a TransportMessageIdStamp. 37 | * 38 | * If a received message cannot be decoded, the message should not 39 | * be retried again (e.g. if there's a queue, it should be removed) 40 | * and a MessageDecodingFailedException should be thrown. 41 | * 42 | * @return iterable 43 | * 44 | * @throws TransportException If there is an issue communicating with the transport 45 | */ 46 | public function get(): iterable; 47 | 48 | /** 49 | * Acknowledges that the passed message was handled. 50 | * 51 | * @throws TransportException If there is an issue communicating with the transport 52 | */ 53 | public function ack(Envelope $envelope): void; 54 | 55 | /** 56 | * Called when handling the message failed and it should not be retried. 57 | * 58 | * @throws TransportException If there is an issue communicating with the transport 59 | */ 60 | public function reject(Envelope $envelope): void; 61 | } 62 | -------------------------------------------------------------------------------- /Transport/Receiver/SingleMessageReceiver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Receiver; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | /** 17 | * Receiver that decorates another, but receives only 1 specific message. 18 | * 19 | * @author Ryan Weaver 20 | * 21 | * @internal 22 | */ 23 | class SingleMessageReceiver implements ReceiverInterface 24 | { 25 | private bool $hasReceived = false; 26 | 27 | public function __construct( 28 | private ReceiverInterface $receiver, 29 | private Envelope $envelope, 30 | ) { 31 | } 32 | 33 | public function get(): iterable 34 | { 35 | if ($this->hasReceived) { 36 | return []; 37 | } 38 | 39 | $this->hasReceived = true; 40 | 41 | return [$this->envelope]; 42 | } 43 | 44 | public function ack(Envelope $envelope): void 45 | { 46 | $this->receiver->ack($envelope); 47 | } 48 | 49 | public function reject(Envelope $envelope): void 50 | { 51 | $this->receiver->reject($envelope); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Transport/Sender/SenderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Sender; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Exception\ExceptionInterface; 16 | 17 | /** 18 | * @author Samuel Roze 19 | */ 20 | interface SenderInterface 21 | { 22 | /** 23 | * Sends the given envelope. 24 | * 25 | * The sender can read different stamps for transport configuration, 26 | * like delivery delay. 27 | * 28 | * If applicable, the returned Envelope should contain a TransportMessageIdStamp. 29 | * 30 | * @throws ExceptionInterface 31 | */ 32 | public function send(Envelope $envelope): Envelope; 33 | } 34 | -------------------------------------------------------------------------------- /Transport/Sender/SendersLocator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Sender; 13 | 14 | use Psr\Container\ContainerInterface; 15 | use Symfony\Component\Messenger\Attribute\AsMessage; 16 | use Symfony\Component\Messenger\Envelope; 17 | use Symfony\Component\Messenger\Exception\RuntimeException; 18 | use Symfony\Component\Messenger\Handler\HandlersLocator; 19 | use Symfony\Component\Messenger\Stamp\TransportNamesStamp; 20 | 21 | /** 22 | * Maps a message to a list of senders. 23 | * 24 | * @author Fabien Potencier 25 | */ 26 | class SendersLocator implements SendersLocatorInterface 27 | { 28 | /** 29 | * @param array> $sendersMap An array, keyed by "type", set to an array of sender aliases 30 | * @param ContainerInterface $sendersLocator Locator of senders, keyed by sender alias 31 | */ 32 | public function __construct( 33 | private array $sendersMap, 34 | private ContainerInterface $sendersLocator, 35 | ) { 36 | } 37 | 38 | public function getSenders(Envelope $envelope): iterable 39 | { 40 | if ($envelope->all(TransportNamesStamp::class)) { 41 | foreach ($envelope->last(TransportNamesStamp::class)->getTransportNames() as $senderAlias) { 42 | yield from $this->getSenderFromAlias($senderAlias); 43 | } 44 | 45 | return; 46 | } 47 | 48 | $seen = []; 49 | $found = false; 50 | 51 | foreach (HandlersLocator::listTypes($envelope) as $type) { 52 | if (str_ends_with($type, '*') && $seen) { 53 | // the '*' acts as a fallback, if other senders already matched 54 | // with previous types, skip the senders bound to the fallback 55 | continue; 56 | } 57 | 58 | foreach ($this->sendersMap[$type] ?? [] as $senderAlias) { 59 | if (!\in_array($senderAlias, $seen, true)) { 60 | $seen[] = $senderAlias; 61 | 62 | yield from $this->getSenderFromAlias($senderAlias); 63 | $found = true; 64 | } 65 | } 66 | } 67 | 68 | // Let the configuration-driven map upper override message attributes, 69 | // this allows environment-specific configuration overriding hardcoded 70 | // transport name. 71 | if ($found) { 72 | return; 73 | } 74 | 75 | foreach ($this->getTransportNamesFromAttribute($envelope) as $senderAlias) { 76 | yield from $this->getSenderFromAlias($senderAlias); 77 | } 78 | } 79 | 80 | private function getTransportNamesFromAttribute(Envelope $envelope): array 81 | { 82 | $transports = []; 83 | $messageClass = $envelope->getMessage()::class; 84 | 85 | foreach ([$messageClass] + class_parents($messageClass) + class_implements($messageClass) as $class) { 86 | foreach ((new \ReflectionClass($class))->getAttributes(AsMessage::class, \ReflectionAttribute::IS_INSTANCEOF) as $refAttr) { 87 | $asMessage = $refAttr->newInstance(); 88 | 89 | if ($asMessage->transport) { 90 | $transports = array_merge($transports, (array) $asMessage->transport); 91 | } 92 | } 93 | } 94 | 95 | return $transports; 96 | } 97 | 98 | private function getSenderFromAlias(string $senderAlias): iterable 99 | { 100 | if (!$this->sendersLocator->has($senderAlias)) { 101 | throw new RuntimeException(\sprintf('Invalid senders configuration: sender "%s" is not in the senders locator.', $senderAlias)); 102 | } 103 | 104 | yield $senderAlias => $this->sendersLocator->get($senderAlias); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Transport/Sender/SendersLocatorInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Sender; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | 16 | /** 17 | * Maps a message to a list of senders. 18 | * 19 | * @author Samuel Roze 20 | * @author Tobias Schultze 21 | */ 22 | interface SendersLocatorInterface 23 | { 24 | /** 25 | * Gets the senders for the given message name. 26 | * 27 | * @return iterable Indexed by sender alias if available 28 | */ 29 | public function getSenders(Envelope $envelope): iterable; 30 | } 31 | -------------------------------------------------------------------------------- /Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Serialization\Normalizer; 13 | 14 | use Symfony\Component\ErrorHandler\Exception\FlattenException; 15 | use Symfony\Component\Messenger\Transport\Serialization\Serializer; 16 | use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; 17 | use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; 18 | use Symfony\Component\Serializer\Normalizer\NormalizerInterface; 19 | 20 | /** 21 | * This normalizer is only used in Debug/Dev/Messenger contexts. 22 | * 23 | * @author Pascal Luna 24 | */ 25 | final class FlattenExceptionNormalizer implements DenormalizerInterface, NormalizerInterface 26 | { 27 | use NormalizerAwareTrait; 28 | 29 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 30 | { 31 | return [ 32 | 'message' => $data->getMessage(), 33 | 'code' => $data->getCode(), 34 | 'headers' => $data->getHeaders(), 35 | 'class' => $data->getClass(), 36 | 'file' => $data->getFile(), 37 | 'line' => $data->getLine(), 38 | 'previous' => null === $data->getPrevious() ? null : $this->normalize($data->getPrevious(), $format, $context), 39 | 'status' => $data->getStatusCode(), 40 | 'status_text' => $data->getStatusText(), 41 | 'trace' => $data->getTrace(), 42 | 'trace_as_string' => $data->getTraceAsString(), 43 | ]; 44 | } 45 | 46 | public function getSupportedTypes(?string $format): array 47 | { 48 | return [ 49 | FlattenException::class => false, 50 | ]; 51 | } 52 | 53 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 54 | { 55 | return $data instanceof FlattenException && ($context[Serializer::MESSENGER_SERIALIZATION_CONTEXT] ?? false); 56 | } 57 | 58 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): FlattenException 59 | { 60 | $object = new FlattenException(); 61 | 62 | $object->setMessage($data['message']); 63 | $object->setCode($data['code']); 64 | $object->setStatusCode($data['status'] ?? 500); 65 | $object->setClass($data['class']); 66 | $object->setFile($data['file']); 67 | $object->setLine($data['line']); 68 | $object->setStatusText($data['status_text']); 69 | $object->setHeaders((array) $data['headers']); 70 | 71 | if (isset($data['previous'])) { 72 | $object->setPrevious($this->denormalize($data['previous'], $type, $format, $context)); 73 | } 74 | 75 | $property = new \ReflectionProperty(FlattenException::class, 'trace'); 76 | $property->setValue($object, (array) $data['trace']); 77 | 78 | $property = new \ReflectionProperty(FlattenException::class, 'traceAsString'); 79 | $property->setValue($object, $data['trace_as_string']); 80 | 81 | return $object; 82 | } 83 | 84 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 85 | { 86 | return FlattenException::class === $type && ($context[Serializer::MESSENGER_SERIALIZATION_CONTEXT] ?? false); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Transport/Serialization/PhpSerializer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Serialization; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Exception\MessageDecodingFailedException; 16 | use Symfony\Component\Messenger\Stamp\MessageDecodingFailedStamp; 17 | use Symfony\Component\Messenger\Stamp\NonSendableStampInterface; 18 | 19 | /** 20 | * @author Ryan Weaver 21 | */ 22 | class PhpSerializer implements SerializerInterface 23 | { 24 | private bool $acceptPhpIncompleteClass = false; 25 | 26 | /** 27 | * @internal 28 | */ 29 | public function acceptPhpIncompleteClass(): void 30 | { 31 | $this->acceptPhpIncompleteClass = true; 32 | } 33 | 34 | /** 35 | * @internal 36 | */ 37 | public function rejectPhpIncompleteClass(): void 38 | { 39 | $this->acceptPhpIncompleteClass = false; 40 | } 41 | 42 | public function decode(array $encodedEnvelope): Envelope 43 | { 44 | if (empty($encodedEnvelope['body'])) { 45 | throw new MessageDecodingFailedException('Encoded envelope should have at least a "body", or maybe you should implement your own serializer.'); 46 | } 47 | 48 | if (!str_ends_with($encodedEnvelope['body'], '}')) { 49 | $encodedEnvelope['body'] = base64_decode($encodedEnvelope['body']); 50 | } 51 | 52 | $serializeEnvelope = stripslashes($encodedEnvelope['body']); 53 | 54 | return $this->safelyUnserialize($serializeEnvelope); 55 | } 56 | 57 | public function encode(Envelope $envelope): array 58 | { 59 | $envelope = $envelope->withoutStampsOfType(NonSendableStampInterface::class); 60 | 61 | $body = addslashes(serialize($envelope)); 62 | 63 | if (!preg_match('//u', $body)) { 64 | $body = base64_encode($body); 65 | } 66 | 67 | return [ 68 | 'body' => $body, 69 | ]; 70 | } 71 | 72 | private function safelyUnserialize(string $contents): Envelope 73 | { 74 | if ('' === $contents) { 75 | throw new MessageDecodingFailedException('Could not decode an empty message using PHP serialization.'); 76 | } 77 | 78 | if ($this->acceptPhpIncompleteClass) { 79 | $prevUnserializeHandler = ini_set('unserialize_callback_func', null); 80 | } else { 81 | $prevUnserializeHandler = ini_set('unserialize_callback_func', self::class.'::handleUnserializeCallback'); 82 | } 83 | $prevErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler) { 84 | if (__FILE__ === $file && !\in_array($type, [\E_DEPRECATED, \E_USER_DEPRECATED], true)) { 85 | throw new \ErrorException($msg, 0, $type, $file, $line); 86 | } 87 | 88 | return $prevErrorHandler ? $prevErrorHandler($type, $msg, $file, $line, $context) : false; 89 | }); 90 | 91 | try { 92 | /** @var Envelope */ 93 | $envelope = unserialize($contents); 94 | } catch (\Throwable $e) { 95 | if ($e instanceof MessageDecodingFailedException) { 96 | throw $e; 97 | } 98 | 99 | throw new MessageDecodingFailedException('Could not decode Envelope: '.$e->getMessage(), 0, $e); 100 | } finally { 101 | restore_error_handler(); 102 | ini_set('unserialize_callback_func', $prevUnserializeHandler); 103 | } 104 | 105 | if (!$envelope instanceof Envelope) { 106 | throw new MessageDecodingFailedException('Could not decode message into an Envelope.'); 107 | } 108 | 109 | if ($envelope->getMessage() instanceof \__PHP_Incomplete_Class) { 110 | $envelope = $envelope->with(new MessageDecodingFailedStamp()); 111 | } 112 | 113 | return $envelope; 114 | } 115 | 116 | /** 117 | * @internal 118 | */ 119 | public static function handleUnserializeCallback(string $class): never 120 | { 121 | throw new MessageDecodingFailedException(\sprintf('Message class "%s" not found during decoding.', $class)); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Transport/Serialization/SerializerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Serialization; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Exception\MessageDecodingFailedException; 16 | 17 | /** 18 | * @author Samuel Roze 19 | */ 20 | interface SerializerInterface 21 | { 22 | /** 23 | * Decodes an envelope and its message from an encoded-form. 24 | * 25 | * The `$encodedEnvelope` parameter is a key-value array that 26 | * describes the envelope and its content, that will be used by the different transports. 27 | * 28 | * The most common keys are: 29 | * - `body` (string) - the message body 30 | * - `headers` (string) - a key/value pair of headers 31 | * 32 | * @throws MessageDecodingFailedException 33 | */ 34 | public function decode(array $encodedEnvelope): Envelope; 35 | 36 | /** 37 | * Encodes an envelope content (message & stamps) to a common format understandable by transports. 38 | * The encoded array should only contain scalars and arrays. 39 | * 40 | * Stamps that implement NonSendableStampInterface should 41 | * not be encoded. 42 | * 43 | * The most common keys of the encoded array are: 44 | * - `body` (string) - the message body 45 | * - `headers` (string) - a key/value pair of headers 46 | */ 47 | public function encode(Envelope $envelope): array; 48 | } 49 | -------------------------------------------------------------------------------- /Transport/SetupableTransportInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport; 13 | 14 | /** 15 | * @author Vincent Touzet 16 | */ 17 | interface SetupableTransportInterface 18 | { 19 | /** 20 | * Setup the transport. 21 | */ 22 | public function setup(): void; 23 | } 24 | -------------------------------------------------------------------------------- /Transport/Sync/SyncTransport.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Sync; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Exception\InvalidArgumentException; 16 | use Symfony\Component\Messenger\MessageBusInterface; 17 | use Symfony\Component\Messenger\Stamp\ReceivedStamp; 18 | use Symfony\Component\Messenger\Stamp\SentStamp; 19 | use Symfony\Component\Messenger\Transport\TransportInterface; 20 | 21 | /** 22 | * Transport that immediately marks messages as received and dispatches for handling. 23 | * 24 | * @author Ryan Weaver 25 | */ 26 | class SyncTransport implements TransportInterface 27 | { 28 | public function __construct( 29 | private MessageBusInterface $messageBus, 30 | ) { 31 | } 32 | 33 | public function get(): iterable 34 | { 35 | throw new InvalidArgumentException('You cannot receive messages from the Messenger SyncTransport.'); 36 | } 37 | 38 | public function ack(Envelope $envelope): void 39 | { 40 | throw new InvalidArgumentException('You cannot call ack() on the Messenger SyncTransport.'); 41 | } 42 | 43 | public function reject(Envelope $envelope): void 44 | { 45 | throw new InvalidArgumentException('You cannot call reject() on the Messenger SyncTransport.'); 46 | } 47 | 48 | public function send(Envelope $envelope): Envelope 49 | { 50 | /** @var SentStamp|null $sentStamp */ 51 | $sentStamp = $envelope->last(SentStamp::class); 52 | $alias = null === $sentStamp ? 'sync' : ($sentStamp->getSenderAlias() ?: $sentStamp->getSenderClass()); 53 | 54 | $envelope = $envelope->with(new ReceivedStamp($alias)); 55 | 56 | return $this->messageBus->dispatch($envelope); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Transport/Sync/SyncTransportFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport\Sync; 13 | 14 | use Symfony\Component\Messenger\MessageBusInterface; 15 | use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; 16 | use Symfony\Component\Messenger\Transport\TransportFactoryInterface; 17 | use Symfony\Component\Messenger\Transport\TransportInterface; 18 | 19 | /** 20 | * @author Ryan Weaver 21 | * 22 | * @implements TransportFactoryInterface 23 | */ 24 | class SyncTransportFactory implements TransportFactoryInterface 25 | { 26 | public function __construct( 27 | private MessageBusInterface $messageBus, 28 | ) { 29 | } 30 | 31 | public function createTransport(#[\SensitiveParameter] string $dsn, array $options, SerializerInterface $serializer): TransportInterface 32 | { 33 | return new SyncTransport($this->messageBus); 34 | } 35 | 36 | public function supports(#[\SensitiveParameter] string $dsn, array $options): bool 37 | { 38 | return str_starts_with($dsn, 'sync://'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Transport/TransportFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport; 13 | 14 | use Symfony\Component\Messenger\Exception\InvalidArgumentException; 15 | use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; 16 | 17 | /** 18 | * @author Samuel Roze 19 | * 20 | * @implements TransportFactoryInterface 21 | */ 22 | class TransportFactory implements TransportFactoryInterface 23 | { 24 | /** 25 | * @param iterable $factories 26 | */ 27 | public function __construct( 28 | private iterable $factories, 29 | ) { 30 | } 31 | 32 | public function createTransport(#[\SensitiveParameter] string $dsn, array $options, SerializerInterface $serializer): TransportInterface 33 | { 34 | foreach ($this->factories as $factory) { 35 | if ($factory->supports($dsn, $options)) { 36 | return $factory->createTransport($dsn, $options, $serializer); 37 | } 38 | } 39 | 40 | // Help the user to select Symfony packages based on protocol. 41 | $packageSuggestion = ''; 42 | if (str_starts_with($dsn, 'amqp://')) { 43 | $packageSuggestion = ' Run "composer require symfony/amqp-messenger" to install AMQP transport.'; 44 | } elseif (str_starts_with($dsn, 'doctrine://')) { 45 | $packageSuggestion = ' Run "composer require symfony/doctrine-messenger" to install Doctrine transport.'; 46 | } elseif (str_starts_with($dsn, 'redis://') || str_starts_with($dsn, 'rediss://')) { 47 | $packageSuggestion = ' Run "composer require symfony/redis-messenger" to install Redis transport.'; 48 | } elseif (str_starts_with($dsn, 'valkey://') || str_starts_with($dsn, 'valkeys://')) { 49 | $packageSuggestion = ' Run "composer require symfony/redis-messenger" to install Valkey transport.'; 50 | } elseif (str_starts_with($dsn, 'sqs://') || preg_match('#^https://sqs\.[\w\-]+\.amazonaws\.com/.+#', $dsn)) { 51 | $packageSuggestion = ' Run "composer require symfony/amazon-sqs-messenger" to install Amazon SQS transport.'; 52 | } elseif (str_starts_with($dsn, 'beanstalkd://')) { 53 | $packageSuggestion = ' Run "composer require symfony/beanstalkd-messenger" to install Beanstalkd transport.'; 54 | } 55 | 56 | if ($dsn = $this->santitizeDsn($dsn)) { 57 | throw new InvalidArgumentException(\sprintf('No transport supports Messenger DSN "%s".', $dsn).$packageSuggestion); 58 | } 59 | 60 | throw new InvalidArgumentException('No transport supports the given Messenger DSN.'.$packageSuggestion); 61 | } 62 | 63 | public function supports(#[\SensitiveParameter] string $dsn, array $options): bool 64 | { 65 | foreach ($this->factories as $factory) { 66 | if ($factory->supports($dsn, $options)) { 67 | return true; 68 | } 69 | } 70 | 71 | return false; 72 | } 73 | 74 | private function santitizeDsn(string $dsn): string 75 | { 76 | $parts = parse_url($dsn); 77 | $dsn = ''; 78 | 79 | if (isset($parts['scheme'])) { 80 | $dsn .= $parts['scheme'].'://'; 81 | } 82 | 83 | if (isset($parts['user']) && !isset($parts['pass'])) { 84 | $dsn .= '******'; 85 | } elseif (isset($parts['user'])) { 86 | $dsn .= $parts['user']; 87 | } 88 | 89 | if (isset($parts['pass'])) { 90 | $dsn .= ':******'; 91 | } 92 | 93 | if (isset($parts['host'])) { 94 | if (isset($parts['user'])) { 95 | $dsn .= '@'; 96 | } 97 | $dsn .= $parts['host']; 98 | } 99 | 100 | if (isset($parts['port'])) { 101 | $dsn .= ':'.$parts['port']; 102 | } 103 | 104 | if (isset($parts['path'])) { 105 | $dsn .= $parts['path']; 106 | } 107 | 108 | return $dsn; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Transport/TransportFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport; 13 | 14 | use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; 15 | 16 | /** 17 | * Creates a Messenger transport. 18 | * 19 | * @author Samuel Roze 20 | * 21 | * @template-covariant TTransport of TransportInterface 22 | */ 23 | interface TransportFactoryInterface 24 | { 25 | /** 26 | * @return TTransport 27 | */ 28 | public function createTransport(#[\SensitiveParameter] string $dsn, array $options, SerializerInterface $serializer): TransportInterface; 29 | 30 | public function supports(#[\SensitiveParameter] string $dsn, array $options): bool; 31 | } 32 | -------------------------------------------------------------------------------- /Transport/TransportInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger\Transport; 13 | 14 | use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; 15 | use Symfony\Component\Messenger\Transport\Sender\SenderInterface; 16 | 17 | /** 18 | * @author Nicolas Grekas 19 | */ 20 | interface TransportInterface extends ReceiverInterface, SenderInterface 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /WorkerMetadata.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Messenger; 13 | 14 | /** 15 | * @author Oleg Krasavin 16 | */ 17 | final class WorkerMetadata 18 | { 19 | public function __construct( 20 | private array $metadata, 21 | ) { 22 | } 23 | 24 | public function set(array $newMetadata): void 25 | { 26 | $this->metadata = array_merge($this->metadata, $newMetadata); 27 | } 28 | 29 | /** 30 | * Returns the queue names the worker consumes from, if "--queues" option was used. 31 | * Returns null otherwise. 32 | */ 33 | public function getQueueNames(): ?array 34 | { 35 | return $this->metadata['queueNames'] ?? null; 36 | } 37 | 38 | /** 39 | * Returns an array of unique identifiers for transport receivers the worker consumes from. 40 | */ 41 | public function getTransportNames(): array 42 | { 43 | return $this->metadata['transportNames'] ?? []; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/messenger", 3 | "type": "library", 4 | "description": "Helps applications send and receive messages to/from other applications or via message queues", 5 | "keywords": [], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Samuel Roze", 11 | "email": "samuel.roze@gmail.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2", 20 | "psr/log": "^1|^2|^3", 21 | "symfony/clock": "^6.4|^7.0", 22 | "symfony/deprecation-contracts": "^2.5|^3" 23 | }, 24 | "require-dev": { 25 | "psr/cache": "^1.0|^2.0|^3.0", 26 | "symfony/console": "^7.2", 27 | "symfony/dependency-injection": "^6.4|^7.0", 28 | "symfony/event-dispatcher": "^6.4|^7.0", 29 | "symfony/http-kernel": "^6.4|^7.0", 30 | "symfony/process": "^6.4|^7.0", 31 | "symfony/property-access": "^6.4|^7.0", 32 | "symfony/lock": "^6.4|^7.0", 33 | "symfony/rate-limiter": "^6.4|^7.0", 34 | "symfony/routing": "^6.4|^7.0", 35 | "symfony/serializer": "^6.4|^7.0", 36 | "symfony/service-contracts": "^2.5|^3", 37 | "symfony/stopwatch": "^6.4|^7.0", 38 | "symfony/validator": "^6.4|^7.0" 39 | }, 40 | "conflict": { 41 | "symfony/console": "<7.2", 42 | "symfony/event-dispatcher": "<6.4", 43 | "symfony/event-dispatcher-contracts": "<2.5", 44 | "symfony/framework-bundle": "<6.4", 45 | "symfony/http-kernel": "<6.4", 46 | "symfony/lock": "<6.4", 47 | "symfony/serializer": "<6.4" 48 | }, 49 | "autoload": { 50 | "psr-4": { "Symfony\\Component\\Messenger\\": "" }, 51 | "exclude-from-classmap": [ 52 | "/Tests/" 53 | ] 54 | }, 55 | "minimum-stability": "dev" 56 | } 57 | --------------------------------------------------------------------------------