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