Initial commit: Grav CMS setup with HTML reference material

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 23:38:26 +02:00
commit a9be15caf3
2261 changed files with 418989 additions and 0 deletions
@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Attribute;
/**
* Service tag to autoconfigure message handlers.
*
* @author Alireza Mirsepassi <alirezamirsepassi@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class AsMessageHandler
{
public function __construct(
public ?string $bus = null,
public ?string $fromTransport = null,
public ?string $handles = null,
public ?string $method = null,
public int $priority = 0,
) {
}
}
+211
View File
@@ -0,0 +1,211 @@
CHANGELOG
=========
5.4
---
* Add `AsMessageHandler` attribute for declaring message handlers on PHP 8.
* Add support for handling messages in batches with `BatchHandlerInterface` and corresponding trait
* Add `StopWorkerExceptionInterface` and its implementation `StopWorkerException` to stop the worker.
* Add support for resetting container services after each messenger message.
* Added `WorkerMetadata` class which allows you to access the configuration details of a worker, like `queueNames` and `transportNames` it consumes from.
* New method `getMetadata()` was added to `Worker` class which returns the `WorkerMetadata` object.
* Deprecate not setting the `reset_on_message` config option, its default value will change to `true` in 6.0
* Add log when worker should stop.
* Add log when `SIGTERM` is received.
5.3
---
* Add the `RouterContextMiddleware` to restore the original router context when handling a message
* `InMemoryTransport` can perform message serialization through dsn `in-memory://?serialize=true`.
* Added `queues` option to `Worker` to only fetch messages from a specific queue from a receiver implementing `QueueReceiverInterface`.
5.2.0
-----
* The `RedeliveryStamp` will no longer be populated with error data. This information is now stored in the `ErrorDetailsStamp` instead.
* Added `FlattenExceptionNormalizer` to give more information about the exception on Messenger background processes. The `FlattenExceptionNormalizer` has a higher priority than `ProblemNormalizer` and it is only used when the Messenger serialization context is set.
* Added factory methods `DelayStamp::delayFor(\DateInterval)` and `DelayStamp::delayUntil(\DateTimeInterface)`.
* Removed the exception when dispatching a message with a `DispatchAfterCurrentBusStamp` and not in a context of another dispatch call
* Added `WorkerMessageRetriedEvent`
5.1.0
-----
* Moved AmqpExt transport to package `symfony/amqp-messenger`. All classes in `Symfony\Component\Messenger\Transport\AmqpExt` have been moved to `Symfony\Component\Messenger\Bridge\Amqp\Transport`
* Moved Doctrine transport to package `symfony/doctrine-messenger`. All classes in `Symfony\Component\Messenger\Transport\Doctrine` have been moved to `Symfony\Component\Messenger\Bridge\Doctrine\Transport`
* Moved RedisExt transport to package `symfony/redis-messenger`. All classes in `Symfony\Component\Messenger\Transport\RedisExt` have been moved to `Symfony\Component\Messenger\Bridge\Redis\Transport`
* Added support for passing a `\Throwable` argument to `RetryStrategyInterface` methods. This allows to define strategies based on the reason of the handling failure.
* Added `StopWorkerOnFailureLimitListener` to stop the worker after a specified amount of failed messages is reached.
* Added `RecoverableExceptionInterface` interface to force retry.
5.0.0
-----
* The `LoggingMiddleware` class has been removed, pass a logger to `SendMessageMiddleware` instead.
* made `SendersLocator` require a `ContainerInterface` as 2nd argument
4.4.0
-----
* Added support for auto trimming of Redis streams.
* `InMemoryTransport` handle acknowledged and rejected messages.
* Made all dispatched worker event classes final.
* Added support for `from_transport` attribute on `messenger.message_handler` tag.
* Added support for passing `dbindex` as a query parameter to the redis transport DSN.
* Added `WorkerStartedEvent` and `WorkerRunningEvent`
* [BC BREAK] Removed `SendersLocatorInterface::getSenderByAlias` added in 4.3.
* [BC BREAK] Removed `$retryStrategies` argument from `Worker::__construct`.
* [BC BREAK] Changed arguments of `ConsumeMessagesCommand::__construct`.
* [BC BREAK] Removed `$senderClassOrAlias` argument from `RedeliveryStamp::__construct`.
* [BC BREAK] Removed `UnknownSenderException`.
* [BC BREAK] Removed `WorkerInterface`.
* [BC BREAK] Removed `$onHandledCallback` of `Worker::run(array $options = [], callable $onHandledCallback = null)`.
* [BC BREAK] Removed `StopWhenMemoryUsageIsExceededWorker` in favor of `StopWorkerOnMemoryLimitListener`.
* [BC BREAK] Removed `StopWhenMessageCountIsExceededWorker` in favor of `StopWorkerOnMessageLimitListener`.
* [BC BREAK] Removed `StopWhenTimeLimitIsReachedWorker` in favor of `StopWorkerOnTimeLimitListener`.
* [BC BREAK] Removed `StopWhenRestartSignalIsReceived` in favor of `StopWorkerOnRestartSignalListener`.
* The component is not marked as `@experimental` anymore.
* Marked the `MessengerDataCollector` class as `@final`.
* Added support for `DelayStamp` to the `redis` transport.
4.3.0
-----
* Added `NonSendableStampInterface` that a stamp can implement if
it should not be sent to a transport. Transport serializers
must now check for these stamps and not encode them.
* [BC BREAK] `SendersLocatorInterface` has an additional method:
`getSenderByAlias()`.
* Removed argument `?bool &$handle = false` from `SendersLocatorInterface::getSenders`
* A new `ListableReceiverInterface` was added, which a receiver
can implement (when applicable) to enable listing and fetching
individual messages by id (used in the new "Failed Messages" commands).
* Both `SenderInterface::send()` and `ReceiverInterface::get()`
should now (when applicable) add a `TransportMessageIdStamp`.
* Added `WorkerStoppedEvent` dispatched when a worker is stopped.
* Added optional `MessageCountAwareInterface` that receivers can implement
to give information about how many messages are waiting to be processed.
* [BC BREAK] The `Envelope::__construct()` signature changed:
you can no longer pass an unlimited number of stamps as the second,
third, fourth, arguments etc: stamps are now an array passed to the
second argument.
* [BC BREAK] The `MessageBusInterface::dispatch()` signature changed:
a second argument `array $stamps = []` was added.
* Added new `messenger:stop-workers` command that sends a signal
to stop all `messenger:consume` workers.
* [BC BREAK] The `TransportFactoryInterface::createTransport()` signature
changed: a required 3rd `SerializerInterface` argument was added.
* Added a new `SyncTransport` to explicitly handle messages synchronously.
* Added `AmqpStamp` allowing to provide a routing key, flags and attributes on message publishing.
* [BC BREAK] Removed publishing with a `routing_key` option from queue configuration, for
AMQP. Use exchange `default_publish_routing_key` or `AmqpStamp` instead.
* [BC BREAK] Changed the `queue` option in the AMQP transport DSN to be `queues[name]`. You can
therefore name the queue but also configure `binding_keys`, `flags` and `arguments`.
* [BC BREAK] The methods `get`, `ack`, `nack` and `queue` of the AMQP `Connection`
have a new argument: the queue name.
* Added optional parameter `prefetch_count` in connection configuration,
to setup channel prefetch count.
* New classes: `RoutableMessageBus`, `AddBusNameStampMiddleware`
and `BusNameStamp` were added, which allow you to add a bus identifier
to the `Envelope` then find the correct bus when receiving from
the transport. See `ConsumeMessagesCommand`.
* The optional `$busNames` constructor argument of the class `ConsumeMessagesCommand` was removed.
* [BC BREAK] 3 new methods were added to `ReceiverInterface`:
`ack()`, `reject()` and `get()`. The methods `receive()`
and `stop()` were removed.
* [BC BREAK] Error handling was moved from the receivers into
`Worker`. Implementations of `ReceiverInterface::handle()`
should now allow all exceptions to be thrown, except for transport
exceptions. They should also not retry (e.g. if there's a queue,
remove from the queue) if there is a problem decoding the message.
* [BC BREAK] `RejectMessageExceptionInterface` was removed and replaced
by `Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException`,
which has the same behavior: a message will not be retried
* The default command name for `ConsumeMessagesCommand` was
changed from `messenger:consume-messages` to `messenger:consume`
* `ConsumeMessagesCommand` has two new optional constructor arguments
* [BC BREAK] The first argument to Worker changed from a single
`ReceiverInterface` to an array of `ReceiverInterface`.
* `Worker` has 3 new optional constructor arguments.
* The `Worker` class now handles calling `pcntl_signal_dispatch()` the
receiver no longer needs to call this.
* The `AmqpSender` will now retry messages using a dead-letter exchange
and delayed queues, instead of retrying via `nack()`
* Senders now receive the `Envelope` with the `SentStamp` on it. Previously,
the `Envelope` was passed to the sender and *then* the `SentStamp`
was added.
* `SerializerInterface` implementations should now throw a
`Symfony\Component\Messenger\Exception\MessageDecodingFailedException`
if `decode()` fails for any reason.
* [BC BREAK] The default `Serializer` will now throw a
`MessageDecodingFailedException` if `decode()` fails, instead
of the underlying exceptions from the Serializer component.
* Added `PhpSerializer` which uses PHP's native `serialize()` and
`unserialize()` to serialize messages to a transport
* [BC BREAK] If no serializer were passed, the default serializer
changed from `Serializer` to `PhpSerializer` inside `AmqpReceiver`,
`AmqpSender`, `AmqpTransport` and `AmqpTransportFactory`.
* Added `TransportException` to mark an exception transport-related
* [BC BREAK] If listening to exceptions while using `AmqpSender` or `AmqpReceiver`, `\AMQPException` is
no longer thrown in favor of `TransportException`.
* Deprecated `LoggingMiddleware`, pass a logger to `SendMessageMiddleware` instead.
* [BC BREAK] `Connection::__construct()` and `Connection::fromDsn()`
both no longer have `$isDebug` arguments.
* [BC BREAK] The Amqp Transport now automatically sets up the exchanges
and queues by default. Previously, this was done when in "debug" mode
only. Pass the `auto_setup` connection option to control this.
* Added a `SetupTransportsCommand` command to setup the transports
* Added a Doctrine transport. For example, use the `doctrine://default` DSN (this uses the `default` Doctrine entity manager)
* [BC BREAK] The `getConnectionConfiguration` method on Amqp's `Connection` has been removed.
* [BC BREAK] A `HandlerFailedException` exception will be thrown if one or more handler fails.
* [BC BREAK] The `HandlersLocationInterface::getHandlers` method needs to return `HandlerDescriptor`
instances instead of callables.
* [BC BREAK] The `HandledStamp` stamp has changed: `handlerAlias` has been renamed to `handlerName`,
`getCallableName` has been removed and its constructor only has 2 arguments now.
* [BC BREAK] The `ReceivedStamp` needs to exposes the name of the transport from which the message
has been received.
4.2.0
-----
* Added `HandleTrait` leveraging a message bus instance to return a single
synchronous message handling result
* Added `HandledStamp` & `SentStamp` stamps
* All the changes below are BC BREAKS
* Senders and handlers subscribing to parent interfaces now receive *all* matching messages, wildcard included
* `MessageBusInterface::dispatch()`, `MiddlewareInterface::handle()` and `SenderInterface::send()` return `Envelope`
* `MiddlewareInterface::handle()` now require an `Envelope` as first argument and a `StackInterface` as second
* `EnvelopeAwareInterface` has been removed
* The signature of `Amqp*` classes changed to take a `Connection` as a first argument and an optional
`Serializer` as a second argument.
* `MessageSubscriberInterface::getHandledMessages()` return value has changed. The value of an array item
needs to be an associative array or the method name.
* `StampInterface` replaces `EnvelopeItemInterface` and doesn't extend `Serializable` anymore
* The `ConsumeMessagesCommand` class now takes an instance of `Psr\Container\ContainerInterface`
as first constructor argument
* The `EncoderInterface` and `DecoderInterface` have been replaced by a unified `Symfony\Component\Messenger\Transport\Serialization\SerializerInterface`.
* Renamed `EnvelopeItemInterface` to `StampInterface`
* `Envelope`'s constructor and `with()` method now accept `StampInterface` objects as variadic parameters
* Renamed and moved `ReceivedMessage`, `ValidationConfiguration` and `SerializerConfiguration` in the `Stamp` namespace
* Removed the `WrapIntoReceivedMessage` class
* `MessengerDataCollector::getMessages()` returns an iterable, not just an array anymore
* `HandlerLocatorInterface::resolve()` has been removed, use `HandlersLocator::getHandlers()` instead
* `SenderLocatorInterface::getSenderForMessage()` has been removed, use `SendersLocator::getSenders()` instead
* Classes in the `Middleware\Enhancers` sub-namespace have been moved to the `Middleware` one
* Classes in the `Asynchronous\Routing` sub-namespace have been moved to the `Transport\Sender\Locator` sub-namespace
* The `Asynchronous/Middleware/SendMessageMiddleware` class has been moved to the `Middleware` namespace
* `SenderInterface` has been moved to the `Transport\Sender` sub-namespace
* The `ChainHandler` and `ChainSender` classes have been removed
* `ReceiverInterface` and its implementations have been moved to the `Transport\Receiver` sub-namespace
* `ActivationMiddlewareDecorator` has been renamed `ActivationMiddleware`
* `AllowNoHandlerMiddleware` has been removed in favor of a new constructor argument on `HandleMessageMiddleware`
* The `ContainerHandlerLocator`, `AbstractHandlerLocator`, `SenderLocator` and `AbstractSenderLocator` classes have been removed
* `Envelope::all()` takes a new optional `$stampFqcn` argument and returns the stamps for the specified FQCN, or all stamps by their class name
* `Envelope::get()` has been renamed `Envelope::last()`
4.1.0
-----
* Introduced the component as experimental
@@ -0,0 +1,298 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Helper\Dumper;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
use Symfony\Component\VarDumper\Caster\Caster;
use Symfony\Component\VarDumper\Caster\TraceStub;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Cloner\Stub;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Contracts\Service\ServiceProviderInterface;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*
* @internal
*/
abstract class AbstractFailedMessagesCommand extends Command
{
protected const DEFAULT_TRANSPORT_OPTION = 'choose';
protected $failureTransports;
private $globalFailureReceiverName;
/**
* @param ServiceProviderInterface $failureTransports
*/
public function __construct(?string $globalFailureReceiverName, $failureTransports)
{
$this->failureTransports = $failureTransports;
if (!$failureTransports instanceof ServiceProviderInterface) {
trigger_deprecation('symfony/messenger', '5.3', 'Passing a receiver as 2nd argument to "%s()" is deprecated, pass a service locator instead.', __METHOD__);
if (null === $globalFailureReceiverName) {
throw new InvalidArgumentException(sprintf('The argument "globalFailureReceiver" from method "%s()" must be not null if 2nd argument is not a ServiceLocator.', __METHOD__));
}
$this->failureTransports = new ServiceLocator([$globalFailureReceiverName => static function () use ($failureTransports) { return $failureTransports; }]);
}
$this->globalFailureReceiverName = $globalFailureReceiverName;
parent::__construct();
}
protected function getReceiverName(): string
{
trigger_deprecation('symfony/messenger', '5.3', 'The method "%s()" is deprecated, use getGlobalFailureReceiverName() instead.', __METHOD__);
return $this->globalFailureReceiverName;
}
protected function getGlobalFailureReceiverName(): ?string
{
return $this->globalFailureReceiverName;
}
/**
* @return mixed
*/
protected function getMessageId(Envelope $envelope)
{
/** @var TransportMessageIdStamp $stamp */
$stamp = $envelope->last(TransportMessageIdStamp::class);
return null !== $stamp ? $stamp->getId() : null;
}
protected function displaySingleMessage(Envelope $envelope, SymfonyStyle $io)
{
$io->title('Failed Message Details');
/** @var SentToFailureTransportStamp|null $sentToFailureTransportStamp */
$sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class);
/** @var RedeliveryStamp|null $lastRedeliveryStamp */
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
$lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class);
$lastRedeliveryStampWithException = $this->getLastRedeliveryStampWithException($envelope, true);
$rows = [
['Class', \get_class($envelope->getMessage())],
];
if (null !== $id = $this->getMessageId($envelope)) {
$rows[] = ['Message Id', $id];
}
if (null === $sentToFailureTransportStamp) {
$io->warning('Message does not appear to have been sent to this transport after failing');
} else {
$failedAt = '';
$errorMessage = '';
$errorCode = '';
$errorClass = '(unknown)';
if (null !== $lastRedeliveryStamp) {
$failedAt = $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s');
}
if (null !== $lastErrorDetailsStamp) {
$errorMessage = $lastErrorDetailsStamp->getExceptionMessage();
$errorCode = $lastErrorDetailsStamp->getExceptionCode();
$errorClass = $lastErrorDetailsStamp->getExceptionClass();
} elseif (null !== $lastRedeliveryStampWithException) {
// Try reading the errorMessage for messages that are still in the queue without the new ErrorDetailStamps.
$errorMessage = $lastRedeliveryStampWithException->getExceptionMessage();
if (null !== $lastRedeliveryStampWithException->getFlattenException()) {
$errorClass = $lastRedeliveryStampWithException->getFlattenException()->getClass();
}
}
$rows = array_merge($rows, [
['Failed at', $failedAt],
['Error', $errorMessage],
['Error Code', $errorCode],
['Error Class', $errorClass],
['Transport', $sentToFailureTransportStamp->getOriginalReceiverName()],
]);
}
$io->table([], $rows);
/** @var RedeliveryStamp[] $redeliveryStamps */
$redeliveryStamps = $envelope->all(RedeliveryStamp::class);
$io->writeln(' Message history:');
foreach ($redeliveryStamps as $redeliveryStamp) {
$io->writeln(sprintf(' * Message failed at <info>%s</info> and was redelivered', $redeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s')));
}
$io->newLine();
if ($io->isVeryVerbose()) {
$io->title('Message:');
$dump = new Dumper($io, null, $this->createCloner());
$io->writeln($dump($envelope->getMessage()));
$io->title('Exception:');
$flattenException = null;
if (null !== $lastErrorDetailsStamp) {
$flattenException = $lastErrorDetailsStamp->getFlattenException();
} elseif (null !== $lastRedeliveryStampWithException) {
$flattenException = $lastRedeliveryStampWithException->getFlattenException();
}
$io->writeln(null === $flattenException ? '(no data)' : $dump($flattenException));
} else {
$io->writeln(' Re-run command with <info>-vv</info> to see more message & error details.');
}
}
protected function printPendingMessagesMessage(ReceiverInterface $receiver, SymfonyStyle $io)
{
if ($receiver instanceof MessageCountAwareInterface) {
if (1 === $receiver->getMessageCount()) {
$io->writeln('There is <comment>1</comment> message pending in the failure transport.');
} else {
$io->writeln(sprintf('There are <comment>%d</comment> messages pending in the failure transport.', $receiver->getMessageCount()));
}
}
}
/**
* @param string|null $name
*/
protected function getReceiver(/* ?string $name = null */): ReceiverInterface
{
if (1 > \func_num_args() && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) {
trigger_deprecation('symfony/messenger', '5.3', 'The "%s()" method will have a new "string $name" argument in version 6.0, not defining it is deprecated.', __METHOD__);
}
$name = \func_num_args() > 0 ? func_get_arg(0) : null;
if (null === $name = $name ?? $this->globalFailureReceiverName) {
throw new InvalidArgumentException(sprintf('No default failure transport is defined. Available transports are: "%s".', implode('", "', array_keys($this->failureTransports->getProvidedServices()))));
}
if (!$this->failureTransports->has($name)) {
throw new InvalidArgumentException(sprintf('The "%s" failure transport was not found. Available transports are: "%s".', $name, implode('", "', array_keys($this->failureTransports->getProvidedServices()))));
}
return $this->failureTransports->get($name);
}
protected function getLastRedeliveryStampWithException(Envelope $envelope): ?RedeliveryStamp
{
if (null === \func_get_args()[1]) {
trigger_deprecation('symfony/messenger', '5.2', sprintf('Using the "getLastRedeliveryStampWithException" method in the "%s" class is deprecated, use the "Envelope::last(%s)" instead.', self::class, ErrorDetailsStamp::class));
}
// Use ErrorDetailsStamp instead if it is available
if (null !== $envelope->last(ErrorDetailsStamp::class)) {
return null;
}
/** @var RedeliveryStamp $stamp */
foreach (array_reverse($envelope->all(RedeliveryStamp::class)) as $stamp) {
if (null !== $stamp->getExceptionMessage()) {
return $stamp;
}
}
return null;
}
private function createCloner(): ?ClonerInterface
{
if (!class_exists(VarCloner::class)) {
return null;
}
$cloner = new VarCloner();
$cloner->addCasters([FlattenException::class => function (FlattenException $flattenException, array $a, Stub $stub): array {
$stub->class = $flattenException->getClass();
return [
Caster::PREFIX_VIRTUAL.'message' => $flattenException->getMessage(),
Caster::PREFIX_VIRTUAL.'code' => $flattenException->getCode(),
Caster::PREFIX_VIRTUAL.'file' => $flattenException->getFile(),
Caster::PREFIX_VIRTUAL.'line' => $flattenException->getLine(),
Caster::PREFIX_VIRTUAL.'trace' => new TraceStub($flattenException->getTrace()),
];
}]);
return $cloner;
}
protected function printWarningAvailableFailureTransports(SymfonyStyle $io, ?string $failureTransportName): void
{
$failureTransports = array_keys($this->failureTransports->getProvidedServices());
$failureTransportsCount = \count($failureTransports);
if ($failureTransportsCount > 1) {
$io->writeln([
sprintf('> Loading messages from the <comment>global</comment> failure transport <comment>%s</comment>.', $failureTransportName),
'> To use a different failure transport, pass <comment>--transport=</comment>.',
sprintf('> Available failure transports are: <comment>%s</comment>', implode(', ', $failureTransports)),
"\n",
]);
}
}
protected function interactiveChooseFailureTransport(SymfonyStyle $io)
{
$failedTransports = array_keys($this->failureTransports->getProvidedServices());
$question = new ChoiceQuestion('Select failed transport:', $failedTransports, 0);
$question->setMultiselect(false);
return $io->askQuestion($question);
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestOptionValuesFor('transport')) {
$suggestions->suggestValues(array_keys($this->failureTransports->getProvidedServices()));
return;
}
if ($input->mustSuggestArgumentValuesFor('id')) {
$transport = $input->getOption('transport');
$transport = self::DEFAULT_TRANSPORT_OPTION === $transport ? $this->getGlobalFailureReceiverName() : $transport;
$receiver = $this->getReceiver($transport);
if (!$receiver instanceof ListableReceiverInterface) {
return;
}
$ids = [];
foreach ($receiver->all(50) as $envelope) {
$ids[] = $this->getMessageId($envelope);
}
$suggestions->suggestValues($ids);
return;
}
}
}
@@ -0,0 +1,276 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Messenger\EventListener\ResetServicesListener;
use Symfony\Component\Messenger\EventListener\StopWorkerOnFailureLimitListener;
use Symfony\Component\Messenger\EventListener\StopWorkerOnMemoryLimitListener;
use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener;
use Symfony\Component\Messenger\EventListener\StopWorkerOnTimeLimitListener;
use Symfony\Component\Messenger\RoutableMessageBus;
use Symfony\Component\Messenger\Worker;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class ConsumeMessagesCommand extends Command
{
protected static $defaultName = 'messenger:consume';
protected static $defaultDescription = 'Consume messages';
private $routableBus;
private $receiverLocator;
private $eventDispatcher;
private $logger;
private $receiverNames;
private $resetServicesListener;
private $busIds;
public function __construct(RoutableMessageBus $routableBus, ContainerInterface $receiverLocator, EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null, array $receiverNames = [], ?ResetServicesListener $resetServicesListener = null, array $busIds = [])
{
$this->routableBus = $routableBus;
$this->receiverLocator = $receiverLocator;
$this->eventDispatcher = $eventDispatcher;
$this->logger = $logger;
$this->receiverNames = $receiverNames;
$this->resetServicesListener = $resetServicesListener;
$this->busIds = $busIds;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$defaultReceiverName = 1 === \count($this->receiverNames) ? current($this->receiverNames) : null;
$this
->setDefinition([
new InputArgument('receivers', InputArgument::IS_ARRAY, 'Names of the receivers/transports to consume in order of priority', $defaultReceiverName ? [$defaultReceiverName] : []),
new InputOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limit the number of received messages'),
new InputOption('failure-limit', 'f', InputOption::VALUE_REQUIRED, 'The number of failed messages the worker can consume'),
new InputOption('memory-limit', 'm', InputOption::VALUE_REQUIRED, 'The memory limit the worker can consume'),
new InputOption('time-limit', 't', InputOption::VALUE_REQUIRED, 'The time limit in seconds the worker can handle new messages'),
new InputOption('sleep', null, InputOption::VALUE_REQUIRED, 'Seconds to sleep before asking for new messages after no messages were found', 1),
new InputOption('bus', 'b', InputOption::VALUE_REQUIRED, 'Name of the bus to which received messages should be dispatched (if not passed, bus is determined automatically)'),
new InputOption('queues', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Limit receivers to only consume from the specified queues'),
new InputOption('no-reset', null, InputOption::VALUE_NONE, 'Do not reset container services after each message'),
])
->setDescription(self::$defaultDescription)
->setHelp(<<<'EOF'
The <info>%command.name%</info> command consumes messages and dispatches them to the message bus.
<info>php %command.full_name% <receiver-name></info>
To receive from multiple transports, pass each name:
<info>php %command.full_name% receiver1 receiver2</info>
Use the --limit option to limit the number of messages received:
<info>php %command.full_name% <receiver-name> --limit=10</info>
Use the --failure-limit option to stop the worker when the given number of failed messages is reached:
<info>php %command.full_name% <receiver-name> --failure-limit=2</info>
Use the --memory-limit option to stop the worker if it exceeds a given memory usage limit. You can use shorthand byte values [K, M or G]:
<info>php %command.full_name% <receiver-name> --memory-limit=128M</info>
Use the --time-limit option to stop the worker when the given time limit (in seconds) is reached.
If a message is being handled, the worker will stop after the processing is finished:
<info>php %command.full_name% <receiver-name> --time-limit=3600</info>
Use the --bus option to specify the message bus to dispatch received messages
to instead of trying to determine it automatically. This is required if the
messages didn't originate from Messenger:
<info>php %command.full_name% <receiver-name> --bus=event_bus</info>
Use the --queues option to limit a receiver to only certain queues (only supported by some receivers):
<info>php %command.full_name% <receiver-name> --queues=fasttrack</info>
Use the --no-reset option to prevent services resetting after each message (may lead to leaking services' state between messages):
<info>php %command.full_name% <receiver-name> --no-reset</info>
EOF
)
;
}
/**
* {@inheritdoc}
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
if ($this->receiverNames && !$input->getArgument('receivers')) {
$io->block('Which transports/receivers do you want to consume?', null, 'fg=white;bg=blue', ' ', true);
$io->writeln('Choose which receivers you want to consume messages from in order of priority.');
if (\count($this->receiverNames) > 1) {
$io->writeln(sprintf('Hint: to consume from multiple, use a list of their names, e.g. <comment>%s</comment>', implode(', ', $this->receiverNames)));
}
$question = new ChoiceQuestion('Select receivers to consume:', $this->receiverNames, 0);
$question->setMultiselect(true);
$input->setArgument('receivers', $io->askQuestion($question));
}
if (!$input->getArgument('receivers')) {
throw new RuntimeException('Please pass at least one receiver.');
}
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$receivers = [];
foreach ($receiverNames = $input->getArgument('receivers') as $receiverName) {
if (!$this->receiverLocator->has($receiverName)) {
$message = sprintf('The receiver "%s" does not exist.', $receiverName);
if ($this->receiverNames) {
$message .= sprintf(' Valid receivers are: %s.', implode(', ', $this->receiverNames));
}
throw new RuntimeException($message);
}
$receivers[$receiverName] = $this->receiverLocator->get($receiverName);
}
if (null !== $this->resetServicesListener && !$input->getOption('no-reset')) {
$this->eventDispatcher->addSubscriber($this->resetServicesListener);
}
$stopsWhen = [];
if (null !== $limit = $input->getOption('limit')) {
if (!is_numeric($limit) || 0 >= $limit) {
throw new InvalidOptionException(sprintf('Option "limit" must be a positive integer, "%s" passed.', $limit));
}
$stopsWhen[] = "processed {$limit} messages";
$this->eventDispatcher->addSubscriber(new StopWorkerOnMessageLimitListener($limit, $this->logger));
}
if ($failureLimit = $input->getOption('failure-limit')) {
$stopsWhen[] = "reached {$failureLimit} failed messages";
$this->eventDispatcher->addSubscriber(new StopWorkerOnFailureLimitListener($failureLimit, $this->logger));
}
if ($memoryLimit = $input->getOption('memory-limit')) {
$stopsWhen[] = "exceeded {$memoryLimit} of memory";
$this->eventDispatcher->addSubscriber(new StopWorkerOnMemoryLimitListener($this->convertToBytes($memoryLimit), $this->logger));
}
if (null !== $timeLimit = $input->getOption('time-limit')) {
if (!is_numeric($timeLimit) || 0 >= $timeLimit) {
throw new InvalidOptionException(sprintf('Option "time-limit" must be a positive integer, "%s" passed.', $timeLimit));
}
$stopsWhen[] = "been running for {$timeLimit}s";
$this->eventDispatcher->addSubscriber(new StopWorkerOnTimeLimitListener($timeLimit, $this->logger));
}
$stopsWhen[] = 'received a stop signal via the messenger:stop-workers command';
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
$io->success(sprintf('Consuming messages from transport%s "%s".', \count($receivers) > 1 ? 's' : '', implode(', ', $receiverNames)));
if ($stopsWhen) {
$last = array_pop($stopsWhen);
$stopsWhen = ($stopsWhen ? implode(', ', $stopsWhen).' or ' : '').$last;
$io->comment("The worker will automatically exit once it has {$stopsWhen}.");
}
$io->comment('Quit the worker with CONTROL-C.');
if (OutputInterface::VERBOSITY_VERBOSE > $output->getVerbosity()) {
$io->comment('Re-run the command with a -vv option to see logs about consumed messages.');
}
$bus = $input->getOption('bus') ? $this->routableBus->getMessageBus($input->getOption('bus')) : $this->routableBus;
$worker = new Worker($receivers, $bus, $this->eventDispatcher, $this->logger);
$options = [
'sleep' => $input->getOption('sleep') * 1000000,
];
if ($queues = $input->getOption('queues')) {
$options['queues'] = $queues;
}
$worker->run($options);
return 0;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('receivers')) {
$suggestions->suggestValues(array_diff($this->receiverNames, array_diff($input->getArgument('receivers'), [$input->getCompletionValue()])));
return;
}
if ($input->mustSuggestOptionValuesFor('bus')) {
$suggestions->suggestValues($this->busIds);
}
}
private function convertToBytes(string $memoryLimit): int
{
$memoryLimit = strtolower($memoryLimit);
$max = ltrim($memoryLimit, '+');
if (str_starts_with($max, '0x')) {
$max = \intval($max, 16);
} elseif (str_starts_with($max, '0')) {
$max = \intval($max, 8);
} else {
$max = (int) $max;
}
switch (substr(rtrim($memoryLimit, 'b'), -1)) {
case 't': $max *= 1024;
// no break
case 'g': $max *= 1024;
// no break
case 'm': $max *= 1024;
// no break
case 'k': $max *= 1024;
}
return $max;
}
}
@@ -0,0 +1,150 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* A console command to debug Messenger information.
*
* @author Roland Franssen <franssen.roland@gmail.com>
*/
class DebugCommand extends Command
{
protected static $defaultName = 'debug:messenger';
protected static $defaultDescription = 'List messages you can dispatch using the message buses';
private $mapping;
public function __construct(array $mapping)
{
$this->mapping = $mapping;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->addArgument('bus', InputArgument::OPTIONAL, sprintf('The bus id (one of "%s")', implode('", "', array_keys($this->mapping))))
->setDescription(self::$defaultDescription)
->setHelp(<<<'EOF'
The <info>%command.name%</info> command displays all messages that can be
dispatched using the message buses:
<info>php %command.full_name%</info>
Or for a specific bus only:
<info>php %command.full_name% command_bus</info>
EOF
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$io->title('Messenger');
$mapping = $this->mapping;
if ($bus = $input->getArgument('bus')) {
if (!isset($mapping[$bus])) {
throw new RuntimeException(sprintf('Bus "%s" does not exist. Known buses are "%s".', $bus, implode('", "', array_keys($this->mapping))));
}
$mapping = [$bus => $mapping[$bus]];
}
foreach ($mapping as $bus => $handlersByMessage) {
$io->section($bus);
$tableRows = [];
foreach ($handlersByMessage as $message => $handlers) {
if ($description = self::getClassDescription($message)) {
$tableRows[] = [sprintf('<comment>%s</>', $description)];
}
$tableRows[] = [sprintf('<fg=cyan>%s</fg=cyan>', $message)];
foreach ($handlers as $handler) {
$tableRows[] = [
sprintf(' handled by <info>%s</>', $handler[0]).$this->formatConditions($handler[1]),
];
if ($handlerDescription = self::getClassDescription($handler[0])) {
$tableRows[] = [sprintf(' <comment>%s</>', $handlerDescription)];
}
}
$tableRows[] = [''];
}
if ($tableRows) {
$io->text('The following messages can be dispatched:');
$io->newLine();
$io->table([], $tableRows);
} else {
$io->warning(sprintf('No handled message found in bus "%s".', $bus));
}
}
return 0;
}
private function formatConditions(array $options): string
{
if (!$options) {
return '';
}
$optionsMapping = [];
foreach ($options as $key => $value) {
$optionsMapping[] = $key.'='.$value;
}
return ' (when '.implode(', ', $optionsMapping).')';
}
private static function getClassDescription(string $class): string
{
try {
$r = new \ReflectionClass($class);
if ($docComment = $r->getDocComment()) {
$docComment = preg_split('#\n\s*\*\s*[\n@]#', substr($docComment, 3, -2), 2)[0];
return trim(preg_replace('#\s*\n\s*\*\s*#', ' ', $docComment));
}
} catch (\ReflectionException $e) {
}
return '';
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('bus')) {
$suggestions->suggestValues(array_keys($this->mapping));
}
}
}
@@ -0,0 +1,104 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class FailedMessagesRemoveCommand extends AbstractFailedMessagesCommand
{
protected static $defaultName = 'messenger:failed:remove';
protected static $defaultDescription = 'Remove given messages from the failure transport';
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDefinition([
new InputArgument('id', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'Specific message id(s) to remove'),
new InputOption('force', null, InputOption::VALUE_NONE, 'Force the operation without confirmation'),
new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
new InputOption('show-messages', null, InputOption::VALUE_NONE, 'Display messages before removing it (if multiple ids are given)'),
])
->setDescription(self::$defaultDescription)
->setHelp(<<<'EOF'
The <info>%command.name%</info> removes given messages that are pending in the failure transport.
<info>php %command.full_name% {id1} [{id2} ...]</info>
The specific ids can be found via the messenger:failed:show command.
EOF
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
$failureTransportName = $input->getOption('transport');
if (self::DEFAULT_TRANSPORT_OPTION === $failureTransportName) {
$failureTransportName = $this->getGlobalFailureReceiverName();
}
$receiver = $this->getReceiver($failureTransportName);
$shouldForce = $input->getOption('force');
$ids = (array) $input->getArgument('id');
$shouldDisplayMessages = $input->getOption('show-messages') || 1 === \count($ids);
$this->removeMessages($failureTransportName, $ids, $receiver, $io, $shouldForce, $shouldDisplayMessages);
return 0;
}
private function removeMessages(string $failureTransportName, array $ids, ReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce, bool $shouldDisplayMessages): void
{
if (!$receiver instanceof ListableReceiverInterface) {
throw new RuntimeException(sprintf('The "%s" receiver does not support removing specific messages.', $failureTransportName));
}
foreach ($ids as $id) {
$envelope = $receiver->find($id);
if (null === $envelope) {
$io->error(sprintf('The message with id "%s" was not found.', $id));
continue;
}
if ($shouldDisplayMessages) {
$this->displaySingleMessage($envelope, $io);
}
if ($shouldForce || $io->confirm('Do you want to permanently remove this message?', false)) {
$receiver->reject($envelope);
$io->success(sprintf('Message with id %s removed.', $id));
} else {
$io->note(sprintf('Message with id %s not removed.', $id));
}
}
}
}
@@ -0,0 +1,226 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener;
use Symfony\Component\Messenger\Exception\LogicException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
use Symfony\Component\Messenger\Transport\Receiver\SingleMessageReceiver;
use Symfony\Component\Messenger\Worker;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class FailedMessagesRetryCommand extends AbstractFailedMessagesCommand
{
protected static $defaultName = 'messenger:failed:retry';
protected static $defaultDescription = 'Retry one or more messages from the failure transport';
private $eventDispatcher;
private $messageBus;
private $logger;
public function __construct(?string $globalReceiverName, $failureTransports, MessageBusInterface $messageBus, EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null)
{
$this->eventDispatcher = $eventDispatcher;
$this->messageBus = $messageBus;
$this->logger = $logger;
parent::__construct($globalReceiverName, $failureTransports);
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDefinition([
new InputArgument('id', InputArgument::IS_ARRAY, 'Specific message id(s) to retry'),
new InputOption('force', null, InputOption::VALUE_NONE, 'Force action without confirmation'),
new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
])
->setDescription(self::$defaultDescription)
->setHelp(<<<'EOF'
The <info>%command.name%</info> retries message in the failure transport.
<info>php %command.full_name%</info>
The command will interactively ask if each message should be retried
or discarded.
Some transports support retrying a specific message id, which comes
from the <info>messenger:failed:show</info> command.
<info>php %command.full_name% {id}</info>
Or pass multiple ids at once to process multiple messages:
<info>php %command.full_name% {id1} {id2} {id3}</info>
EOF
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->eventDispatcher->addSubscriber(new StopWorkerOnMessageLimitListener(1));
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
$io->comment('Quit this command with CONTROL-C.');
if (!$output->isVeryVerbose()) {
$io->comment('Re-run the command with a -vv option to see logs about consumed messages.');
}
$failureTransportName = $input->getOption('transport');
if (self::DEFAULT_TRANSPORT_OPTION === $failureTransportName) {
$this->printWarningAvailableFailureTransports($io, $this->getGlobalFailureReceiverName());
}
if ('' === $failureTransportName || null === $failureTransportName) {
$failureTransportName = $this->interactiveChooseFailureTransport($io);
}
$failureTransportName = self::DEFAULT_TRANSPORT_OPTION === $failureTransportName ? $this->getGlobalFailureReceiverName() : $failureTransportName;
$receiver = $this->getReceiver($failureTransportName);
$this->printPendingMessagesMessage($receiver, $io);
$io->writeln(sprintf('To retry all the messages, run <comment>messenger:consume %s</comment>', $failureTransportName));
$shouldForce = $input->getOption('force');
$ids = $input->getArgument('id');
if (0 === \count($ids)) {
if (!$input->isInteractive()) {
throw new RuntimeException('Message id must be passed when in non-interactive mode.');
}
$this->runInteractive($failureTransportName, $io, $shouldForce);
return 0;
}
$this->retrySpecificIds($failureTransportName, $ids, $io, $shouldForce);
$io->success('All done!');
return 0;
}
private function runInteractive(string $failureTransportName, SymfonyStyle $io, bool $shouldForce)
{
$receiver = $this->failureTransports->get($failureTransportName);
$count = 0;
if ($receiver instanceof ListableReceiverInterface) {
// for listable receivers, find the messages one-by-one
// this avoids using get(), which for some less-robust
// transports (like Doctrine), will cause the message
// to be temporarily "acked", even if the user aborts
// handling the message
while (true) {
$ids = [];
foreach ($receiver->all(1) as $envelope) {
++$count;
$id = $this->getMessageId($envelope);
if (null === $id) {
throw new LogicException(sprintf('The "%s" receiver is able to list messages by id but the envelope is missing the TransportMessageIdStamp stamp.', $failureTransportName));
}
$ids[] = $id;
}
// break the loop if all messages are consumed
if (0 === \count($ids)) {
break;
}
$this->retrySpecificIds($failureTransportName, $ids, $io, $shouldForce);
}
} else {
// get() and ask messages one-by-one
$count = $this->runWorker($failureTransportName, $receiver, $io, $shouldForce);
}
// avoid success message if nothing was processed
if (1 <= $count) {
$io->success('All failed messages have been handled or removed!');
}
}
private function runWorker(string $failureTransportName, ReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce): int
{
$count = 0;
$listener = function (WorkerMessageReceivedEvent $messageReceivedEvent) use ($io, $receiver, $shouldForce, &$count) {
++$count;
$envelope = $messageReceivedEvent->getEnvelope();
$this->displaySingleMessage($envelope, $io);
$shouldHandle = $shouldForce || $io->confirm('Do you want to retry (yes) or delete this message (no)?');
if ($shouldHandle) {
return;
}
$messageReceivedEvent->shouldHandle(false);
$receiver->reject($envelope);
};
$this->eventDispatcher->addListener(WorkerMessageReceivedEvent::class, $listener);
$worker = new Worker(
[$failureTransportName => $receiver],
$this->messageBus,
$this->eventDispatcher,
$this->logger
);
try {
$worker->run();
} finally {
$this->eventDispatcher->removeListener(WorkerMessageReceivedEvent::class, $listener);
}
return $count;
}
private function retrySpecificIds(string $failureTransportName, array $ids, SymfonyStyle $io, bool $shouldForce)
{
$receiver = $this->getReceiver($failureTransportName);
if (!$receiver instanceof ListableReceiverInterface) {
throw new RuntimeException(sprintf('The "%s" receiver does not support retrying messages by id.', $failureTransportName));
}
foreach ($ids as $id) {
$envelope = $receiver->find($id);
if (null === $envelope) {
throw new RuntimeException(sprintf('The message "%s" was not found.', $id));
}
$singleReceiver = new SingleMessageReceiver($receiver, $envelope);
$this->runWorker($failureTransportName, $singleReceiver, $io, $shouldForce);
}
}
}
@@ -0,0 +1,153 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class FailedMessagesShowCommand extends AbstractFailedMessagesCommand
{
protected static $defaultName = 'messenger:failed:show';
protected static $defaultDescription = 'Show one or more messages from the failure transport';
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDefinition([
new InputArgument('id', InputArgument::OPTIONAL, 'Specific message id to show'),
new InputOption('max', null, InputOption::VALUE_REQUIRED, 'Maximum number of messages to list', 50),
new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
])
->setDescription(self::$defaultDescription)
->setHelp(<<<'EOF'
The <info>%command.name%</info> shows message that are pending in the failure transport.
<info>php %command.full_name%</info>
Or look at a specific message by its id:
<info>php %command.full_name% {id}</info>
EOF
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
$failureTransportName = $input->getOption('transport');
if (self::DEFAULT_TRANSPORT_OPTION === $failureTransportName) {
$this->printWarningAvailableFailureTransports($io, $this->getGlobalFailureReceiverName());
}
if ('' === $failureTransportName || null === $failureTransportName) {
$failureTransportName = $this->interactiveChooseFailureTransport($io);
}
$failureTransportName = self::DEFAULT_TRANSPORT_OPTION === $failureTransportName ? $this->getGlobalFailureReceiverName() : $failureTransportName;
$receiver = $this->getReceiver($failureTransportName);
$this->printPendingMessagesMessage($receiver, $io);
if (!$receiver instanceof ListableReceiverInterface) {
throw new RuntimeException(sprintf('The "%s" receiver does not support listing or showing specific messages.', $failureTransportName));
}
if (null === $id = $input->getArgument('id')) {
$this->listMessages($failureTransportName, $io, $input->getOption('max'));
} else {
$this->showMessage($failureTransportName, $id, $io);
}
return 0;
}
private function listMessages(?string $failedTransportName, SymfonyStyle $io, int $max)
{
/** @var ListableReceiverInterface $receiver */
$receiver = $this->getReceiver($failedTransportName);
$envelopes = $receiver->all($max);
$rows = [];
foreach ($envelopes as $envelope) {
/** @var RedeliveryStamp|null $lastRedeliveryStamp */
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
$lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class);
$lastRedeliveryStampWithException = $this->getLastRedeliveryStampWithException($envelope, true);
$errorMessage = '';
if (null !== $lastErrorDetailsStamp) {
$errorMessage = $lastErrorDetailsStamp->getExceptionMessage();
} elseif (null !== $lastRedeliveryStampWithException) {
// Try reading the errorMessage for messages that are still in the queue without the new ErrorDetailStamps.
$errorMessage = $lastRedeliveryStampWithException->getExceptionMessage();
}
$rows[] = [
$this->getMessageId($envelope),
\get_class($envelope->getMessage()),
null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s'),
$errorMessage,
];
}
if (0 === \count($rows)) {
$io->success('No failed messages were found.');
return;
}
$io->table(['Id', 'Class', 'Failed at', 'Error'], $rows);
if (\count($rows) === $max) {
$io->comment(sprintf('Showing first %d messages.', $max));
}
$io->comment(sprintf('Run <comment>messenger:failed:show {id} --transport=%s -vv</comment> to see message details.', $failedTransportName));
}
private function showMessage(?string $failedTransportName, string $id, SymfonyStyle $io)
{
/** @var ListableReceiverInterface $receiver */
$receiver = $this->getReceiver($failedTransportName);
$envelope = $receiver->find($id);
if (null === $envelope) {
throw new RuntimeException(sprintf('The message "%s" was not found.', $id));
}
$this->displaySingleMessage($envelope, $io);
$io->writeln([
'',
sprintf(' Run <comment>messenger:failed:retry %s --transport=%s</comment> to retry this message.', $id, $failedTransportName),
sprintf(' Run <comment>messenger:failed:remove %s --transport=%s</comment> to delete it.', $id, $failedTransportName),
]);
}
}
@@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\Transport\SetupableTransportInterface;
/**
* @author Vincent Touzet <vincent.touzet@gmail.com>
*/
class SetupTransportsCommand extends Command
{
protected static $defaultName = 'messenger:setup-transports';
protected static $defaultDescription = 'Prepare the required infrastructure for the transport';
private $transportLocator;
private $transportNames;
public function __construct(ContainerInterface $transportLocator, array $transportNames = [])
{
$this->transportLocator = $transportLocator;
$this->transportNames = $transportNames;
parent::__construct();
}
protected function configure()
{
$this
->addArgument('transport', InputArgument::OPTIONAL, 'Name of the transport to setup', null)
->setDescription(self::$defaultDescription)
->setHelp(<<<EOF
The <info>%command.name%</info> command setups the transports:
<info>php %command.full_name%</info>
Or a specific transport only:
<info>php %command.full_name% <transport></info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$transportNames = $this->transportNames;
// do we want to set up only one transport?
if ($transport = $input->getArgument('transport')) {
if (!$this->transportLocator->has($transport)) {
throw new \RuntimeException(sprintf('The "%s" transport does not exist.', $transport));
}
$transportNames = [$transport];
}
foreach ($transportNames as $id => $transportName) {
$transport = $this->transportLocator->get($transportName);
if ($transport instanceof SetupableTransportInterface) {
$transport->setup();
$io->success(sprintf('The "%s" transport was set up successfully.', $transportName));
} else {
$io->note(sprintf('The "%s" transport does not support setup.', $transportName));
}
}
return 0;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('transport')) {
$suggestions->suggestValues($this->transportNames);
return;
}
}
}
@@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Command;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\EventListener\StopWorkerOnRestartSignalListener;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class StopWorkersCommand extends Command
{
protected static $defaultName = 'messenger:stop-workers';
protected static $defaultDescription = 'Stop workers after their current message';
private $restartSignalCachePool;
public function __construct(CacheItemPoolInterface $restartSignalCachePool)
{
$this->restartSignalCachePool = $restartSignalCachePool;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDefinition([])
->setDescription(self::$defaultDescription)
->setHelp(<<<'EOF'
The <info>%command.name%</info> command sends a signal to stop any <info>messenger:consume</info> processes that are running.
<info>php %command.full_name%</info>
Each worker command will finish the message they are currently processing
and then exit. Worker commands are *not* automatically restarted: that
should be handled by a process control system.
EOF
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
$cacheItem = $this->restartSignalCachePool->getItem(StopWorkerOnRestartSignalListener::RESTART_REQUESTED_TIMESTAMP_KEY);
$cacheItem->set(microtime(true));
$this->restartSignalCachePool->save($cacheItem);
$io->success('Signal successfully sent to stop any running workers.');
return 0;
}
}
@@ -0,0 +1,149 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\Messenger\TraceableMessageBus;
use Symfony\Component\VarDumper\Caster\ClassStub;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*
* @final
*/
class MessengerDataCollector extends DataCollector implements LateDataCollectorInterface
{
private $traceableBuses = [];
public function registerBus(string $name, TraceableMessageBus $bus)
{
$this->traceableBuses[$name] = $bus;
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, ?\Throwable $exception = null)
{
// Noop. Everything is collected live by the traceable buses & cloned as late as possible.
}
/**
* {@inheritdoc}
*/
public function lateCollect()
{
$this->data = ['messages' => [], 'buses' => array_keys($this->traceableBuses)];
$messages = [];
foreach ($this->traceableBuses as $busName => $bus) {
foreach ($bus->getDispatchedMessages() as $message) {
$debugRepresentation = $this->cloneVar($this->collectMessage($busName, $message));
$messages[] = [$debugRepresentation, $message['callTime']];
}
}
// Order by call time
usort($messages, function ($a, $b) { return $a[1] <=> $b[1]; });
// Keep the messages clones only
$this->data['messages'] = array_column($messages, 0);
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'messenger';
}
/**
* {@inheritdoc}
*/
public function reset()
{
$this->data = [];
foreach ($this->traceableBuses as $traceableBus) {
$traceableBus->reset();
}
}
/**
* {@inheritdoc}
*/
protected function getCasters(): array
{
$casters = parent::getCasters();
// Unset the default caster truncating collectors data.
unset($casters['*']);
return $casters;
}
private function collectMessage(string $busName, array $tracedMessage)
{
$message = $tracedMessage['message'];
$debugRepresentation = [
'bus' => $busName,
'stamps' => $tracedMessage['stamps'] ?? null,
'stamps_after_dispatch' => $tracedMessage['stamps_after_dispatch'] ?? null,
'message' => [
'type' => new ClassStub(\get_class($message)),
'value' => $message,
],
'caller' => $tracedMessage['caller'],
];
if (isset($tracedMessage['exception'])) {
$exception = $tracedMessage['exception'];
$debugRepresentation['exception'] = [
'type' => \get_class($exception),
'value' => $exception,
];
}
return $debugRepresentation;
}
public function getExceptionsCount(?string $bus = null): int
{
$count = 0;
foreach ($this->getMessages($bus) as $message) {
$count += (int) isset($message['exception']);
}
return $count;
}
public function getMessages(?string $bus = null): array
{
if (null === $bus) {
return $this->data['messages'];
}
return array_filter($this->data['messages'], function ($message) use ($bus) {
return $bus === $message['bus'];
});
}
public function getBuses(): array
{
return $this->data['buses'];
}
}
@@ -0,0 +1,400 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\DependencyInjection;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Messenger\Handler\HandlerDescriptor;
use Symfony\Component\Messenger\Handler\HandlersLocator;
use Symfony\Component\Messenger\Handler\MessageSubscriberInterface;
use Symfony\Component\Messenger\TraceableMessageBus;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class MessengerPass implements CompilerPassInterface
{
private $handlerTag;
private $busTag;
private $receiverTag;
public function __construct(string $handlerTag = 'messenger.message_handler', string $busTag = 'messenger.bus', string $receiverTag = 'messenger.receiver')
{
if (0 < \func_num_args()) {
trigger_deprecation('symfony/messenger', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->handlerTag = $handlerTag;
$this->busTag = $busTag;
$this->receiverTag = $receiverTag;
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$busIds = [];
foreach ($container->findTaggedServiceIds($this->busTag) as $busId => $tags) {
$busIds[] = $busId;
if ($container->hasParameter($busMiddlewareParameter = $busId.'.middleware')) {
$this->registerBusMiddleware($container, $busId, $container->getParameter($busMiddlewareParameter));
$container->getParameterBag()->remove($busMiddlewareParameter);
}
if ($container->hasDefinition('data_collector.messenger')) {
$this->registerBusToCollector($container, $busId);
}
}
if ($container->hasDefinition('messenger.receiver_locator')) {
$this->registerReceivers($container, $busIds);
}
$this->registerHandlers($container, $busIds);
}
private function registerHandlers(ContainerBuilder $container, array $busIds)
{
$definitions = [];
$handlersByBusAndMessage = [];
$handlerToOriginalServiceIdMapping = [];
foreach ($container->findTaggedServiceIds($this->handlerTag, true) as $serviceId => $tags) {
foreach ($tags as $tag) {
if (isset($tag['bus']) && !\in_array($tag['bus'], $busIds, true)) {
throw new RuntimeException(sprintf('Invalid handler service "%s": bus "%s" specified on the tag "%s" does not exist (known ones are: "%s").', $serviceId, $tag['bus'], $this->handlerTag, implode('", "', $busIds)));
}
$className = $this->getServiceClass($container, $serviceId);
$r = $container->getReflectionClass($className);
if (null === $r) {
throw new RuntimeException(sprintf('Invalid service "%s": class "%s" does not exist.', $serviceId, $className));
}
if (isset($tag['handles'])) {
$handles = isset($tag['method']) ? [$tag['handles'] => $tag['method']] : [$tag['handles']];
} else {
$handles = $this->guessHandledClasses($r, $serviceId);
}
$message = null;
$handlerBuses = (array) ($tag['bus'] ?? $busIds);
foreach ($handles as $message => $options) {
$buses = $handlerBuses;
if (\is_int($message)) {
if (\is_string($options)) {
$message = $options;
$options = [];
} else {
throw new RuntimeException(sprintf('The handler configuration needs to return an array of messages or an associated array of message and configuration. Found value of type "%s" at position "%d" for service "%s".', get_debug_type($options), $message, $serviceId));
}
}
if (\is_string($options)) {
$options = ['method' => $options];
}
$options += array_filter($tag);
unset($options['handles']);
$priority = $options['priority'] ?? 0;
$method = $options['method'] ?? '__invoke';
if (isset($options['bus'])) {
if (!\in_array($options['bus'], $busIds)) {
$messageLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : ($r->implementsInterface(MessageSubscriberInterface::class) ? sprintf('returned by method "%s::getHandledMessages()"', $r->getName()) : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method));
throw new RuntimeException(sprintf('Invalid configuration '.$messageLocation.' for message "%s": bus "%s" does not exist.', $message, $options['bus']));
}
$buses = [$options['bus']];
}
if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) {
$messageLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : ($r->implementsInterface(MessageSubscriberInterface::class) ? sprintf('returned by method "%s::getHandledMessages()"', $r->getName()) : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method));
throw new RuntimeException(sprintf('Invalid handler service "%s": class or interface "%s" '.$messageLocation.' not found.', $serviceId, $message));
}
if (!$r->hasMethod($method)) {
throw new RuntimeException(sprintf('Invalid handler service "%s": method "%s::%s()" does not exist.', $serviceId, $r->getName(), $method));
}
if ('__invoke' !== $method) {
$wrapperDefinition = (new Definition('Closure'))->addArgument([new Reference($serviceId), $method])->setFactory('Closure::fromCallable');
$definitions[$definitionId = '.messenger.method_on_object_wrapper.'.ContainerBuilder::hash($message.':'.$priority.':'.$serviceId.':'.$method)] = $wrapperDefinition;
} else {
$definitionId = $serviceId;
}
$handlerToOriginalServiceIdMapping[$definitionId] = $serviceId;
foreach ($buses as $handlerBus) {
$handlersByBusAndMessage[$handlerBus][$message][$priority][] = [$definitionId, $options];
}
}
if (null === $message) {
throw new RuntimeException(sprintf('Invalid handler service "%s": method "%s::getHandledMessages()" must return one or more messages.', $serviceId, $r->getName()));
}
}
}
foreach ($handlersByBusAndMessage as $bus => $handlersByMessage) {
foreach ($handlersByMessage as $message => $handlersByPriority) {
krsort($handlersByPriority);
$handlersByBusAndMessage[$bus][$message] = array_merge(...$handlersByPriority);
}
}
$handlersLocatorMappingByBus = [];
foreach ($handlersByBusAndMessage as $bus => $handlersByMessage) {
foreach ($handlersByMessage as $message => $handlers) {
$handlerDescriptors = [];
foreach ($handlers as $handler) {
$definitions[$definitionId = '.messenger.handler_descriptor.'.ContainerBuilder::hash($bus.':'.$message.':'.$handler[0])] = (new Definition(HandlerDescriptor::class))->setArguments([new Reference($handler[0]), $handler[1]]);
$handlerDescriptors[] = new Reference($definitionId);
}
$handlersLocatorMappingByBus[$bus][$message] = new IteratorArgument($handlerDescriptors);
}
}
$container->addDefinitions($definitions);
foreach ($busIds as $bus) {
$container->register($locatorId = $bus.'.messenger.handlers_locator', HandlersLocator::class)
->setArgument(0, $handlersLocatorMappingByBus[$bus] ?? [])
;
if ($container->has($handleMessageId = $bus.'.middleware.handle_message')) {
$container->getDefinition($handleMessageId)
->replaceArgument(0, new Reference($locatorId))
;
}
}
if ($container->hasDefinition('console.command.messenger_debug')) {
$debugCommandMapping = $handlersByBusAndMessage;
foreach ($busIds as $bus) {
if (!isset($debugCommandMapping[$bus])) {
$debugCommandMapping[$bus] = [];
}
foreach ($debugCommandMapping[$bus] as $message => $handlers) {
foreach ($handlers as $key => $handler) {
$debugCommandMapping[$bus][$message][$key][0] = $handlerToOriginalServiceIdMapping[$handler[0]];
}
}
}
$container->getDefinition('console.command.messenger_debug')->replaceArgument(0, $debugCommandMapping);
}
}
private function guessHandledClasses(\ReflectionClass $handlerClass, string $serviceId): iterable
{
if ($handlerClass->implementsInterface(MessageSubscriberInterface::class)) {
return $handlerClass->getName()::getHandledMessages();
}
try {
$method = $handlerClass->getMethod('__invoke');
} catch (\ReflectionException $e) {
throw new RuntimeException(sprintf('Invalid handler service "%s": class "%s" must have an "__invoke()" method.', $serviceId, $handlerClass->getName()));
}
if (0 === $method->getNumberOfRequiredParameters()) {
throw new RuntimeException(sprintf('Invalid handler service "%s": method "%s::__invoke()" requires at least one argument, first one being the message it handles.', $serviceId, $handlerClass->getName()));
}
$parameters = $method->getParameters();
if (!$type = $parameters[0]->getType()) {
throw new RuntimeException(sprintf('Invalid handler service "%s": argument "$%s" of method "%s::__invoke()" must have a type-hint corresponding to the message class it handles.', $serviceId, $parameters[0]->getName(), $handlerClass->getName()));
}
if ($type instanceof \ReflectionUnionType) {
$types = [];
$invalidTypes = [];
foreach ($type->getTypes() as $type) {
if (!$type->isBuiltin()) {
$types[] = (string) $type;
} else {
$invalidTypes[] = (string) $type;
}
}
if ($types) {
return $types;
}
throw new RuntimeException(sprintf('Invalid handler service "%s": type-hint of argument "$%s" in method "%s::__invoke()" must be a class , "%s" given.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), implode('|', $invalidTypes)));
}
if ($type->isBuiltin()) {
throw new RuntimeException(sprintf('Invalid handler service "%s": type-hint of argument "$%s" in method "%s::__invoke()" must be a class , "%s" given.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type));
}
return [$type->getName()];
}
private function registerReceivers(ContainerBuilder $container, array $busIds)
{
$receiverMapping = [];
$failureTransportsMap = [];
if ($container->hasDefinition('console.command.messenger_failed_messages_retry')) {
$commandDefinition = $container->getDefinition('console.command.messenger_failed_messages_retry');
$globalReceiverName = $commandDefinition->getArgument(0);
if (null !== $globalReceiverName) {
if ($container->hasAlias('messenger.failure_transports.default')) {
$failureTransportsMap[$globalReceiverName] = new Reference('messenger.failure_transports.default');
} else {
$failureTransportsMap[$globalReceiverName] = new Reference('messenger.transport.'.$globalReceiverName);
}
}
}
foreach ($container->findTaggedServiceIds($this->receiverTag) as $id => $tags) {
$receiverClass = $this->getServiceClass($container, $id);
if (!is_subclass_of($receiverClass, ReceiverInterface::class)) {
throw new RuntimeException(sprintf('Invalid receiver "%s": class "%s" must implement interface "%s".', $id, $receiverClass, ReceiverInterface::class));
}
$receiverMapping[$id] = new Reference($id);
foreach ($tags as $tag) {
if (isset($tag['alias'])) {
$receiverMapping[$tag['alias']] = $receiverMapping[$id];
if ($tag['is_failure_transport'] ?? false) {
$failureTransportsMap[$tag['alias']] = $receiverMapping[$id];
}
}
}
}
$receiverNames = [];
foreach ($receiverMapping as $name => $reference) {
$receiverNames[(string) $reference] = $name;
}
$buses = [];
foreach ($busIds as $busId) {
$buses[$busId] = new Reference($busId);
}
if ($hasRoutableMessageBus = $container->hasDefinition('messenger.routable_message_bus')) {
$container->getDefinition('messenger.routable_message_bus')
->replaceArgument(0, ServiceLocatorTagPass::register($container, $buses));
}
if ($container->hasDefinition('console.command.messenger_consume_messages')) {
$consumeCommandDefinition = $container->getDefinition('console.command.messenger_consume_messages');
if ($hasRoutableMessageBus) {
$consumeCommandDefinition->replaceArgument(0, new Reference('messenger.routable_message_bus'));
}
$consumeCommandDefinition->replaceArgument(4, array_values($receiverNames));
try {
$consumeCommandDefinition->replaceArgument(6, $busIds);
} catch (OutOfBoundsException $e) {
// ignore to preserve compatibility with symfony/framework-bundle < 5.4
}
}
if ($container->hasDefinition('console.command.messenger_setup_transports')) {
$container->getDefinition('console.command.messenger_setup_transports')
->replaceArgument(1, array_values($receiverNames));
}
$container->getDefinition('messenger.receiver_locator')->replaceArgument(0, $receiverMapping);
$failureTransportsLocator = ServiceLocatorTagPass::register($container, $failureTransportsMap);
$failedCommandIds = [
'console.command.messenger_failed_messages_retry',
'console.command.messenger_failed_messages_show',
'console.command.messenger_failed_messages_remove',
];
foreach ($failedCommandIds as $failedCommandId) {
if ($container->hasDefinition($failedCommandId)) {
$definition = $container->getDefinition($failedCommandId);
$definition->replaceArgument(1, $failureTransportsLocator);
}
}
}
private function registerBusToCollector(ContainerBuilder $container, string $busId)
{
$container->setDefinition(
$tracedBusId = 'debug.traced.'.$busId,
(new Definition(TraceableMessageBus::class, [new Reference($tracedBusId.'.inner')]))->setDecoratedService($busId)
);
$container->getDefinition('data_collector.messenger')->addMethodCall('registerBus', [$busId, new Reference($tracedBusId)]);
}
private function registerBusMiddleware(ContainerBuilder $container, string $busId, array $middlewareCollection)
{
$middlewareReferences = [];
foreach ($middlewareCollection as $middlewareItem) {
$id = $middlewareItem['id'];
$arguments = $middlewareItem['arguments'] ?? [];
if (!$container->has($messengerMiddlewareId = 'messenger.middleware.'.$id)) {
$messengerMiddlewareId = $id;
}
if (!$container->has($messengerMiddlewareId)) {
throw new RuntimeException(sprintf('Invalid middleware: service "%s" not found.', $id));
}
if ($container->findDefinition($messengerMiddlewareId)->isAbstract()) {
$childDefinition = new ChildDefinition($messengerMiddlewareId);
$childDefinition->setArguments($arguments);
if (isset($middlewareReferences[$messengerMiddlewareId = $busId.'.middleware.'.$id])) {
$messengerMiddlewareId .= '.'.ContainerBuilder::hash($arguments);
}
$container->setDefinition($messengerMiddlewareId, $childDefinition);
} elseif ($arguments) {
throw new RuntimeException(sprintf('Invalid middleware factory "%s": a middleware factory must be an abstract definition.', $id));
}
$middlewareReferences[$messengerMiddlewareId] = new Reference($messengerMiddlewareId);
}
$container->getDefinition($busId)->replaceArgument(0, new IteratorArgument(array_values($middlewareReferences)));
}
private function getServiceClass(ContainerBuilder $container, string $serviceId): string
{
while (true) {
$definition = $container->findDefinition($serviceId);
if (!$definition->getClass() && $definition instanceof ChildDefinition) {
$serviceId = $definition->getParent();
continue;
}
return $definition->getClass();
}
}
}
+131
View File
@@ -0,0 +1,131 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger;
use Symfony\Component\Messenger\Stamp\StampInterface;
/**
* A message wrapped in an envelope with stamps (configurations, markers, ...).
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
final class Envelope
{
/**
* @var array<string, list<StampInterface>>
*/
private $stamps = [];
private $message;
/**
* @param StampInterface[] $stamps
*/
public function __construct(object $message, array $stamps = [])
{
$this->message = $message;
foreach ($stamps as $stamp) {
$this->stamps[\get_class($stamp)][] = $stamp;
}
}
/**
* Makes sure the message is in an Envelope and adds the given stamps.
*
* @param object|Envelope $message
* @param StampInterface[] $stamps
*/
public static function wrap(object $message, array $stamps = []): self
{
$envelope = $message instanceof self ? $message : new self($message);
return $envelope->with(...$stamps);
}
/**
* @return static A new Envelope instance with additional stamp
*/
public function with(StampInterface ...$stamps): self
{
$cloned = clone $this;
foreach ($stamps as $stamp) {
$cloned->stamps[\get_class($stamp)][] = $stamp;
}
return $cloned;
}
/**
* @return static A new Envelope instance without any stamps of the given class
*/
public function withoutAll(string $stampFqcn): self
{
$cloned = clone $this;
unset($cloned->stamps[$this->resolveAlias($stampFqcn)]);
return $cloned;
}
/**
* Removes all stamps that implement the given type.
*/
public function withoutStampsOfType(string $type): self
{
$cloned = clone $this;
$type = $this->resolveAlias($type);
foreach ($cloned->stamps as $class => $stamps) {
if ($class === $type || is_subclass_of($class, $type)) {
unset($cloned->stamps[$class]);
}
}
return $cloned;
}
public function last(string $stampFqcn): ?StampInterface
{
return isset($this->stamps[$stampFqcn = $this->resolveAlias($stampFqcn)]) ? end($this->stamps[$stampFqcn]) : null;
}
/**
* @return StampInterface[]|StampInterface[][] The stamps for the specified FQCN, or all stamps by their class name
*/
public function all(?string $stampFqcn = null): array
{
if (null !== $stampFqcn) {
return $this->stamps[$this->resolveAlias($stampFqcn)] ?? [];
}
return $this->stamps;
}
/**
* @return object The original message contained in the envelope
*/
public function getMessage(): object
{
return $this->message;
}
/**
* BC to be removed in 6.0.
*/
private function resolveAlias(string $fqcn): string
{
static $resolved;
return $resolved[$fqcn] ?? ($resolved[$fqcn] = class_exists($fqcn) ? (new \ReflectionClass($fqcn))->getName() : $fqcn);
}
}
@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\StampInterface;
abstract class AbstractWorkerMessageEvent
{
private $envelope;
private $receiverName;
public function __construct(Envelope $envelope, string $receiverName)
{
$this->envelope = $envelope;
$this->receiverName = $receiverName;
}
public function getEnvelope(): Envelope
{
return $this->envelope;
}
/**
* Returns a unique identifier for transport receiver this message was received from.
*/
public function getReceiverName(): string
{
return $this->receiverName;
}
public function addStamps(StampInterface ...$stamps): void
{
$this->envelope = $this->envelope->with(...$stamps);
}
}
@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
use Symfony\Component\Messenger\Envelope;
/**
* Event is dispatched before a message is sent to the transport.
*
* The event is *only* dispatched if the message will actually
* be sent to at least one transport. If the message is sent
* to multiple transports, the message is dispatched only one time.
* This message is only dispatched the first time a message
* is sent to a transport, not also if it is retried.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
final class SendMessageToTransportsEvent
{
private $envelope;
public function __construct(Envelope $envelope)
{
$this->envelope = $envelope;
}
public function getEnvelope(): Envelope
{
return $this->envelope;
}
public function setEnvelope(Envelope $envelope)
{
$this->envelope = $envelope;
}
}
@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
use Symfony\Component\Messenger\Envelope;
/**
* Dispatched when a message was received from a transport and handling failed.
*
* The event name is the class name.
*/
final class WorkerMessageFailedEvent extends AbstractWorkerMessageEvent
{
private $throwable;
private $willRetry = false;
public function __construct(Envelope $envelope, string $receiverName, \Throwable $error)
{
$this->throwable = $error;
parent::__construct($envelope, $receiverName);
}
public function getThrowable(): \Throwable
{
return $this->throwable;
}
public function willRetry(): bool
{
return $this->willRetry;
}
public function setForRetry(): void
{
$this->willRetry = true;
}
}
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
/**
* Dispatched after a message was received from a transport and successfully handled.
*
* The event name is the class name.
*/
final class WorkerMessageHandledEvent extends AbstractWorkerMessageEvent
{
}
@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
/**
* Dispatched when a message was received from a transport but before sent to the bus.
*
* The event name is the class name.
*/
final class WorkerMessageReceivedEvent extends AbstractWorkerMessageEvent
{
private $shouldHandle = true;
public function shouldHandle(?bool $shouldHandle = null): bool
{
if (null !== $shouldHandle) {
$this->shouldHandle = $shouldHandle;
}
return $this->shouldHandle;
}
}
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
/**
* Dispatched after a message has been sent for retry.
*
* The event name is the class name.
*/
final class WorkerMessageRetriedEvent extends AbstractWorkerMessageEvent
{
}
@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
use Symfony\Component\Messenger\Worker;
/**
* Dispatched after the worker processed a message or didn't receive a message at all.
*
* @author Tobias Schultze <http://tobion.de>
*/
final class WorkerRunningEvent
{
private $worker;
private $isWorkerIdle;
public function __construct(Worker $worker, bool $isWorkerIdle)
{
$this->worker = $worker;
$this->isWorkerIdle = $isWorkerIdle;
}
public function getWorker(): Worker
{
return $this->worker;
}
/**
* Returns true when no message has been received by the worker.
*/
public function isWorkerIdle(): bool
{
return $this->isWorkerIdle;
}
}
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
use Symfony\Component\Messenger\Worker;
/**
* Dispatched when a worker has been started.
*
* @author Tobias Schultze <http://tobion.de>
*/
final class WorkerStartedEvent
{
private $worker;
public function __construct(Worker $worker)
{
$this->worker = $worker;
}
public function getWorker(): Worker
{
return $this->worker;
}
}
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Event;
use Symfony\Component\Messenger\Worker;
/**
* Dispatched when a worker has been stopped.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
final class WorkerStoppedEvent
{
private $worker;
public function __construct(Worker $worker)
{
$this->worker = $worker;
}
public function getWorker(): Worker
{
return $this->worker;
}
}
@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
final class AddErrorDetailsStampListener implements EventSubscriberInterface
{
public function onMessageFailed(WorkerMessageFailedEvent $event): void
{
$stamp = ErrorDetailsStamp::create($event->getThrowable());
$previousStamp = $event->getEnvelope()->last(ErrorDetailsStamp::class);
// Do not append duplicate information
if (null === $previousStamp || !$previousStamp->equals($stamp)) {
$event->addStamps($stamp);
}
}
public static function getSubscribedEvents(): array
{
return [
// must have higher priority than SendFailedMessageForRetryListener
WorkerMessageFailedEvent::class => ['onMessageFailed', 200],
];
}
}
@@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
/**
* @author Tobias Schultze <http://tobion.de>
*/
class DispatchPcntlSignalListener implements EventSubscriberInterface
{
public function onWorkerRunning(): void
{
pcntl_signal_dispatch();
}
public static function getSubscribedEvents()
{
if (!\function_exists('pcntl_signal_dispatch')) {
return [];
}
return [
WorkerRunningEvent::class => ['onWorkerRunning', 100],
];
}
}
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Event\WorkerStoppedEvent;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class ResetServicesListener implements EventSubscriberInterface
{
private $servicesResetter;
public function __construct(ServicesResetter $servicesResetter)
{
$this->servicesResetter = $servicesResetter;
}
public function resetServices(WorkerRunningEvent $event): void
{
if (!$event->isWorkerIdle()) {
$this->servicesResetter->reset();
}
}
public function resetServicesAtStop(WorkerStoppedEvent $event): void
{
$this->servicesResetter->reset();
}
public static function getSubscribedEvents(): array
{
return [
WorkerRunningEvent::class => ['resetServices', -1024],
WorkerStoppedEvent::class => ['resetServicesAtStop', -1024],
];
}
}
@@ -0,0 +1,173 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerMessageRetriedEvent;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\RecoverableExceptionInterface;
use Symfony\Component\Messenger\Exception\RuntimeException;
use Symfony\Component\Messenger\Exception\UnrecoverableExceptionInterface;
use Symfony\Component\Messenger\Retry\RetryStrategyInterface;
use Symfony\Component\Messenger\Stamp\DelayStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\StampInterface;
use Symfony\Component\Messenger\Transport\Sender\SenderInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* @author Tobias Schultze <http://tobion.de>
*/
class SendFailedMessageForRetryListener implements EventSubscriberInterface
{
private $sendersLocator;
private $retryStrategyLocator;
private $logger;
private $eventDispatcher;
private $historySize;
public function __construct(ContainerInterface $sendersLocator, ContainerInterface $retryStrategyLocator, ?LoggerInterface $logger = null, ?EventDispatcherInterface $eventDispatcher = null, int $historySize = 10)
{
$this->sendersLocator = $sendersLocator;
$this->retryStrategyLocator = $retryStrategyLocator;
$this->logger = $logger;
$this->eventDispatcher = $eventDispatcher;
$this->historySize = $historySize;
}
public function onMessageFailed(WorkerMessageFailedEvent $event)
{
$retryStrategy = $this->getRetryStrategyForTransport($event->getReceiverName());
$envelope = $event->getEnvelope();
$throwable = $event->getThrowable();
$message = $envelope->getMessage();
$context = [
'class' => \get_class($message),
];
$shouldRetry = $retryStrategy && $this->shouldRetry($throwable, $envelope, $retryStrategy);
$retryCount = RedeliveryStamp::getRetryCountFromEnvelope($envelope);
if ($shouldRetry) {
$event->setForRetry();
++$retryCount;
$delay = $retryStrategy->getWaitingTime($envelope, $throwable);
if (null !== $this->logger) {
$this->logger->warning('Error thrown while handling message {class}. Sending for retry #{retryCount} using {delay} ms delay. Error: "{error}"', $context + ['retryCount' => $retryCount, 'delay' => $delay, 'error' => $throwable->getMessage(), 'exception' => $throwable]);
}
// add the delay and retry stamp info
$retryEnvelope = $this->withLimitedHistory($envelope, new DelayStamp($delay), new RedeliveryStamp($retryCount));
// re-send the message for retry
$retryEnvelope = $this->getSenderForTransport($event->getReceiverName())->send($retryEnvelope);
if (null !== $this->eventDispatcher) {
$this->eventDispatcher->dispatch(new WorkerMessageRetriedEvent($retryEnvelope, $event->getReceiverName()));
}
} else {
if (null !== $this->logger) {
$this->logger->critical('Error thrown while handling message {class}. Removing from transport after {retryCount} retries. Error: "{error}"', $context + ['retryCount' => $retryCount, 'error' => $throwable->getMessage(), 'exception' => $throwable]);
}
}
}
/**
* Adds stamps to the envelope by keeping only the First + Last N stamps.
*/
private function withLimitedHistory(Envelope $envelope, StampInterface ...$stamps): Envelope
{
foreach ($stamps as $stamp) {
$history = $envelope->all(\get_class($stamp));
if (\count($history) < $this->historySize) {
$envelope = $envelope->with($stamp);
continue;
}
$history = array_merge(
[$history[0]],
\array_slice($history, -$this->historySize + 2),
[$stamp]
);
$envelope = $envelope->withoutAll(\get_class($stamp))->with(...$history);
}
return $envelope;
}
public static function getSubscribedEvents()
{
return [
// must have higher priority than SendFailedMessageToFailureTransportListener
WorkerMessageFailedEvent::class => ['onMessageFailed', 100],
];
}
private function shouldRetry(\Throwable $e, Envelope $envelope, RetryStrategyInterface $retryStrategy): bool
{
if ($e instanceof RecoverableExceptionInterface) {
return true;
}
// if one or more nested Exceptions is an instance of RecoverableExceptionInterface we should retry
// if ALL nested Exceptions are an instance of UnrecoverableExceptionInterface we should not retry
if ($e instanceof HandlerFailedException) {
$shouldNotRetry = true;
foreach ($e->getNestedExceptions() as $nestedException) {
if ($nestedException instanceof RecoverableExceptionInterface) {
return true;
}
if (!$nestedException instanceof UnrecoverableExceptionInterface) {
$shouldNotRetry = false;
break;
}
}
if ($shouldNotRetry) {
return false;
}
}
if ($e instanceof UnrecoverableExceptionInterface) {
return false;
}
return $retryStrategy->isRetryable($envelope, $e);
}
private function getRetryStrategyForTransport(string $alias): ?RetryStrategyInterface
{
if ($this->retryStrategyLocator->has($alias)) {
return $this->retryStrategyLocator->get($alias);
}
return null;
}
private function getSenderForTransport(string $alias): SenderInterface
{
if ($this->sendersLocator->has($alias)) {
return $this->sendersLocator->get($alias);
}
throw new RuntimeException(sprintf('Could not find sender "%s" based on the same receiver to send the failed message to for retry.', $alias));
}
}
@@ -0,0 +1,104 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Stamp\DelayStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Transport\Sender\SenderInterface;
/**
* Sends a rejected message to a "failure transport".
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class SendFailedMessageToFailureTransportListener implements EventSubscriberInterface
{
private $failureSenders;
private $logger;
/**
* @param ContainerInterface $failureSenders
*/
public function __construct($failureSenders, ?LoggerInterface $logger = null)
{
if (!$failureSenders instanceof ContainerInterface) {
trigger_deprecation('symfony/messenger', '5.3', 'Passing a SenderInterface value as 1st argument to "%s()" is deprecated, pass a ServiceLocator instead.', __METHOD__);
}
$this->failureSenders = $failureSenders;
$this->logger = $logger;
}
public function onMessageFailed(WorkerMessageFailedEvent $event)
{
if ($event->willRetry()) {
return;
}
if (!$this->hasFailureTransports($event)) {
return;
}
$failureSender = $this->getFailureSender($event->getReceiverName());
if (null === $failureSender) {
return;
}
$envelope = $event->getEnvelope();
// avoid re-sending to the failed sender
if (null !== $envelope->last(SentToFailureTransportStamp::class)) {
return;
}
$envelope = $envelope->with(
new SentToFailureTransportStamp($event->getReceiverName()),
new DelayStamp(0),
new RedeliveryStamp(0)
);
if (null !== $this->logger) {
$this->logger->info('Rejected message {class} will be sent to the failure transport {transport}.', [
'class' => \get_class($envelope->getMessage()),
'transport' => \get_class($failureSender),
]);
}
$failureSender->send($envelope);
}
public static function getSubscribedEvents()
{
return [
WorkerMessageFailedEvent::class => ['onMessageFailed', -100],
];
}
private function getFailureSender(string $receiverName): SenderInterface
{
if ($this->failureSenders instanceof SenderInterface) {
return $this->failureSenders;
}
return $this->failureSenders->get($receiverName);
}
private function hasFailureTransports(WorkerMessageFailedEvent $event): bool
{
return ($this->failureSenders instanceof ContainerInterface && $this->failureSenders->has($event->getReceiverName())) || $this->failureSenders instanceof SenderInterface;
}
}
@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\StopWorkerExceptionInterface;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class StopWorkerOnCustomStopExceptionListener implements EventSubscriberInterface
{
private $stop = false;
public function onMessageFailed(WorkerMessageFailedEvent $event): void
{
$th = $event->getThrowable();
if ($th instanceof StopWorkerExceptionInterface) {
$this->stop = true;
}
if ($th instanceof HandlerFailedException) {
foreach ($th->getNestedExceptions() as $e) {
if ($e instanceof StopWorkerExceptionInterface) {
$this->stop = true;
break;
}
}
}
}
public function onWorkerRunning(WorkerRunningEvent $event): void
{
if ($this->stop) {
$event->getWorker()->stop();
}
}
public static function getSubscribedEvents(): array
{
return [
WorkerMessageFailedEvent::class => 'onMessageFailed',
WorkerRunningEvent::class => 'onWorkerRunning',
];
}
}
@@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
/**
* @author Michel Hunziker <info@michelhunziker.com>
*/
class StopWorkerOnFailureLimitListener implements EventSubscriberInterface
{
private $maximumNumberOfFailures;
private $logger;
private $failedMessages = 0;
public function __construct(int $maximumNumberOfFailures, ?LoggerInterface $logger = null)
{
$this->maximumNumberOfFailures = $maximumNumberOfFailures;
$this->logger = $logger;
if ($maximumNumberOfFailures <= 0) {
throw new InvalidArgumentException('Failure limit must be greater than zero.');
}
}
public function onMessageFailed(WorkerMessageFailedEvent $event): void
{
++$this->failedMessages;
}
public function onWorkerRunning(WorkerRunningEvent $event): void
{
if (!$event->isWorkerIdle() && $this->failedMessages >= $this->maximumNumberOfFailures) {
$this->failedMessages = 0;
$event->getWorker()->stop();
if (null !== $this->logger) {
$this->logger->info('Worker stopped due to limit of {count} failed message(s) is reached', ['count' => $this->maximumNumberOfFailures]);
}
}
}
public static function getSubscribedEvents(): array
{
return [
WorkerMessageFailedEvent::class => 'onMessageFailed',
WorkerRunningEvent::class => 'onWorkerRunning',
];
}
}
@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
/**
* @author Simon Delicata <simon.delicata@free.fr>
* @author Tobias Schultze <http://tobion.de>
*/
class StopWorkerOnMemoryLimitListener implements EventSubscriberInterface
{
private $memoryLimit;
private $logger;
private $memoryResolver;
public function __construct(int $memoryLimit, ?LoggerInterface $logger = null, ?callable $memoryResolver = null)
{
$this->memoryLimit = $memoryLimit;
$this->logger = $logger;
$this->memoryResolver = $memoryResolver ?: static function () {
return memory_get_usage(true);
};
}
public function onWorkerRunning(WorkerRunningEvent $event): void
{
$memoryResolver = $this->memoryResolver;
$usedMemory = $memoryResolver();
if ($usedMemory > $this->memoryLimit) {
$event->getWorker()->stop();
if (null !== $this->logger) {
$this->logger->info('Worker stopped due to memory limit of {limit} bytes exceeded ({memory} bytes used)', ['limit' => $this->memoryLimit, 'memory' => $usedMemory]);
}
}
}
public static function getSubscribedEvents()
{
return [
WorkerRunningEvent::class => 'onWorkerRunning',
];
}
}
@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
* @author Tobias Schultze <http://tobion.de>
*/
class StopWorkerOnMessageLimitListener implements EventSubscriberInterface
{
private $maximumNumberOfMessages;
private $logger;
private $receivedMessages = 0;
public function __construct(int $maximumNumberOfMessages, ?LoggerInterface $logger = null)
{
$this->maximumNumberOfMessages = $maximumNumberOfMessages;
$this->logger = $logger;
if ($maximumNumberOfMessages <= 0) {
throw new InvalidArgumentException('Message limit must be greater than zero.');
}
}
public function onWorkerRunning(WorkerRunningEvent $event): void
{
if (!$event->isWorkerIdle() && ++$this->receivedMessages >= $this->maximumNumberOfMessages) {
$this->receivedMessages = 0;
$event->getWorker()->stop();
if (null !== $this->logger) {
$this->logger->info('Worker stopped due to maximum count of {count} messages processed', ['count' => $this->maximumNumberOfMessages]);
}
}
}
public static function getSubscribedEvents()
{
return [
WorkerRunningEvent::class => 'onWorkerRunning',
];
}
}
@@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Event\WorkerStartedEvent;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class StopWorkerOnRestartSignalListener implements EventSubscriberInterface
{
public const RESTART_REQUESTED_TIMESTAMP_KEY = 'workers.restart_requested_timestamp';
private $cachePool;
private $logger;
private $workerStartedAt;
public function __construct(CacheItemPoolInterface $cachePool, ?LoggerInterface $logger = null)
{
$this->cachePool = $cachePool;
$this->logger = $logger;
}
public function onWorkerStarted(): void
{
$this->workerStartedAt = microtime(true);
}
public function onWorkerRunning(WorkerRunningEvent $event): void
{
if ($this->shouldRestart()) {
$event->getWorker()->stop();
if (null !== $this->logger) {
$this->logger->info('Worker stopped because a restart was requested.');
}
}
}
public static function getSubscribedEvents()
{
return [
WorkerStartedEvent::class => 'onWorkerStarted',
WorkerRunningEvent::class => 'onWorkerRunning',
];
}
private function shouldRestart(): bool
{
$cacheItem = $this->cachePool->getItem(self::RESTART_REQUESTED_TIMESTAMP_KEY);
if (!$cacheItem->isHit()) {
// no restart has ever been scheduled
return false;
}
return $this->workerStartedAt < $cacheItem->get();
}
}
@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerStartedEvent;
/**
* @author Tobias Schultze <http://tobion.de>
*/
class StopWorkerOnSigtermSignalListener implements EventSubscriberInterface
{
private $logger;
public function __construct(?LoggerInterface $logger = null)
{
$this->logger = $logger;
}
public function onWorkerStarted(WorkerStartedEvent $event): void
{
pcntl_signal(\SIGTERM, function () use ($event) {
if (null !== $this->logger) {
$this->logger->info('Received SIGTERM signal.', ['transport_names' => $event->getWorker()->getMetadata()->getTransportNames()]);
}
$event->getWorker()->stop();
});
}
public static function getSubscribedEvents()
{
if (!\function_exists('pcntl_signal')) {
return [];
}
return [
WorkerStartedEvent::class => ['onWorkerStarted', 100],
];
}
}
@@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Event\WorkerStartedEvent;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
/**
* @author Simon Delicata <simon.delicata@free.fr>
* @author Tobias Schultze <http://tobion.de>
*/
class StopWorkerOnTimeLimitListener implements EventSubscriberInterface
{
private $timeLimitInSeconds;
private $logger;
private $endTime;
public function __construct(int $timeLimitInSeconds, ?LoggerInterface $logger = null)
{
$this->timeLimitInSeconds = $timeLimitInSeconds;
$this->logger = $logger;
if ($timeLimitInSeconds <= 0) {
throw new InvalidArgumentException('Time limit must be greater than zero.');
}
}
public function onWorkerStarted(): void
{
$startTime = microtime(true);
$this->endTime = $startTime + $this->timeLimitInSeconds;
}
public function onWorkerRunning(WorkerRunningEvent $event): void
{
if ($this->endTime < microtime(true)) {
$event->getWorker()->stop();
if (null !== $this->logger) {
$this->logger->info('Worker stopped due to time limit of {timeLimit}s exceeded', ['timeLimit' => $this->timeLimitInSeconds]);
}
}
}
public static function getSubscribedEvents()
{
return [
WorkerStartedEvent::class => 'onWorkerStarted',
WorkerRunningEvent::class => 'onWorkerRunning',
];
}
}
@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* When handling queued messages from {@link DispatchAfterCurrentBusMiddleware},
* some handlers caused an exception. This exception contains all those handler exceptions.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class DelayedMessageHandlingException extends RuntimeException
{
private $exceptions;
public function __construct(array $exceptions)
{
$exceptionMessages = implode(", \n", array_map(
function (\Throwable $e) {
return \get_class($e).': '.$e->getMessage();
},
$exceptions
));
if (1 === \count($exceptions)) {
$message = sprintf("A delayed message handler threw an exception: \n\n%s", $exceptionMessages);
} else {
$message = sprintf("Some delayed message handlers threw an exception: \n\n%s", $exceptionMessages);
}
$this->exceptions = $exceptions;
parent::__construct($message, 0, $exceptions[0]);
}
public function getExceptions(): array
{
return $this->exceptions;
}
}
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* Base Messenger component's exception.
*
* @author Samuel Roze <samuel.roze@gmail.com>
*/
interface ExceptionInterface extends \Throwable
{
}
@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
use Symfony\Component\Messenger\Envelope;
class HandlerFailedException extends RuntimeException
{
private $exceptions;
private $envelope;
/**
* @param \Throwable[] $exceptions
*/
public function __construct(Envelope $envelope, array $exceptions)
{
$firstFailure = current($exceptions);
$message = sprintf('Handling "%s" failed: ', \get_class($envelope->getMessage()));
parent::__construct(
$message.(1 === \count($exceptions)
? $firstFailure->getMessage()
: sprintf('%d handlers failed. First failure is: %s', \count($exceptions), $firstFailure->getMessage())
),
(int) $firstFailure->getCode(),
$firstFailure
);
$this->envelope = $envelope;
$this->exceptions = $exceptions;
}
public function getEnvelope(): Envelope
{
return $this->envelope;
}
/**
* @return \Throwable[]
*/
public function getNestedExceptions(): array
{
return $this->exceptions;
}
public function getNestedExceptionOfClass(string $exceptionClassName): array
{
return array_values(
array_filter(
$this->exceptions,
function ($exception) use ($exceptionClassName) {
return is_a($exception, $exceptionClassName);
}
)
);
}
}
@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}
@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Roland Franssen <franssen.roland@gmail.com>
*/
class LogicException extends \LogicException implements ExceptionInterface
{
}
@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* Thrown when a message cannot be decoded in a serializer.
*/
class MessageDecodingFailedException extends InvalidArgumentException
{
}
@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class NoHandlerForMessageException extends LogicException
{
}
@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* Marker interface for exceptions to indicate that handling a message should have worked.
*
* If something goes wrong while handling a message that's received from a transport
* and the message should be retried, a handler can throw such an exception.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface RecoverableExceptionInterface extends \Throwable
{
}
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* A concrete implementation of RecoverableExceptionInterface that can be used directly.
*
* @author Frederic Bouchery <frederic@bouchery.fr>
*/
class RecoverableMessageHandlingException extends RuntimeException implements RecoverableExceptionInterface
{
}
@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Tobias Schultze <http://tobion.de>
*/
class RejectRedeliveredMessageException extends RuntimeException
{
}
@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class StopWorkerException extends RuntimeException implements StopWorkerExceptionInterface
{
public function __construct(string $message = 'Worker should stop.', ?\Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
}
}
@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
interface StopWorkerExceptionInterface extends \Throwable
{
}
@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* @author Eric Masoero <em@studeal.fr>
*/
class TransportException extends RuntimeException
{
}
@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* Marker interface for exceptions to indicate that handling a message will continue to fail.
*
* If something goes wrong while handling a message that's received from a transport
* and the message should not be retried, a handler can throw such an exception.
*
* @author Tobias Schultze <http://tobion.de>
*/
interface UnrecoverableExceptionInterface extends \Throwable
{
}
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
/**
* A concrete implementation of UnrecoverableExceptionInterface that can be used directly.
*
* @author Frederic Bouchery <frederic@bouchery.fr>
*/
class UnrecoverableMessageHandlingException extends RuntimeException implements UnrecoverableExceptionInterface
{
}
@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Exception;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class ValidationFailedException extends RuntimeException
{
private $violations;
private $violatingMessage;
public function __construct(object $violatingMessage, ConstraintViolationListInterface $violations)
{
$this->violatingMessage = $violatingMessage;
$this->violations = $violations;
parent::__construct(sprintf('Message of type "%s" failed validation.', \get_class($this->violatingMessage)));
}
public function getViolatingMessage()
{
return $this->violatingMessage;
}
public function getViolations(): ConstraintViolationListInterface
{
return $this->violations;
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger;
use Symfony\Component\Messenger\Exception\LogicException;
use Symfony\Component\Messenger\Stamp\HandledStamp;
/**
* Leverages a message bus to expect a single, synchronous message handling and return its result.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
trait HandleTrait
{
/** @var MessageBusInterface */
private $messageBus;
/**
* Dispatches the given message, expecting to be handled by a single handler
* and returns the result from the handler returned value.
* This behavior is useful for both synchronous command & query buses,
* the last one usually returning the handler result.
*
* @param object|Envelope $message The message or the message pre-wrapped in an envelope
*
* @return mixed
*/
private function handle(object $message)
{
if (!$this->messageBus instanceof MessageBusInterface) {
throw new LogicException(sprintf('You must provide a "%s" instance in the "%s::$messageBus" property, "%s" given.', MessageBusInterface::class, static::class, get_debug_type($this->messageBus)));
}
$envelope = $this->messageBus->dispatch($message);
/** @var HandledStamp[] $handledStamps */
$handledStamps = $envelope->all(HandledStamp::class);
if (!$handledStamps) {
throw new LogicException(sprintf('Message of type "%s" was handled zero times. Exactly one handler is expected when using "%s::%s()".', get_debug_type($envelope->getMessage()), static::class, __FUNCTION__));
}
if (\count($handledStamps) > 1) {
$handlers = implode(', ', array_map(function (HandledStamp $stamp): string {
return sprintf('"%s"', $stamp->getHandlerName());
}, $handledStamps));
throw new LogicException(sprintf('Message of type "%s" was handled multiple times. Only one handler is expected when using "%s::%s()", got %d: %s.', get_debug_type($envelope->getMessage()), static::class, __FUNCTION__, \count($handledStamps), $handlers));
}
return $handledStamps[0]->getResult();
}
}
@@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
use Symfony\Component\Messenger\Exception\LogicException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class Acknowledger
{
private $handlerClass;
private $ack;
private $error = null;
private $result = null;
/**
* @param \Closure(\Throwable|null, mixed):void|null $ack
*/
public function __construct(string $handlerClass, ?\Closure $ack = null)
{
$this->handlerClass = $handlerClass;
$this->ack = $ack ?? static function () {};
}
/**
* @param mixed $result
*/
public function ack($result = null): void
{
$this->doAck(null, $result);
}
public function nack(\Throwable $error): void
{
$this->doAck($error);
}
public function getError(): ?\Throwable
{
return $this->error;
}
/**
* @return mixed
*/
public function getResult()
{
return $this->result;
}
public function isAcknowledged(): bool
{
return null === $this->ack;
}
public function __destruct()
{
if ($this->ack instanceof \Closure) {
throw new LogicException(sprintf('The acknowledger was not called by the "%s" batch handler.', $this->handlerClass));
}
}
private function doAck(?\Throwable $e = null, $result = null): void
{
if (!$ack = $this->ack) {
throw new LogicException(sprintf('The acknowledger cannot be called twice by the "%s" batch handler.', $this->handlerClass));
}
$this->ack = null;
$this->error = $e;
$this->result = $result;
$ack($e, $result);
}
}
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
interface BatchHandlerInterface
{
/**
* @param Acknowledger|null $ack The function to call to ack/nack the $message.
* The message should be handled synchronously when null.
*
* @return mixed The number of pending messages in the batch if $ack is not null,
* the result from handling the message otherwise
*/
// public function __invoke(object $message, ?Acknowledger $ack = null): mixed;
/**
* Flushes any pending buffers.
*
* @param bool $force Whether flushing is required; it can be skipped if not
*/
public function flush(bool $force): void;
}
@@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
use Symfony\Component\Messenger\Exception\LogicException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
trait BatchHandlerTrait
{
private $jobs = [];
/**
* {@inheritdoc}
*/
public function flush(bool $force): void
{
if ($jobs = $this->jobs) {
$this->jobs = [];
$this->process($jobs);
}
}
/**
* @param Acknowledger|null $ack The function to call to ack/nack the $message.
* The message should be handled synchronously when null.
*
* @return mixed The number of pending messages in the batch if $ack is not null,
* the result from handling the message otherwise
*/
private function handle(object $message, ?Acknowledger $ack)
{
if (null === $ack) {
$ack = new Acknowledger(get_debug_type($this));
$this->jobs[] = [$message, $ack];
$this->flush(true);
return $ack->getResult();
}
$this->jobs[] = [$message, $ack];
if (!$this->shouldFlush()) {
return \count($this->jobs);
}
$this->flush(true);
return 0;
}
private function shouldFlush(): bool
{
return 10 <= \count($this->jobs);
}
/**
* Completes the jobs in the list.
*
* @param list<array{0: object, 1: Acknowledger}> $jobs A list of pairs of messages and their corresponding acknowledgers
*/
private function process(array $jobs): void
{
throw new LogicException(sprintf('"%s" should implement abstract method "process()".', get_debug_type($this)));
}
}
@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
/**
* Describes a handler and the possible associated options, such as `from_transport`, `bus`, etc.
*
* @author Samuel Roze <samuel.roze@gmail.com>
*/
final class HandlerDescriptor
{
private $handler;
private $name;
private $batchHandler;
private $options;
public function __construct(callable $handler, array $options = [])
{
if (!$handler instanceof \Closure) {
$handler = \Closure::fromCallable($handler);
}
$this->handler = $handler;
$this->options = $options;
$r = new \ReflectionFunction($handler);
if (str_contains($r->name, '{closure')) {
$this->name = 'Closure';
} elseif (!$handler = $r->getClosureThis()) {
$class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass();
$this->name = ($class ? $class->name.'::' : '').$r->name;
} else {
if ($handler instanceof BatchHandlerInterface) {
$this->batchHandler = $handler;
}
$this->name = \get_class($handler).'::'.$r->name;
}
}
public function getHandler(): callable
{
return $this->handler;
}
public function getName(): string
{
$name = $this->name;
$alias = $this->options['alias'] ?? null;
if (null !== $alias) {
$name .= '@'.$alias;
}
return $name;
}
public function getBatchHandler(): ?BatchHandlerInterface
{
return $this->batchHandler;
}
public function getOption(string $option)
{
return $this->options[$option] ?? null;
}
}
@@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
/**
* Maps a message to a list of handlers.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class HandlersLocator implements HandlersLocatorInterface
{
private $handlers;
/**
* @param HandlerDescriptor[][]|callable[][] $handlers
*/
public function __construct(array $handlers)
{
$this->handlers = $handlers;
}
/**
* {@inheritdoc}
*/
public function getHandlers(Envelope $envelope): iterable
{
$seen = [];
foreach (self::listTypes($envelope) as $type) {
foreach ($this->handlers[$type] ?? [] as $handlerDescriptor) {
if (\is_callable($handlerDescriptor)) {
$handlerDescriptor = new HandlerDescriptor($handlerDescriptor);
}
if (!$this->shouldHandle($envelope, $handlerDescriptor)) {
continue;
}
$name = $handlerDescriptor->getName();
if (\in_array($name, $seen)) {
continue;
}
$seen[] = $name;
yield $handlerDescriptor;
}
}
}
/**
* @internal
*/
public static function listTypes(Envelope $envelope): array
{
$class = \get_class($envelope->getMessage());
return [$class => $class]
+ class_parents($class)
+ class_implements($class)
+ ['*' => '*'];
}
private function shouldHandle(Envelope $envelope, HandlerDescriptor $handlerDescriptor): bool
{
if (null === $received = $envelope->last(ReceivedStamp::class)) {
return true;
}
if (null === $expectedTransport = $handlerDescriptor->getOption('from_transport')) {
return true;
}
return $received->getTransportName() === $expectedTransport;
}
}
@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
use Symfony\Component\Messenger\Envelope;
/**
* Maps a message to a list of handlers.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface HandlersLocatorInterface
{
/**
* Returns the handlers for the given message name.
*
* @return iterable<int, HandlerDescriptor>
*/
public function getHandlers(Envelope $envelope): iterable;
}
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
/**
* Marker interface for message handlers.
*
* @author Samuel Roze <samuel.roze@gmail.com>
*/
interface MessageHandlerInterface
{
}
@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Handler;
/**
* Handlers can implement this interface to handle multiple messages.
*
* @author Samuel Roze <samuel.roze@gmail.com>
*/
interface MessageSubscriberInterface extends MessageHandlerInterface
{
/**
* Returns a list of messages to be handled.
*
* It returns a list of messages like in the following example:
*
* yield MyMessage::class;
*
* It can also change the priority per classes.
*
* yield FirstMessage::class => ['priority' => 0];
* yield SecondMessage::class => ['priority' => -10];
*
* It can also specify a method, a priority, a bus and/or a transport per message:
*
* yield FirstMessage::class => ['method' => 'firstMessageMethod'];
* yield SecondMessage::class => [
* 'method' => 'secondMessageMethod',
* 'priority' => 20,
* 'bus' => 'my_bus_name',
* 'from_transport' => 'your_transport_name',
* ];
*
* The benefit of using `yield` instead of returning an array is that you can `yield` multiple times the
* same key and therefore subscribe to the same message multiple times with different options.
*
* The `__invoke` method of the handler will be called as usual with the message to handle.
*/
public static function getHandledMessages(): iterable;
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2018-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+79
View File
@@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackMiddleware;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
* @author Matthias Noback <matthiasnoback@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class MessageBus implements MessageBusInterface
{
private $middlewareAggregate;
/**
* @param iterable<mixed, MiddlewareInterface> $middlewareHandlers
*/
public function __construct(iterable $middlewareHandlers = [])
{
if ($middlewareHandlers instanceof \IteratorAggregate) {
$this->middlewareAggregate = $middlewareHandlers;
} elseif (\is_array($middlewareHandlers)) {
$this->middlewareAggregate = new \ArrayObject($middlewareHandlers);
} else {
// $this->middlewareAggregate should be an instance of IteratorAggregate.
// When $middlewareHandlers is an Iterator, we wrap it to ensure it is lazy-loaded and can be rewound.
$this->middlewareAggregate = new class($middlewareHandlers) implements \IteratorAggregate {
private $middlewareHandlers;
private $cachedIterator;
public function __construct(\Traversable $middlewareHandlers)
{
$this->middlewareHandlers = $middlewareHandlers;
}
public function getIterator(): \Traversable
{
if (null === $this->cachedIterator) {
$this->cachedIterator = new \ArrayObject(iterator_to_array($this->middlewareHandlers, false));
}
return $this->cachedIterator;
}
};
}
}
/**
* {@inheritdoc}
*/
public function dispatch(object $message, array $stamps = []): Envelope
{
$envelope = Envelope::wrap($message, $stamps);
$middlewareIterator = $this->middlewareAggregate->getIterator();
while ($middlewareIterator instanceof \IteratorAggregate) {
$middlewareIterator = $middlewareIterator->getIterator();
}
$middlewareIterator->rewind();
if (!$middlewareIterator->valid()) {
return $envelope;
}
$stack = new StackMiddleware($middlewareIterator);
return $middlewareIterator->current()->handle($envelope, $stack);
}
}
@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger;
use Symfony\Component\Messenger\Stamp\StampInterface;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
interface MessageBusInterface
{
/**
* Dispatches the given message.
*
* @param object|Envelope $message The message or the message pre-wrapped in an envelope
* @param StampInterface[] $stamps
*/
public function dispatch(object $message, array $stamps = []): Envelope;
}
@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
/**
* Execute the inner middleware according to an activation strategy.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class ActivationMiddleware implements MiddlewareInterface
{
private $inner;
private $activated;
/**
* @param bool|callable $activated
*/
public function __construct(MiddlewareInterface $inner, $activated)
{
$this->inner = $inner;
$this->activated = $activated;
}
/**
* {@inheritdoc}
*/
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
if (\is_callable($this->activated) ? ($this->activated)($envelope) : $this->activated) {
return $this->inner->handle($envelope, $stack);
}
return $stack->next()->handle($envelope, $stack);
}
}
@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\BusNameStamp;
/**
* Adds the BusNameStamp to the bus.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class AddBusNameStampMiddleware implements MiddlewareInterface
{
private $busName;
public function __construct(string $busName)
{
$this->busName = $busName;
}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
if (null === $envelope->last(BusNameStamp::class)) {
$envelope = $envelope->with(new BusNameStamp($this->busName));
}
return $stack->next()->handle($envelope, $stack);
}
}
@@ -0,0 +1,133 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\DelayedMessageHandlingException;
use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp;
/**
* Allow to configure messages to be handled after the current bus is finished.
*
* I.e, messages dispatched from a handler with a DispatchAfterCurrentBus stamp
* will actually be handled once the current message being dispatched is fully
* handled.
*
* For instance, using this middleware before the DoctrineTransactionMiddleware
* means sub-dispatched messages with a DispatchAfterCurrentBus stamp would be
* handled after the Doctrine transaction has been committed.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class DispatchAfterCurrentBusMiddleware implements MiddlewareInterface
{
/**
* @var QueuedEnvelope[] A queue of messages and next middleware
*/
private $queue = [];
/**
* @var bool this property is used to signal if we are inside a the first/root call to
* MessageBusInterface::dispatch() or if dispatch has been called inside a message handler
*/
private $isRootDispatchCallRunning = false;
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
if (null !== $envelope->last(DispatchAfterCurrentBusStamp::class)) {
if ($this->isRootDispatchCallRunning) {
$this->queue[] = new QueuedEnvelope($envelope, $stack);
return $envelope;
}
$envelope = $envelope->withoutAll(DispatchAfterCurrentBusStamp::class);
}
if ($this->isRootDispatchCallRunning) {
/*
* A call to MessageBusInterface::dispatch() was made from inside the main bus handling,
* but the message does not have the stamp. So, process it like normal.
*/
return $stack->next()->handle($envelope, $stack);
}
// First time we get here, mark as inside a "root dispatch" call:
$this->isRootDispatchCallRunning = true;
try {
// Execute the whole middleware stack & message handling for main dispatch:
$returnedEnvelope = $stack->next()->handle($envelope, $stack);
} catch (\Throwable $exception) {
/*
* Whenever an exception occurs while handling a message that has
* queued other messages, we drop the queued ones.
* This is intentional since the queued commands were likely dependent
* on the preceding command.
*/
$this->queue = [];
$this->isRootDispatchCallRunning = false;
throw $exception;
}
// "Root dispatch" call is finished, dispatch stored messages.
$exceptions = [];
while (null !== $queueItem = array_shift($this->queue)) {
// Save how many messages are left in queue before handling the message
$queueLengthBefore = \count($this->queue);
try {
// Execute the stored messages
$queueItem->getStack()->next()->handle($queueItem->getEnvelope(), $queueItem->getStack());
} catch (\Exception $exception) {
// Gather all exceptions
$exceptions[] = $exception;
// Restore queue to previous state
$this->queue = \array_slice($this->queue, 0, $queueLengthBefore);
}
}
$this->isRootDispatchCallRunning = false;
if (\count($exceptions) > 0) {
throw new DelayedMessageHandlingException($exceptions);
}
return $returnedEnvelope;
}
}
/**
* @internal
*/
final class QueuedEnvelope
{
/** @var Envelope */
private $envelope;
/** @var StackInterface */
private $stack;
public function __construct(Envelope $envelope, StackInterface $stack)
{
$this->envelope = $envelope->withoutAll(DispatchAfterCurrentBusStamp::class);
$this->stack = $stack;
}
public function getEnvelope(): Envelope
{
return $this->envelope;
}
public function getStack(): StackInterface
{
return $this->stack;
}
}
@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class FailedMessageProcessingMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
// look for "received" messages decorated with the SentToFailureTransportStamp
/** @var SentToFailureTransportStamp|null $sentToFailureStamp */
$sentToFailureStamp = $envelope->last(SentToFailureTransportStamp::class);
if (null !== $sentToFailureStamp && null !== $envelope->last(ReceivedStamp::class)) {
// mark the message as "received" from the original transport
// this guarantees the same behavior as when originally received
$envelope = $envelope->with(new ReceivedStamp($sentToFailureStamp->getOriginalReceiverName()));
}
return $stack->next()->handle($envelope, $stack);
}
}
@@ -0,0 +1,147 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
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\NoAutoAckStamp;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class HandleMessageMiddleware implements MiddlewareInterface
{
use LoggerAwareTrait;
private $handlersLocator;
private $allowNoHandlers;
public function __construct(HandlersLocatorInterface $handlersLocator, bool $allowNoHandlers = false)
{
$this->handlersLocator = $handlersLocator;
$this->allowNoHandlers = $allowNoHandlers;
$this->logger = new NullLogger();
}
/**
* {@inheritdoc}
*
* @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' => \get_class($message),
];
$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, [$e]);
} else {
$envelope = $envelope->with(HandledStamp::fromDescriptor($handlerDescriptor, $result));
}
$ackStamp->ack($envelope, $e);
});
$result = $handler($message, $ack);
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 = $handler($message);
}
$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[] = $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[] = $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;
}
}
@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
interface MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope;
}
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceivedStamp;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\RejectRedeliveredMessageException;
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceivedStamp as LegacyAmqpReceivedStamp;
/**
* Middleware that throws a RejectRedeliveredMessageException when a message is detected that has been redelivered by AMQP.
*
* The middleware runs before the HandleMessageMiddleware and prevents redelivered messages from being handled directly.
* The thrown exception is caught by the worker and will trigger the retry logic according to the retry strategy.
*
* AMQP redelivers messages when they do not get acknowledged or rejected. This can happen when the connection times out
* or an exception is thrown before acknowledging or rejecting. When such errors happen again while handling the
* redelivered message, the message would get redelivered again and again. The purpose of this middleware is to prevent
* infinite redelivery loops and to unblock the queue by republishing the redelivered messages as retries with a retry
* limit and potential delay.
*
* @author Tobias Schultze <http://tobion.de>
*/
class RejectRedeliveredMessageMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$amqpReceivedStamp = $envelope->last(AmqpReceivedStamp::class);
if ($amqpReceivedStamp instanceof AmqpReceivedStamp && $amqpReceivedStamp->getAmqpEnvelope()->isRedelivery()) {
throw new RejectRedeliveredMessageException('Redelivered message from AMQP detected that will be rejected and trigger the retry logic.');
}
// Legacy code to support symfony/messenger < 5.1
$amqpReceivedStamp = $envelope->last(LegacyAmqpReceivedStamp::class);
if ($amqpReceivedStamp instanceof LegacyAmqpReceivedStamp && $amqpReceivedStamp->getAmqpEnvelope()->isRedelivery()) {
throw new RejectRedeliveredMessageException('Redelivered message from AMQP detected that will be rejected and trigger the retry logic.');
}
return $stack->next()->handle($envelope, $stack);
}
}
@@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp;
use Symfony\Component\Messenger\Stamp\RouterContextStamp;
use Symfony\Component\Routing\RequestContextAwareInterface;
/**
* Restore the Router context when processing the message.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class RouterContextMiddleware implements MiddlewareInterface
{
private $router;
public function __construct(RequestContextAwareInterface $router)
{
$this->router = $router;
}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
if (!$envelope->last(ConsumedByWorkerStamp::class) || !$contextStamp = $envelope->last(RouterContextStamp::class)) {
$context = $this->router->getContext();
$envelope = $envelope->with(new RouterContextStamp(
$context->getBaseUrl(),
$context->getMethod(),
$context->getHost(),
$context->getScheme(),
$context->getHttpPort(),
$context->getHttpsPort(),
$context->getPathInfo(),
$context->getQueryString()
));
return $stack->next()->handle($envelope, $stack);
}
$context = $this->router->getContext();
$currentBaseUrl = $context->getBaseUrl();
$currentMethod = $context->getMethod();
$currentHost = $context->getHost();
$currentScheme = $context->getScheme();
$currentHttpPort = $context->getHttpPort();
$currentHttpsPort = $context->getHttpsPort();
$currentPathInfo = $context->getPathInfo();
$currentQueryString = $context->getQueryString();
/* @var RouterContextStamp $contextStamp */
$context
->setBaseUrl($contextStamp->getBaseUrl())
->setMethod($contextStamp->getMethod())
->setHost($contextStamp->getHost())
->setScheme($contextStamp->getScheme())
->setHttpPort($contextStamp->getHttpPort())
->setHttpsPort($contextStamp->getHttpsPort())
->setPathInfo($contextStamp->getPathInfo())
->setQueryString($contextStamp->getQueryString())
;
try {
return $stack->next()->handle($envelope, $stack);
} finally {
$context
->setBaseUrl($currentBaseUrl)
->setMethod($currentMethod)
->setHost($currentHost)
->setScheme($currentScheme)
->setHttpPort($currentHttpPort)
->setHttpsPort($currentHttpsPort)
->setPathInfo($currentPathInfo)
->setQueryString($currentQueryString)
;
}
}
}
@@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent;
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
use Symfony\Component\Messenger\Stamp\SentStamp;
use Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
* @author Tobias Schultze <http://tobion.de>
*/
class SendMessageMiddleware implements MiddlewareInterface
{
use LoggerAwareTrait;
private $sendersLocator;
private $eventDispatcher;
public function __construct(SendersLocatorInterface $sendersLocator, ?EventDispatcherInterface $eventDispatcher = null)
{
$this->sendersLocator = $sendersLocator;
$this->eventDispatcher = class_exists(Event::class) ? LegacyEventDispatcherProxy::decorate($eventDispatcher) : $eventDispatcher;
$this->logger = new NullLogger();
}
/**
* {@inheritdoc}
*/
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$context = [
'class' => \get_class($envelope->getMessage()),
];
$sender = null;
if ($envelope->all(ReceivedStamp::class)) {
// it's a received message, do not send it back
$this->logger->info('Received message {class}', $context);
} else {
$shouldDispatchEvent = true;
foreach ($this->sendersLocator->getSenders($envelope) as $alias => $sender) {
if (null !== $this->eventDispatcher && $shouldDispatchEvent) {
$event = new SendMessageToTransportsEvent($envelope);
$this->eventDispatcher->dispatch($event);
$envelope = $event->getEnvelope();
$shouldDispatchEvent = false;
}
$this->logger->info('Sending message {class} with {alias} sender using {sender}', $context + ['alias' => $alias, 'sender' => \get_class($sender)]);
$envelope = $sender->send($envelope->with(new SentStamp(\get_class($sender), \is_string($alias) ? $alias : null)));
}
}
if (null === $sender) {
return $stack->next()->handle($envelope, $stack);
}
// message should only be sent and not be handled by the next middleware
return $envelope;
}
}
@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* Implementations must be cloneable, and each clone must unstack the stack independently.
*/
interface StackInterface
{
/**
* Returns the next middleware to process a message.
*/
public function next(): MiddlewareInterface;
}
@@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class StackMiddleware implements MiddlewareInterface, StackInterface
{
private $stack;
private $offset = 0;
/**
* @param iterable<mixed, MiddlewareInterface>|MiddlewareInterface|null $middlewareIterator
*/
public function __construct($middlewareIterator = null)
{
$this->stack = new MiddlewareStack();
if (null === $middlewareIterator) {
return;
}
if ($middlewareIterator instanceof \Iterator) {
$this->stack->iterator = $middlewareIterator;
} elseif ($middlewareIterator instanceof MiddlewareInterface) {
$this->stack->stack[] = $middlewareIterator;
} elseif (!is_iterable($middlewareIterator)) {
throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be iterable of "%s", "%s" given.', __METHOD__, MiddlewareInterface::class, get_debug_type($middlewareIterator)));
} else {
$this->stack->iterator = (function () use ($middlewareIterator) {
yield from $middlewareIterator;
})();
}
}
public function next(): MiddlewareInterface
{
if (null === $next = $this->stack->next($this->offset)) {
return $this;
}
++$this->offset;
return $next;
}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
return $envelope;
}
}
/**
* @internal
*/
class MiddlewareStack
{
/** @var \Iterator<mixed, MiddlewareInterface> */
public $iterator;
public $stack = [];
public function next(int $offset): ?MiddlewareInterface
{
if (isset($this->stack[$offset])) {
return $this->stack[$offset];
}
if (null === $this->iterator) {
return null;
}
$this->iterator->next();
if (!$this->iterator->valid()) {
return $this->iterator = null;
}
return $this->stack[] = $this->iterator->current();
}
}
@@ -0,0 +1,102 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Stopwatch\Stopwatch;
/**
* Collects some data about a middleware.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class TraceableMiddleware implements MiddlewareInterface
{
private $stopwatch;
private $busName;
private $eventCategory;
public function __construct(Stopwatch $stopwatch, string $busName, string $eventCategory = 'messenger.middleware')
{
$this->stopwatch = $stopwatch;
$this->busName = $busName;
$this->eventCategory = $eventCategory;
}
/**
* {@inheritdoc}
*/
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$stack = new TraceableStack($stack, $this->stopwatch, $this->busName, $this->eventCategory);
try {
return $stack->next()->handle($envelope, $stack);
} finally {
$stack->stop();
}
}
}
/**
* @internal
*/
class TraceableStack implements StackInterface
{
private $stack;
private $stopwatch;
private $busName;
private $eventCategory;
private $currentEvent;
public function __construct(StackInterface $stack, Stopwatch $stopwatch, string $busName, string $eventCategory)
{
$this->stack = $stack;
$this->stopwatch = $stopwatch;
$this->busName = $busName;
$this->eventCategory = $eventCategory;
}
/**
* {@inheritdoc}
*/
public function next(): MiddlewareInterface
{
if (null !== $this->currentEvent && $this->stopwatch->isStarted($this->currentEvent)) {
$this->stopwatch->stop($this->currentEvent);
}
if ($this->stack === $nextMiddleware = $this->stack->next()) {
$this->currentEvent = 'Tail';
} else {
$this->currentEvent = sprintf('"%s"', get_debug_type($nextMiddleware));
}
$this->currentEvent .= sprintf(' on "%s"', $this->busName);
$this->stopwatch->start($this->currentEvent, $this->eventCategory);
return $nextMiddleware;
}
public function stop(): void
{
if (null !== $this->currentEvent && $this->stopwatch->isStarted($this->currentEvent)) {
$this->stopwatch->stop($this->currentEvent);
}
$this->currentEvent = null;
}
public function __clone()
{
$this->stack = clone $this->stack;
}
}
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\ValidationFailedException;
use Symfony\Component\Messenger\Stamp\ValidationStamp;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class ValidationMiddleware implements MiddlewareInterface
{
private $validator;
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
/**
* {@inheritdoc}
*/
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$message = $envelope->getMessage();
$groups = null;
/** @var ValidationStamp|null $validationStamp */
if ($validationStamp = $envelope->last(ValidationStamp::class)) {
$groups = $validationStamp->getGroups();
}
$violations = $this->validator->validate($message, null, $groups);
if (\count($violations)) {
throw new ValidationFailedException($message, $violations);
}
return $stack->next()->handle($envelope, $stack);
}
}
+29
View File
@@ -0,0 +1,29 @@
Messenger Component
===================
The Messenger component helps applications send and receive messages to/from
other applications or via message queues.
Sponsor
-------
The Messenger component for Symfony 5.4/6.0 is [backed][1] by [SensioLabs][2].
As the creator of Symfony, SensioLabs supports companies using Symfony, with an
offering encompassing consultancy, expertise, services, training, and technical
assistance to ensure the success of web application development projects.
Help Symfony by [sponsoring][3] its development!
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/messenger.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
[1]: https://symfony.com/backers
[2]: https://sensiolabs.com
[3]: https://symfony.com/sponsor
@@ -0,0 +1,91 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Retry;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
/**
* A retry strategy with a constant or exponential retry delay.
*
* For example, if $delayMilliseconds=10000 & $multiplier=1 (default),
* each retry will wait exactly 10 seconds.
*
* But if $delayMilliseconds=10000 & $multiplier=2:
* * Retry 1: 10 second delay
* * Retry 2: 20 second delay (10000 * 2 = 20000)
* * Retry 3: 40 second delay (20000 * 2 = 40000)
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*
* @final
*/
class MultiplierRetryStrategy implements RetryStrategyInterface
{
private $maxRetries;
private $delayMilliseconds;
private $multiplier;
private $maxDelayMilliseconds;
/**
* @param int $maxRetries The maximum number of times to retry
* @param int $delayMilliseconds Amount of time to delay (or the initial value when multiplier is used)
* @param float $multiplier Multiplier to apply to the delay each time a retry occurs
* @param int $maxDelayMilliseconds Maximum delay to allow (0 means no maximum)
*/
public function __construct(int $maxRetries = 3, int $delayMilliseconds = 1000, float $multiplier = 1, int $maxDelayMilliseconds = 0)
{
$this->maxRetries = $maxRetries;
if ($delayMilliseconds < 0) {
throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMilliseconds));
}
$this->delayMilliseconds = $delayMilliseconds;
if ($multiplier < 1) {
throw new InvalidArgumentException(sprintf('Multiplier must be greater than zero: "%s" given.', $multiplier));
}
$this->multiplier = $multiplier;
if ($maxDelayMilliseconds < 0) {
throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMilliseconds));
}
$this->maxDelayMilliseconds = $maxDelayMilliseconds;
}
/**
* @param \Throwable|null $throwable The cause of the failed handling
*/
public function isRetryable(Envelope $message, ?\Throwable $throwable = null): bool
{
$retries = RedeliveryStamp::getRetryCountFromEnvelope($message);
return $retries < $this->maxRetries;
}
/**
* @param \Throwable|null $throwable The cause of the failed handling
*/
public function getWaitingTime(Envelope $message, ?\Throwable $throwable = null): int
{
$retries = RedeliveryStamp::getRetryCountFromEnvelope($message);
$delay = $this->delayMilliseconds * $this->multiplier ** $retries;
if ($delay > $this->maxDelayMilliseconds && 0 !== $this->maxDelayMilliseconds) {
return $this->maxDelayMilliseconds;
}
return (int) ceil($delay);
}
}
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Retry;
use Symfony\Component\Messenger\Envelope;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
interface RetryStrategyInterface
{
/**
* @param \Throwable|null $throwable The cause of the failed handling
*/
public function isRetryable(Envelope $message/* , ?\Throwable $throwable = null */): bool;
/**
* @param \Throwable|null $throwable The cause of the failed handling
*
* @return int The time to delay/wait in milliseconds
*/
public function getWaitingTime(Envelope $message/* , ?\Throwable $throwable = null */): int;
}
@@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger;
use Psr\Container\ContainerInterface;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
use Symfony\Component\Messenger\Stamp\BusNameStamp;
/**
* Bus of buses that is routable using a BusNameStamp.
*
* This is useful when passed to Worker: messages received
* from the transport can be sent to the correct bus.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class RoutableMessageBus implements MessageBusInterface
{
private $busLocator;
private $fallbackBus;
public function __construct(ContainerInterface $busLocator, ?MessageBusInterface $fallbackBus = null)
{
$this->busLocator = $busLocator;
$this->fallbackBus = $fallbackBus;
}
public function dispatch(object $envelope, array $stamps = []): Envelope
{
if (!$envelope instanceof Envelope) {
throw new InvalidArgumentException('Messages passed to RoutableMessageBus::dispatch() must be inside an Envelope.');
}
/** @var BusNameStamp|null $busNameStamp */
$busNameStamp = $envelope->last(BusNameStamp::class);
if (null === $busNameStamp) {
if (null === $this->fallbackBus) {
throw new InvalidArgumentException('Envelope is missing a BusNameStamp and no fallback message bus is configured on RoutableMessageBus.');
}
return $this->fallbackBus->dispatch($envelope, $stamps);
}
return $this->getMessageBus($busNameStamp->getBusName())->dispatch($envelope, $stamps);
}
/**
* @internal
*/
public function getMessageBus(string $busName): MessageBusInterface
{
if (!$this->busLocator->has($busName)) {
throw new InvalidArgumentException(sprintf('Bus named "%s" does not exist.', $busName));
}
return $this->busLocator->get($busName);
}
}
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\Messenger\Envelope;
/**
* Marker stamp for messages that can be ack/nack'ed.
*/
final class AckStamp implements NonSendableStampInterface
{
private $ack;
/**
* @param \Closure(Envelope, \Throwable|null) $ack
*/
public function __construct(\Closure $ack)
{
$this->ack = $ack;
}
public function ack(Envelope $envelope, ?\Throwable $e = null): void
{
($this->ack)($envelope, $e);
}
}
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* Stamp used to identify which bus it was passed to.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
final class BusNameStamp implements StampInterface
{
private $busName;
public function __construct(string $busName)
{
$this->busName = $busName;
}
public function getBusName(): string
{
return $this->busName;
}
}
@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* A marker that this message was consumed by a worker process.
*/
class ConsumedByWorkerStamp implements NonSendableStampInterface
{
}
@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* Apply this stamp to delay delivery of your message on a transport.
*/
final class DelayStamp implements StampInterface
{
private $delay;
/**
* @param int $delay The delay in milliseconds
*/
public function __construct(int $delay)
{
$this->delay = $delay;
}
public function getDelay(): int
{
return $this->delay;
}
public static function delayFor(\DateInterval $interval): self
{
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
$end = $now->add($interval);
return new self(($end->getTimestamp() - $now->getTimestamp()) * 1000);
}
public static function delayUntil(\DateTimeInterface $dateTime): self
{
return new self(($dateTime->getTimestamp() - time()) * 1000);
}
}
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* Marker item to tell this message should be handled in after the current bus has finished.
*
* @see \Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class DispatchAfterCurrentBusStamp implements NonSendableStampInterface
{
}
@@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
/**
* Stamp applied when a messages fails due to an exception in the handler.
*/
final class ErrorDetailsStamp implements StampInterface
{
/** @var string */
private $exceptionClass;
/** @var int|string */
private $exceptionCode;
/** @var string */
private $exceptionMessage;
/** @var FlattenException|null */
private $flattenException;
/**
* @param int|string $exceptionCode
*/
public function __construct(string $exceptionClass, $exceptionCode, string $exceptionMessage, ?FlattenException $flattenException = null)
{
$this->exceptionClass = $exceptionClass;
$this->exceptionCode = $exceptionCode;
$this->exceptionMessage = $exceptionMessage;
$this->flattenException = $flattenException;
}
public static function create(\Throwable $throwable): self
{
if ($throwable instanceof HandlerFailedException) {
$throwable = $throwable->getPrevious();
}
$flattenException = null;
if (class_exists(FlattenException::class)) {
$flattenException = FlattenException::createFromThrowable($throwable);
}
return new self(\get_class($throwable), $throwable->getCode(), $throwable->getMessage(), $flattenException);
}
public function getExceptionClass(): string
{
return $this->exceptionClass;
}
public function getExceptionCode()
{
return $this->exceptionCode;
}
public function getExceptionMessage(): string
{
return $this->exceptionMessage;
}
public function getFlattenException(): ?FlattenException
{
return $this->flattenException;
}
public function equals(?self $that): bool
{
if (null === $that) {
return false;
}
if ($this->flattenException && $that->flattenException) {
return $this->flattenException->getClass() === $that->flattenException->getClass()
&& $this->flattenException->getCode() === $that->flattenException->getCode()
&& $this->flattenException->getMessage() === $that->flattenException->getMessage();
}
return $this->exceptionClass === $that->exceptionClass
&& $this->exceptionCode === $that->exceptionCode
&& $this->exceptionMessage === $that->exceptionMessage;
}
}
@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* Marker telling that any batch handlers bound to the envelope should be flushed.
*/
final class FlushBatchHandlersStamp implements NonSendableStampInterface
{
private $force;
public function __construct(bool $force)
{
$this->force = $force;
}
public function force(): bool
{
return $this->force;
}
}
@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\Messenger\Handler\HandlerDescriptor;
/**
* Stamp identifying a message handled by the `HandleMessageMiddleware` middleware
* and storing the handler returned value.
*
* This is used by synchronous command buses expecting a return value and the retry logic
* to only execute handlers that didn't succeed.
*
* @see \Symfony\Component\Messenger\Middleware\HandleMessageMiddleware
* @see \Symfony\Component\Messenger\HandleTrait
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
final class HandledStamp implements StampInterface
{
private $result;
private $handlerName;
/**
* @param mixed $result The returned value of the message handler
*/
public function __construct($result, string $handlerName)
{
$this->result = $result;
$this->handlerName = $handlerName;
}
/**
* @param mixed $result The returned value of the message handler
*/
public static function fromDescriptor(HandlerDescriptor $handler, $result): self
{
return new self($result, $handler->getName());
}
/**
* @return mixed
*/
public function getResult()
{
return $this->result;
}
public function getHandlerName(): string
{
return $this->handlerName;
}
}
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\Messenger\Handler\HandlerDescriptor;
/**
* Marker telling that ack should not be done automatically for this message.
*/
final class NoAutoAckStamp implements NonSendableStampInterface
{
private $handlerDescriptor;
public function __construct(HandlerDescriptor $handlerDescriptor)
{
$this->handlerDescriptor = $handlerDescriptor;
}
public function getHandlerDescriptor(): HandlerDescriptor
{
return $this->handlerDescriptor;
}
}
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* A stamp that should not be included with the Envelope if sent to a transport.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
interface NonSendableStampInterface extends StampInterface
{
}
@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\Messenger\Middleware\SendMessageMiddleware;
/**
* Marker stamp for a received message.
*
* This is mainly used by the `SendMessageMiddleware` middleware to identify
* a message should not be sent if it was just received.
*
* @see SendMessageMiddleware
*
* @author Samuel Roze <samuel.roze@gmail.com>
*/
final class ReceivedStamp implements NonSendableStampInterface
{
private $transportName;
public function __construct(string $transportName)
{
$this->transportName = $transportName;
}
public function getTransportName(): string
{
return $this->transportName;
}
}
@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Messenger\Envelope;
/**
* Stamp applied when a messages needs to be redelivered.
*/
final class RedeliveryStamp implements StampInterface
{
private $retryCount;
private $redeliveredAt;
private $exceptionMessage;
private $flattenException;
/**
* @param \DateTimeInterface|null $redeliveredAt
*/
public function __construct(int $retryCount, $redeliveredAt = null)
{
if (2 < \func_num_args() || null !== $redeliveredAt && !$redeliveredAt instanceof \DateTimeInterface) {
trigger_deprecation('symfony/messenger', '5.2', sprintf('Using parameters "$exceptionMessage" or "$flattenException" of class "%s" is deprecated, use "%s" instead and/or pass "$redeliveredAt" as parameter #2.', self::class, ErrorDetailsStamp::class));
$this->exceptionMessage = $redeliveredAt instanceof \DateTimeInterface ? null : $redeliveredAt;
$redeliveredAt = 4 <= \func_num_args() ? func_get_arg(3) : ($redeliveredAt instanceof \DateTimeInterface ? $redeliveredAt : null);
$this->flattenException = 3 <= \func_num_args() ? func_get_arg(2) : null;
}
$this->retryCount = $retryCount;
$this->redeliveredAt = $redeliveredAt ?? new \DateTimeImmutable();
}
public static function getRetryCountFromEnvelope(Envelope $envelope): int
{
/** @var self|null $retryMessageStamp */
$retryMessageStamp = $envelope->last(self::class);
return $retryMessageStamp ? $retryMessageStamp->getRetryCount() : 0;
}
public function getRetryCount(): int
{
return $this->retryCount;
}
/**
* @deprecated since Symfony 5.2, use ErrorDetailsStamp instead.
*/
public function getExceptionMessage(): ?string
{
trigger_deprecation('symfony/messenger', '5.2', sprintf('Using the "getExceptionMessage()" method of the "%s" class is deprecated, use the "%s" class instead.', self::class, ErrorDetailsStamp::class));
return $this->exceptionMessage;
}
/**
* @deprecated since Symfony 5.2, use ErrorDetailsStamp instead.
*/
public function getFlattenException(): ?FlattenException
{
trigger_deprecation('symfony/messenger', '5.2', sprintf('Using the "getFlattenException()" method of the "%s" class is deprecated, use the "%s" class instead.', self::class, ErrorDetailsStamp::class));
return $this->flattenException;
}
public function getRedeliveredAt(): \DateTimeInterface
{
return $this->redeliveredAt;
}
}
@@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class RouterContextStamp implements StampInterface
{
private $baseUrl;
private $method;
private $host;
private $scheme;
private $httpPort;
private $httpsPort;
private $pathInfo;
private $queryString;
public function __construct(string $baseUrl, string $method, string $host, string $scheme, int $httpPort, int $httpsPort, string $pathInfo, string $queryString)
{
$this->baseUrl = $baseUrl;
$this->method = $method;
$this->host = $host;
$this->scheme = $scheme;
$this->httpPort = $httpPort;
$this->httpsPort = $httpsPort;
$this->pathInfo = $pathInfo;
$this->queryString = $queryString;
}
public function getBaseUrl(): string
{
return $this->baseUrl;
}
public function getMethod(): string
{
return $this->method;
}
public function getHost(): string
{
return $this->host;
}
public function getScheme(): string
{
return $this->scheme;
}
public function getHttpPort(): int
{
return $this->httpPort;
}
public function getHttpsPort(): int
{
return $this->httpsPort;
}
public function getPathInfo(): string
{
return $this->pathInfo;
}
public function getQueryString(): string
{
return $this->queryString;
}
}
@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* Marker stamp identifying a message sent by the `SendMessageMiddleware`.
*
* @see \Symfony\Component\Messenger\Middleware\SendMessageMiddleware
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
final class SentStamp implements NonSendableStampInterface
{
private $senderClass;
private $senderAlias;
public function __construct(string $senderClass, ?string $senderAlias = null)
{
$this->senderAlias = $senderAlias;
$this->senderClass = $senderClass;
}
public function getSenderClass(): string
{
return $this->senderClass;
}
public function getSenderAlias(): ?string
{
return $this->senderAlias;
}
}
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* Stamp applied when a message is sent to the failure transport.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
final class SentToFailureTransportStamp implements StampInterface
{
private $originalReceiverName;
public function __construct(string $originalReceiverName)
{
$this->originalReceiverName = $originalReceiverName;
}
public function getOriginalReceiverName(): string
{
return $this->originalReceiverName;
}
}
@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
final class SerializerStamp implements StampInterface
{
private $context;
public function __construct(array $context)
{
$this->context = $context;
}
public function getContext(): array
{
return $this->context;
}
}
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* An envelope stamp related to a message.
*
* Stamps must be serializable value objects for transport.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
interface StampInterface
{
}
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
/**
* Added by a sender or receiver to indicate the id of this message in that transport.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
final class TransportMessageIdStamp implements StampInterface
{
private $id;
/**
* @param mixed $id some "identifier" of the message in a transport
*/
public function __construct($id)
{
$this->id = $id;
}
public function getId()
{
return $this->id;
}
}
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\Validator\Constraints\GroupSequence;
/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
final class ValidationStamp implements StampInterface
{
private $groups;
/**
* @param string[]|GroupSequence $groups
*/
public function __construct($groups)
{
$this->groups = $groups;
}
public function getGroups()
{
return $this->groups;
}
}
@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Test\Middleware;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
use Symfony\Component\Messenger\Middleware\StackMiddleware;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class MiddlewareTestCase extends TestCase
{
protected function getStackMock(bool $nextIsCalled = true)
{
if (!$nextIsCalled) {
$stack = $this->createMock(StackInterface::class);
$stack
->expects($this->never())
->method('next')
;
return $stack;
}
$nextMiddleware = $this->createMock(MiddlewareInterface::class);
$nextMiddleware
->expects($this->once())
->method('handle')
->willReturnCallback(function (Envelope $envelope, StackInterface $stack): Envelope {
return $envelope;
})
;
return new StackMiddleware($nextMiddleware);
}
protected function getThrowingStackMock(?\Throwable $throwable = null)
{
$nextMiddleware = $this->createMock(MiddlewareInterface::class);
$nextMiddleware
->expects($this->once())
->method('handle')
->willThrowException($throwable ?? new \RuntimeException('Thrown from next middleware.'))
;
return new StackMiddleware($nextMiddleware);
}
}

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