feat(demo): add story 1 — Sorano: Rock and Time
This commit is contained in:
@@ -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
@@ -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
|
||||
+298
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+226
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+153
@@ -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;
|
||||
}
|
||||
}
|
||||
+149
@@ -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'];
|
||||
}
|
||||
}
|
||||
+400
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
+45
@@ -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;
|
||||
}
|
||||
}
|
||||
+38
@@ -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],
|
||||
];
|
||||
}
|
||||
}
|
||||
+37
@@ -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],
|
||||
];
|
||||
}
|
||||
}
|
||||
+50
@@ -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],
|
||||
];
|
||||
}
|
||||
}
|
||||
+173
@@ -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));
|
||||
}
|
||||
}
|
||||
plugins/email/vendor/symfony/messenger/EventListener/SendFailedMessageToFailureTransportListener.php
Vendored
+104
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+57
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
+63
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
+55
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
+57
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
+71
@@ -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();
|
||||
}
|
||||
}
|
||||
+51
@@ -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],
|
||||
];
|
||||
}
|
||||
}
|
||||
+63
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
+48
@@ -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);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+19
@@ -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
|
||||
{
|
||||
}
|
||||
+19
@@ -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
|
||||
{
|
||||
}
|
||||
+19
@@ -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
|
||||
{
|
||||
}
|
||||
+24
@@ -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
|
||||
{
|
||||
}
|
||||
+21
@@ -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
|
||||
{
|
||||
}
|
||||
+19
@@ -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);
|
||||
}
|
||||
}
|
||||
+19
@@ -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
|
||||
{
|
||||
}
|
||||
+24
@@ -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
|
||||
{
|
||||
}
|
||||
+21
@@ -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
|
||||
{
|
||||
}
|
||||
+41
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
+49
@@ -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
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+39
@@ -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);
|
||||
}
|
||||
}
|
||||
+133
@@ -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;
|
||||
}
|
||||
}
|
||||
+36
@@ -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);
|
||||
}
|
||||
}
|
||||
+147
@@ -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;
|
||||
}
|
||||
+50
@@ -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);
|
||||
}
|
||||
}
|
||||
+88
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
}
|
||||
+60
@@ -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
Reference in New Issue
Block a user