diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bdf2dcb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +/Tests export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +phpunit.xml.dist export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..28a46e9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: mongodb + + - run: php Tests/fix_composer_json.php + + - uses: "ramsey/composer-install@v1" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 658dcab..0000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - php Tests/fix_composer_json.php - - composer self-update - - composer install - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/Client/ArrayProcessorRegistry.php b/ArrayProcessorRegistry.php similarity index 55% rename from Client/ArrayProcessorRegistry.php rename to ArrayProcessorRegistry.php index d6cb300..592908c 100644 --- a/Client/ArrayProcessorRegistry.php +++ b/ArrayProcessorRegistry.php @@ -1,37 +1,33 @@ processors = $processors; + $this->processors = []; + array_walk($processors, function (Processor $processor, string $key) { + $this->processors[$key] = $processor; + }); } - /** - * @param string $name - * @param PsrProcessor $processor - */ - public function add($name, PsrProcessor $processor) + public function add(string $name, Processor $processor): void { $this->processors[$name] = $processor; } - /** - * {@inheritdoc} - */ - public function get($processorName) + public function get(string $processorName): Processor { if (false == isset($this->processors[$processorName])) { throw new \LogicException(sprintf('Processor was not found. processorName: "%s"', $processorName)); diff --git a/Client/Amqp/AmqpDriver.php b/Client/Amqp/AmqpDriver.php deleted file mode 100644 index e8b5871..0000000 --- a/Client/Amqp/AmqpDriver.php +++ /dev/null @@ -1,205 +0,0 @@ -context = $context; - $this->config = $config; - $this->queueMetaRegistry = $queueMetaRegistry; - } - - /** - * {@inheritdoc} - */ - public function sendToRouter(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_TOPIC_NAME)) { - throw new \LogicException('Topic name parameter is required but is not set'); - } - - $topic = $this->createRouterTopic(); - $transportMessage = $this->createTransportMessage($message); - - $this->context->createProducer()->send($topic, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException('Processor name parameter is required but is not set'); - } - - if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException('Queue name parameter is required but is not set'); - } - - $transportMessage = $this->createTransportMessage($message); - $destination = $this->createQueue($queueName); - - $this->context->createProducer()->send($destination, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function setupBroker(LoggerInterface $logger = null) - { - $logger = $logger ?: new NullLogger(); - $log = function ($text, ...$args) use ($logger) { - $logger->debug(sprintf('[AmqpDriver] '.$text, ...$args)); - }; - - // setup router - $routerTopic = $this->createRouterTopic(); - $routerQueue = $this->createQueue($this->config->getRouterQueueName()); - - $log('Declare router exchange: %s', $routerTopic->getTopicName()); - $this->context->declareTopic($routerTopic); - $log('Declare router queue: %s', $routerQueue->getQueueName()); - $this->context->declareQueue($routerQueue); - $log('Bind router queue to exchange: %s -> %s', $routerQueue->getQueueName(), $routerTopic->getTopicName()); - $this->context->bind(new AmqpBind($routerTopic, $routerQueue, $routerQueue->getQueueName())); - - // setup queues - foreach ($this->queueMetaRegistry->getQueuesMeta() as $meta) { - $queue = $this->createQueue($meta->getClientName()); - - $log('Declare processor queue: %s', $queue->getQueueName()); - $this->context->declareQueue($queue); - } - } - - /** - * {@inheritdoc} - * - * @return AmqpQueue - */ - public function createQueue($queueName) - { - $transportName = $this->queueMetaRegistry->getQueueMeta($queueName)->getTransportName(); - - $queue = $this->context->createQueue($transportName); - $queue->addFlag(AmqpQueue::FLAG_DURABLE); - - return $queue; - } - - /** - * {@inheritdoc} - * - * @return AmqpMessage - */ - public function createTransportMessage(Message $message) - { - $headers = $message->getHeaders(); - $properties = $message->getProperties(); - - $transportMessage = $this->context->createMessage(); - $transportMessage->setBody($message->getBody()); - $transportMessage->setHeaders($headers); - $transportMessage->setProperties($properties); - $transportMessage->setMessageId($message->getMessageId()); - $transportMessage->setTimestamp($message->getTimestamp()); - $transportMessage->setReplyTo($message->getReplyTo()); - $transportMessage->setCorrelationId($message->getCorrelationId()); - $transportMessage->setContentType($message->getContentType()); - $transportMessage->setDeliveryMode(AmqpMessage::DELIVERY_MODE_PERSISTENT); - - if ($message->getExpire()) { - $transportMessage->setExpiration((string) ($message->getExpire() * 1000)); - } - - return $transportMessage; - } - - /** - * @param AmqpMessage $message - * - * {@inheritdoc} - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = new Message(); - - $clientMessage->setBody($message->getBody()); - $clientMessage->setHeaders($message->getHeaders()); - $clientMessage->setProperties($message->getProperties()); - $clientMessage->setContentType($message->getContentType()); - - if ($expiration = $message->getExpiration()) { - if (false == is_numeric($expiration)) { - throw new \LogicException(sprintf('expiration header is not numeric. "%s"', $expiration)); - } - - $clientMessage->setExpire((int) ((int) $expiration) / 1000); - } - - $clientMessage->setMessageId($message->getMessageId()); - $clientMessage->setTimestamp($message->getTimestamp()); - $clientMessage->setReplyTo($message->getReplyTo()); - $clientMessage->setCorrelationId($message->getCorrelationId()); - - return $clientMessage; - } - - /** - * @return Config - */ - public function getConfig() - { - return $this->config; - } - - /** - * @return AmqpTopic - */ - private function createRouterTopic() - { - $topic = $this->context->createTopic( - $this->config->createTransportRouterTopicName($this->config->getRouterTopicName()) - ); - $topic->setType(AmqpTopic::TYPE_FANOUT); - $topic->addFlag(AmqpTopic::FLAG_DURABLE); - - return $topic; - } -} diff --git a/Client/Amqp/RabbitMqDriver.php b/Client/Amqp/RabbitMqDriver.php deleted file mode 100644 index 518b900..0000000 --- a/Client/Amqp/RabbitMqDriver.php +++ /dev/null @@ -1,154 +0,0 @@ -config = $config; - $this->context = $context; - $this->queueMetaRegistry = $queueMetaRegistry; - - $this->priorityMap = [ - MessagePriority::VERY_LOW => 0, - MessagePriority::LOW => 1, - MessagePriority::NORMAL => 2, - MessagePriority::HIGH => 3, - MessagePriority::VERY_HIGH => 4, - ]; - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException('Processor name parameter is required but is not set'); - } - - if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException('Queue name parameter is required but is not set'); - } - - $transportMessage = $this->createTransportMessage($message); - $destination = $this->createQueue($queueName); - $producer = $this->context->createProducer(); - - if ($message->getDelay()) { - $producer->setDeliveryDelay($message->getDelay() * 1000); - } - - $producer->send($destination, $transportMessage); - } - - /** - * {@inheritdoc} - * - * @return AmqpQueue - */ - public function createQueue($queueName) - { - $queue = parent::createQueue($queueName); - $queue->setArguments(['x-max-priority' => 4]); - - return $queue; - } - - /** - * {@inheritdoc} - * - * @return AmqpMessage - */ - public function createTransportMessage(Message $message) - { - $transportMessage = parent::createTransportMessage($message); - - if ($priority = $message->getPriority()) { - if (false == array_key_exists($priority, $this->priorityMap)) { - throw new \InvalidArgumentException(sprintf( - 'Given priority could not be converted to client\'s one. Got: %s', - $priority - )); - } - - $transportMessage->setPriority($this->priorityMap[$priority]); - } - - if ($message->getDelay()) { - if (false == $this->config->getTransportOption('delay_strategy', false)) { - throw new LogicException('The message delaying is not supported. In order to use delay feature install RabbitMQ delay strategy.'); - } - - $transportMessage->setProperty('enqueue-delay', $message->getDelay() * 1000); - } - - return $transportMessage; - } - - /** - * @param AmqpMessage $message - * - * {@inheritdoc} - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = parent::createClientMessage($message); - - if ($priority = $message->getPriority()) { - if (false === $clientPriority = array_search($priority, $this->priorityMap, true)) { - throw new \LogicException(sprintf('Cant convert transport priority to client: "%s"', $priority)); - } - - $clientMessage->setPriority($clientPriority); - } - - if ($delay = $message->getProperty('enqueue-delay')) { - if (false == is_numeric($delay)) { - throw new \LogicException(sprintf('"enqueue-delay" header is not numeric. "%s"', $delay)); - } - - $clientMessage->setDelay((int) ((int) $delay) / 1000); - } - - return $clientMessage; - } -} diff --git a/Client/ChainExtension.php b/Client/ChainExtension.php index c202e98..655b75f 100644 --- a/Client/ChainExtension.php +++ b/Client/ChainExtension.php @@ -2,38 +2,101 @@ namespace Enqueue\Client; -class ChainExtension implements ExtensionInterface +final class ChainExtension implements ExtensionInterface { /** - * @var ExtensionInterface[] + * @var PreSendEventExtensionInterface[] */ - private $extensions; + private $preSendEventExtensions; /** - * @param ExtensionInterface[] $extensions + * @var PreSendCommandExtensionInterface[] */ + private $preSendCommandExtensions; + + /** + * @var DriverPreSendExtensionInterface[] + */ + private $driverPreSendExtensions; + + /** + * @var PostSendExtensionInterface[] + */ + private $postSendExtensions; + public function __construct(array $extensions) { - $this->extensions = $extensions; + $this->preSendEventExtensions = []; + $this->preSendCommandExtensions = []; + $this->driverPreSendExtensions = []; + $this->postSendExtensions = []; + + array_walk($extensions, function ($extension) { + if ($extension instanceof ExtensionInterface) { + $this->preSendEventExtensions[] = $extension; + $this->preSendCommandExtensions[] = $extension; + $this->driverPreSendExtensions[] = $extension; + $this->postSendExtensions[] = $extension; + + return; + } + + $extensionValid = false; + if ($extension instanceof PreSendEventExtensionInterface) { + $this->preSendEventExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof PreSendCommandExtensionInterface) { + $this->preSendCommandExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof DriverPreSendExtensionInterface) { + $this->driverPreSendExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof PostSendExtensionInterface) { + $this->postSendExtensions[] = $extension; + + $extensionValid = true; + } + + if (false == $extensionValid) { + throw new \LogicException(sprintf('Invalid extension given %s', $extension::class)); + } + }); + } + + public function onPreSendEvent(PreSend $context): void + { + foreach ($this->preSendEventExtensions as $extension) { + $extension->onPreSendEvent($context); + } } - /** - * {@inheritdoc} - */ - public function onPreSend($topic, Message $message) + public function onPreSendCommand(PreSend $context): void { - foreach ($this->extensions as $extension) { - $extension->onPreSend($topic, $message); + foreach ($this->preSendCommandExtensions as $extension) { + $extension->onPreSendCommand($context); } } - /** - * {@inheritdoc} - */ - public function onPostSend($topic, Message $message) + public function onDriverPreSend(DriverPreSend $context): void + { + foreach ($this->driverPreSendExtensions as $extension) { + $extension->onDriverPreSend($context); + } + } + + public function onPostSend(PostSend $context): void { - foreach ($this->extensions as $extension) { - $extension->onPostSend($topic, $message); + foreach ($this->postSendExtensions as $extension) { + $extension->onPostSend($context); } } } diff --git a/Client/CommandSubscriberInterface.php b/Client/CommandSubscriberInterface.php index 99f6b27..d7b06da 100644 --- a/Client/CommandSubscriberInterface.php +++ b/Client/CommandSubscriberInterface.php @@ -2,6 +2,15 @@ namespace Enqueue\Client; +/** + * @phpstan-type CommandConfig = array{ + * command: string, + * processor?: string, + * queue?: string, + * prefix_queue?: bool, + * exclusive?: bool, + * } + */ interface CommandSubscriberInterface { /** @@ -12,17 +21,40 @@ interface CommandSubscriberInterface * or * * [ - * 'processorName' => 'aCommandName', - * 'queueName' => 'a_client_queue_name', - * 'queueNameHardcoded' => true, + * 'command' => 'aSubscribedCommand', + * 'processor' => 'aProcessorName', + * 'queue' => 'a_client_queue_name', + * 'prefix_queue' => true, * 'exclusive' => true, * ] * - * queueName, exclusive and queueNameHardcoded are optional. + * or + * + * [ + * [ + * 'command' => 'aSubscribedCommand', + * 'processor' => 'aProcessorName', + * 'queue' => 'a_client_queue_name', + * 'prefix_queue' => true, + * 'exclusive' => true, + * ], + * [ + * 'command' => 'aSubscribedCommand', + * 'processor' => 'aProcessorName', + * 'queue' => 'a_client_queue_name', + * 'prefix_queue' => true, + * 'exclusive' => true, + * ] + * ] + * + * queue, processor, prefix_queue, and exclusive are optional. + * It is possible to pass other options, they could be accessible on a route instance through options. * - * Note: If you set queueNameHardcoded to true then the queueName is used as is and therefor the driver is not used to create a transport queue name. + * Note: If you set "prefix_queue" to true then the "queue" is used as is and therefor the driver is not used to create a transport queue name. * * @return string|array + * + * @phpstan-return string|CommandConfig|array */ public static function getSubscribedCommand(); } diff --git a/Client/Config.php b/Client/Config.php index d5d8b07..8210dff 100644 --- a/Client/Config.php +++ b/Client/Config.php @@ -4,12 +4,13 @@ class Config { - const PARAMETER_TOPIC_NAME = 'enqueue.topic_name'; - const PARAMETER_COMMAND_NAME = 'enqueue.command_name'; - const PARAMETER_PROCESSOR_NAME = 'enqueue.processor_name'; - const PARAMETER_PROCESSOR_QUEUE_NAME = 'enqueue.processor_queue_name'; - const DEFAULT_PROCESSOR_QUEUE_NAME = 'default'; - const COMMAND_TOPIC = '__command__'; + public const TOPIC = 'enqueue.topic'; + public const COMMAND = 'enqueue.command'; + public const PROCESSOR = 'enqueue.processor'; + public const EXPIRE = 'enqueue.expire'; + public const PRIORITY = 'enqueue.priority'; + public const DELAY = 'enqueue.delay'; + public const CONTENT_TYPE = 'enqueue.content_type'; /** * @var string @@ -19,27 +20,32 @@ class Config /** * @var string */ - private $appName; + private $separator; /** * @var string */ - private $routerTopicName; + private $app; /** * @var string */ - private $routerQueueName; + private $routerTopic; /** * @var string */ - private $defaultProcessorQueueName; + private $routerQueue; /** * @var string */ - private $routerProcessorName; + private $defaultQueue; + + /** + * @var string + */ + private $routerProcessor; /** * @var array @@ -47,116 +53,126 @@ class Config private $transportConfig; /** - * @param string $prefix - * @param string $appName - * @param string $routerTopicName - * @param string $routerQueueName - * @param string $defaultProcessorQueueName - * @param string $routerProcessorName - * @param array $transportConfig + * @var array */ - public function __construct($prefix, $appName, $routerTopicName, $routerQueueName, $defaultProcessorQueueName, $routerProcessorName, array $transportConfig = []) - { - $this->prefix = $prefix; - $this->appName = $appName; - $this->routerTopicName = $routerTopicName; - $this->routerQueueName = $routerQueueName; - $this->defaultProcessorQueueName = $defaultProcessorQueueName; - $this->routerProcessorName = $routerProcessorName; + private $driverConfig; + + public function __construct( + string $prefix, + string $separator, + string $app, + string $routerTopic, + string $routerQueue, + string $defaultQueue, + string $routerProcessor, + array $transportConfig, + array $driverConfig, + ) { + $this->prefix = trim($prefix); + $this->app = trim($app); + + $this->routerTopic = trim($routerTopic); + if (empty($this->routerTopic)) { + throw new \InvalidArgumentException('Router topic is empty.'); + } + + $this->routerQueue = trim($routerQueue); + if (empty($this->routerQueue)) { + throw new \InvalidArgumentException('Router queue is empty.'); + } + + $this->defaultQueue = trim($defaultQueue); + if (empty($this->defaultQueue)) { + throw new \InvalidArgumentException('Default processor queue name is empty.'); + } + + $this->routerProcessor = trim($routerProcessor); + if (empty($this->routerProcessor)) { + throw new \InvalidArgumentException('Router processor name is empty.'); + } + $this->transportConfig = $transportConfig; + $this->driverConfig = $driverConfig; + + $this->separator = $separator; } - /** - * @return string - */ - public function getRouterTopicName() + public function getPrefix(): string { - return $this->routerTopicName; + return $this->prefix; } - /** - * @return string - */ - public function getRouterQueueName() + public function getSeparator(): string { - return $this->routerQueueName; + return $this->separator; } - /** - * @return string - */ - public function getDefaultProcessorQueueName() + public function getApp(): string { - return $this->defaultProcessorQueueName; + return $this->app; } - /** - * @return string - */ - public function getRouterProcessorName() + public function getRouterTopic(): string { - return $this->routerProcessorName; + return $this->routerTopic; } - /** - * @param string $name - * - * @return string - */ - public function createTransportRouterTopicName($name) + public function getRouterQueue(): string { - return strtolower(implode('.', array_filter([trim($this->prefix), trim($name)]))); + return $this->routerQueue; } - /** - * @param string $name - * - * @return string - */ - public function createTransportQueueName($name) + public function getDefaultQueue(): string { - return strtolower(implode('.', array_filter([trim($this->prefix), trim($this->appName), trim($name)]))); + return $this->defaultQueue; } - /** - * @param string $name - * @param mixed|null $default - * - * @return array - */ - public function getTransportOption($name, $default = null) + public function getRouterProcessor(): string + { + return $this->routerProcessor; + } + + public function getTransportOption(string $name, $default = null) { return array_key_exists($name, $this->transportConfig) ? $this->transportConfig[$name] : $default; } - /** - * @param string|null $prefix - * @param string|null $appName - * @param string|null $routerTopicName - * @param string|null $routerQueueName - * @param string|null $defaultProcessorQueueName - * @param string|null $routerProcessorName - * @param array $transportConfig - * - * @return static - */ + public function getTransportOptions(): array + { + return $this->transportConfig; + } + + public function getDriverOption(string $name, $default = null) + { + return array_key_exists($name, $this->driverConfig) ? $this->driverConfig[$name] : $default; + } + + public function getDriverOptions(): array + { + return $this->driverConfig; + } + public static function create( - $prefix = null, - $appName = null, - $routerTopicName = null, - $routerQueueName = null, - $defaultProcessorQueueName = null, - $routerProcessorName = null, - array $transportConfig = [] - ) { - return new static( + ?string $prefix = null, + ?string $separator = null, + ?string $app = null, + ?string $routerTopic = null, + ?string $routerQueue = null, + ?string $defaultQueue = null, + ?string $routerProcessor = null, + array $transportConfig = [], + array $driverConfig = [], + ): self { + return new self( $prefix ?: '', - $appName ?: '', - $routerTopicName ?: 'router', - $routerQueueName ?: 'default', - $defaultProcessorQueueName ?: 'default', - $routerProcessorName ?: 'router', - $transportConfig + $separator ?: '.', + $app ?: '', + $routerTopic ?: 'router', + $routerQueue ?: 'default', + $defaultQueue ?: 'default', + $routerProcessor ?: 'router', + $transportConfig, + $driverConfig ); } } diff --git a/Client/ConsumptionExtension/DelayRedeliveredMessageExtension.php b/Client/ConsumptionExtension/DelayRedeliveredMessageExtension.php index 98fa483..475e2cf 100644 --- a/Client/ConsumptionExtension/DelayRedeliveredMessageExtension.php +++ b/Client/ConsumptionExtension/DelayRedeliveredMessageExtension.php @@ -3,16 +3,13 @@ namespace Enqueue\Client\ConsumptionExtension; use Enqueue\Client\DriverInterface; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\MessageReceivedExtensionInterface; use Enqueue\Consumption\Result; -class DelayRedeliveredMessageExtension implements ExtensionInterface +class DelayRedeliveredMessageExtension implements MessageReceivedExtensionInterface { - use EmptyExtensionTrait; - - const PROPERTY_REDELIVER_COUNT = 'enqueue.redelivery_count'; + public const PROPERTY_REDELIVER_COUNT = 'enqueue.redelivery_count'; /** * @var DriverInterface @@ -27,8 +24,7 @@ class DelayRedeliveredMessageExtension implements ExtensionInterface private $delay; /** - * @param DriverInterface $driver - * @param int $delay The number of seconds the message should be delayed + * @param int $delay The number of seconds the message should be delayed */ public function __construct(DriverInterface $driver, $delay) { @@ -36,12 +32,9 @@ public function __construct(DriverInterface $driver, $delay) $this->delay = $delay; } - /** - * {@inheritdoc} - */ - public function onPreReceived(Context $context) + public function onMessageReceived(MessageReceived $context): void { - $message = $context->getPsrMessage(); + $message = $context->getMessage(); if (false == $message->isRedelivered()) { return; } diff --git a/Client/ConsumptionExtension/ExclusiveCommandExtension.php b/Client/ConsumptionExtension/ExclusiveCommandExtension.php index a9afde0..7ab88ae 100644 --- a/Client/ConsumptionExtension/ExclusiveCommandExtension.php +++ b/Client/ConsumptionExtension/ExclusiveCommandExtension.php @@ -3,83 +3,75 @@ namespace Enqueue\Client\ConsumptionExtension; use Enqueue\Client\Config; -use Enqueue\Client\ExtensionInterface as ClientExtensionInterface; -use Enqueue\Client\Message; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface as ConsumptionExtensionInterface; +use Enqueue\Client\DriverInterface; +use Enqueue\Client\Route; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\MessageReceivedExtensionInterface; -class ExclusiveCommandExtension implements ConsumptionExtensionInterface, ClientExtensionInterface +final class ExclusiveCommandExtension implements MessageReceivedExtensionInterface { - use EmptyExtensionTrait; - /** - * @var string[] + * @var DriverInterface */ - private $queueNameToProcessorNameMap; + private $driver; /** - * @var string[] + * @var Route[] */ - private $processorNameToQueueNameMap; + private $queueToRouteMap; - /** - * @param string[] $queueNameToProcessorNameMap - */ - public function __construct(array $queueNameToProcessorNameMap) + public function __construct(DriverInterface $driver) { - $this->queueNameToProcessorNameMap = $queueNameToProcessorNameMap; - $this->processorNameToQueueNameMap = array_flip($queueNameToProcessorNameMap); + $this->driver = $driver; } - public function onPreReceived(Context $context) + public function onMessageReceived(MessageReceived $context): void { - $message = $context->getPsrMessage(); - $queue = $context->getPsrQueue(); - - if ($message->getProperty(Config::PARAMETER_TOPIC_NAME)) { + $message = $context->getMessage(); + if ($message->getProperty(Config::TOPIC)) { return; } - if ($message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { + if ($message->getProperty(Config::COMMAND)) { return; } - if ($message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { + if ($message->getProperty(Config::PROCESSOR)) { return; } - if ($message->getProperty(Config::PARAMETER_COMMAND_NAME)) { - return; + + if (null === $this->queueToRouteMap) { + $this->queueToRouteMap = $this->buildMap(); } - if (array_key_exists($queue->getQueueName(), $this->queueNameToProcessorNameMap)) { + $queue = $context->getConsumer()->getQueue(); + if (array_key_exists($queue->getQueueName(), $this->queueToRouteMap)) { $context->getLogger()->debug('[ExclusiveCommandExtension] This is a exclusive command queue and client\'s properties are not set. Setting them'); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, Config::COMMAND_TOPIC); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $queue->getQueueName()); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, $this->queueNameToProcessorNameMap[$queue->getQueueName()]); - $message->setProperty(Config::PARAMETER_COMMAND_NAME, $this->queueNameToProcessorNameMap[$queue->getQueueName()]); + $route = $this->queueToRouteMap[$queue->getQueueName()]; + $message->setProperty(Config::PROCESSOR, $route->getProcessor()); + $message->setProperty(Config::COMMAND, $route->getSource()); } } - /** - * {@inheritdoc} - */ - public function onPreSend($topic, Message $message) + private function buildMap(): array { - if (Config::COMMAND_TOPIC != $topic) { - return; - } + $map = []; + foreach ($this->driver->getRouteCollection()->all() as $route) { + if (false == $route->isCommand()) { + continue; + } + + if (false == $route->isProcessorExclusive()) { + continue; + } + + $queueName = $this->driver->createRouteQueue($route)->getQueueName(); + if (array_key_exists($queueName, $map)) { + throw new \LogicException('The queue name has been already bound by another exclusive command processor'); + } - $commandName = $message->getProperty(Config::PARAMETER_COMMAND_NAME); - if (array_key_exists($commandName, $this->processorNameToQueueNameMap)) { - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, $commandName); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $this->processorNameToQueueNameMap[$commandName]); + $map[$queueName] = $route; } - } - /** - * {@inheritdoc} - */ - public function onPostSend($topic, Message $message) - { + return $map; } } diff --git a/Client/ConsumptionExtension/FlushSpoolProducerExtension.php b/Client/ConsumptionExtension/FlushSpoolProducerExtension.php index efb6848..6682cad 100644 --- a/Client/ConsumptionExtension/FlushSpoolProducerExtension.php +++ b/Client/ConsumptionExtension/FlushSpoolProducerExtension.php @@ -3,36 +3,29 @@ namespace Enqueue\Client\ConsumptionExtension; use Enqueue\Client\SpoolProducer; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\End; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\EndExtensionInterface; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; -class FlushSpoolProducerExtension implements ExtensionInterface +class FlushSpoolProducerExtension implements PostMessageReceivedExtensionInterface, EndExtensionInterface { - use EmptyExtensionTrait; - /** * @var SpoolProducer */ private $producer; - /** - * @param SpoolProducer $producer - */ public function __construct(SpoolProducer $producer) { $this->producer = $producer; } - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) + public function onPostMessageReceived(PostMessageReceived $context): void { $this->producer->flush(); } - public function onInterrupted(Context $context) + public function onEnd(End $context): void { $this->producer->flush(); } diff --git a/Client/ConsumptionExtension/LogExtension.php b/Client/ConsumptionExtension/LogExtension.php new file mode 100644 index 0000000..693be20 --- /dev/null +++ b/Client/ConsumptionExtension/LogExtension.php @@ -0,0 +1,69 @@ +getResult(); + $message = $context->getMessage(); + + $logLevel = Result::REJECT == ((string) $result) ? LogLevel::ERROR : LogLevel::INFO; + + if ($command = $message->getProperty(Config::COMMAND)) { + $reason = ''; + $logMessage = "[client] Processed {command}\t{body}\t{result}"; + if ($result instanceof Result && $result->getReason()) { + $reason = $result->getReason(); + + $logMessage .= ' {reason}'; + } + + $context->getLogger()->log($logLevel, $logMessage, [ + 'result' => str_replace('enqueue.', '', $result), + 'reason' => $reason, + 'command' => $command, + 'queueName' => $context->getConsumer()->getQueue()->getQueueName(), + 'body' => Stringify::that($message->getBody()), + 'properties' => Stringify::that($message->getProperties()), + 'headers' => Stringify::that($message->getHeaders()), + ]); + + return; + } + + $topic = $message->getProperty(Config::TOPIC); + $processor = $message->getProperty(Config::PROCESSOR); + if ($topic && $processor) { + $reason = ''; + $logMessage = "[client] Processed {topic} -> {processor}\t{body}\t{result}"; + if ($result instanceof Result && $result->getReason()) { + $reason = $result->getReason(); + + $logMessage .= ' {reason}'; + } + + $context->getLogger()->log($logLevel, $logMessage, [ + 'result' => str_replace('enqueue.', '', $result), + 'reason' => $reason, + 'topic' => $topic, + 'processor' => $processor, + 'queueName' => $context->getConsumer()->getQueue()->getQueueName(), + 'body' => Stringify::that($message->getBody()), + 'properties' => Stringify::that($message->getProperties()), + 'headers' => Stringify::that($message->getHeaders()), + ]); + + return; + } + + parent::onPostMessageReceived($context); + } +} diff --git a/Client/ConsumptionExtension/SetRouterPropertiesExtension.php b/Client/ConsumptionExtension/SetRouterPropertiesExtension.php index d294a9d..0d22783 100644 --- a/Client/ConsumptionExtension/SetRouterPropertiesExtension.php +++ b/Client/ConsumptionExtension/SetRouterPropertiesExtension.php @@ -4,45 +4,43 @@ use Enqueue\Client\Config; use Enqueue\Client\DriverInterface; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\MessageReceivedExtensionInterface; -class SetRouterPropertiesExtension implements ExtensionInterface +class SetRouterPropertiesExtension implements MessageReceivedExtensionInterface { - use EmptyExtensionTrait; - /** * @var DriverInterface */ private $driver; - /** - * @param DriverInterface $driver - */ public function __construct(DriverInterface $driver) { $this->driver = $driver; } - /** - * {@inheritdoc} - */ - public function onPreReceived(Context $context) + public function onMessageReceived(MessageReceived $context): void { - $message = $context->getPsrMessage(); - if ($message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { + $message = $context->getMessage(); + if (false == $message->getProperty(Config::TOPIC)) { + return; + } + if ($message->getProperty(Config::PROCESSOR)) { return; } $config = $this->driver->getConfig(); - $queue = $this->driver->createQueue($config->getRouterQueueName()); - if ($context->getPsrQueue()->getQueueName() != $queue->getQueueName()) { + $queue = $this->driver->createQueue($config->getRouterQueue()); + if ($context->getConsumer()->getQueue()->getQueueName() != $queue->getQueueName()) { return; } // RouterProcessor is our default message processor when that header is not set - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, $config->getRouterProcessorName()); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $config->getRouterQueueName()); + $message->setProperty(Config::PROCESSOR, $config->getRouterProcessor()); + + $context->getLogger()->debug( + '[SetRouterPropertiesExtension] '. + sprintf('Set router processor "%s"', $config->getRouterProcessor()) + ); } } diff --git a/Client/ConsumptionExtension/SetupBrokerExtension.php b/Client/ConsumptionExtension/SetupBrokerExtension.php index 8b6aecb..44d610f 100644 --- a/Client/ConsumptionExtension/SetupBrokerExtension.php +++ b/Client/ConsumptionExtension/SetupBrokerExtension.php @@ -3,14 +3,11 @@ namespace Enqueue\Client\ConsumptionExtension; use Enqueue\Client\DriverInterface; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\Start; +use Enqueue\Consumption\StartExtensionInterface; -class SetupBrokerExtension implements ExtensionInterface +class SetupBrokerExtension implements StartExtensionInterface { - use EmptyExtensionTrait; - /** * @var DriverInterface */ @@ -21,19 +18,13 @@ class SetupBrokerExtension implements ExtensionInterface */ private $isDone; - /** - * @param DriverInterface $driver - */ public function __construct(DriverInterface $driver) { $this->driver = $driver; $this->isDone = false; } - /** - * {@inheritdoc} - */ - public function onStart(Context $context) + public function onStart(Start $context): void { if (false == $this->isDone) { $this->isDone = true; diff --git a/Client/DelegateProcessor.php b/Client/DelegateProcessor.php index 6cb8f98..7582c52 100644 --- a/Client/DelegateProcessor.php +++ b/Client/DelegateProcessor.php @@ -2,36 +2,31 @@ namespace Enqueue\Client; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Enqueue\ProcessorRegistryInterface; +use Interop\Queue\Context; +use Interop\Queue\Message as InteropMessage; +use Interop\Queue\Processor; -class DelegateProcessor implements PsrProcessor +class DelegateProcessor implements Processor { /** * @var ProcessorRegistryInterface */ private $registry; - /** - * @param ProcessorRegistryInterface $registry - */ public function __construct(ProcessorRegistryInterface $registry) { $this->registry = $registry; } /** - * {@inheritdoc} + * @return string|object */ - public function process(PsrMessage $message, PsrContext $context) + public function process(InteropMessage $message, Context $context) { - $processorName = $message->getProperty(Config::PARAMETER_PROCESSOR_NAME); + $processorName = $message->getProperty(Config::PROCESSOR); if (false == $processorName) { - throw new \LogicException(sprintf( - 'Got message without required parameter: "%s"', - Config::PARAMETER_PROCESSOR_NAME - )); + throw new \LogicException(sprintf('Got message without required parameter: "%s"', Config::PROCESSOR)); } return $this->registry->get($processorName)->process($message, $context); diff --git a/Client/Driver/AmqpDriver.php b/Client/Driver/AmqpDriver.php new file mode 100644 index 0000000..1def3fb --- /dev/null +++ b/Client/Driver/AmqpDriver.php @@ -0,0 +1,132 @@ +setDeliveryMode(AmqpMessage::DELIVERY_MODE_PERSISTENT); + $transportMessage->setContentType($clientMessage->getContentType()); + + if ($clientMessage->getExpire()) { + $transportMessage->setExpiration($clientMessage->getExpire() * 1000); + } + + $priorityMap = $this->getPriorityMap(); + if ($priority = $clientMessage->getPriority()) { + if (false == array_key_exists($priority, $priorityMap)) { + throw new \InvalidArgumentException(sprintf('Cant convert client priority "%s" to transport one. Could be one of "%s"', $priority, implode('", "', array_keys($priorityMap)))); + } + + $transportMessage->setPriority($priorityMap[$priority]); + } + + return $transportMessage; + } + + public function setupBroker(?LoggerInterface $logger = null): void + { + $logger = $logger ?: new NullLogger(); + $log = function ($text, ...$args) use ($logger) { + $logger->debug(sprintf('[AmqpDriver] '.$text, ...$args)); + }; + + // setup router + $routerTopic = $this->createRouterTopic(); + $log('Declare router exchange: %s', $routerTopic->getTopicName()); + $this->getContext()->declareTopic($routerTopic); + + $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue()); + $log('Declare router queue: %s', $routerQueue->getQueueName()); + $this->getContext()->declareQueue($routerQueue); + + $log('Bind router queue to exchange: %s -> %s', $routerQueue->getQueueName(), $routerTopic->getTopicName()); + $this->getContext()->bind(new AmqpBind($routerTopic, $routerQueue, $routerQueue->getQueueName())); + + // setup queues + $declaredQueues = []; + foreach ($this->getRouteCollection()->all() as $route) { + /** @var AmqpQueue $queue */ + $queue = $this->createRouteQueue($route); + if (array_key_exists($queue->getQueueName(), $declaredQueues)) { + continue; + } + + $log('Declare processor queue: %s', $queue->getQueueName()); + $this->getContext()->declareQueue($queue); + + $declaredQueues[$queue->getQueueName()] = true; + } + } + + /** + * @return AmqpTopic + */ + protected function createRouterTopic(): Destination + { + $topic = $this->doCreateTopic( + $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true) + ); + $topic->setType(AmqpTopic::TYPE_FANOUT); + $topic->addFlag(AmqpTopic::FLAG_DURABLE); + + return $topic; + } + + /** + * @return AmqpQueue + */ + protected function doCreateQueue(string $transportQueueName): InteropQueue + { + /** @var AmqpQueue $queue */ + $queue = parent::doCreateQueue($transportQueueName); + $queue->addFlag(AmqpQueue::FLAG_DURABLE); + + return $queue; + } + + /** + * @param AmqpProducer $producer + * @param AmqpTopic $topic + * @param AmqpMessage $transportMessage + */ + protected function doSendToRouter(InteropProducer $producer, Destination $topic, InteropMessage $transportMessage): void + { + // We should not handle priority, expiration, and delay at this stage. + // The router will take care of it while re-sending the message to the final destinations. + $transportMessage->setPriority(null); + $transportMessage->setExpiration(null); + + $producer->send($topic, $transportMessage); + } +} diff --git a/Client/Driver/DbalDriver.php b/Client/Driver/DbalDriver.php new file mode 100644 index 0000000..34875ef --- /dev/null +++ b/Client/Driver/DbalDriver.php @@ -0,0 +1,29 @@ +debug(sprintf('[DbalDriver] '.$text, ...$args)); + }; + + $log('Creating database table: "%s"', $this->getContext()->getTableName()); + $this->getContext()->createDataBaseTable(); + } +} diff --git a/Client/Driver/FsDriver.php b/Client/Driver/FsDriver.php new file mode 100644 index 0000000..f578b17 --- /dev/null +++ b/Client/Driver/FsDriver.php @@ -0,0 +1,49 @@ +debug(sprintf('[FsDriver] '.$text, ...$args)); + }; + + // setup router + $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue()); + + $log('Declare router queue "%s" file: %s', $routerQueue->getQueueName(), $routerQueue->getFileInfo()); + $this->getContext()->declareDestination($routerQueue); + + // setup queues + $declaredQueues = []; + foreach ($this->getRouteCollection()->all() as $route) { + /** @var FsDestination $queue */ + $queue = $this->createRouteQueue($route); + if (array_key_exists($queue->getQueueName(), $declaredQueues)) { + continue; + } + + $log('Declare processor queue "%s" file: %s', $queue->getQueueName(), $queue->getFileInfo()); + $this->getContext()->declareDestination($queue); + + $declaredQueues[$queue->getQueueName()] = true; + } + } +} diff --git a/Client/Driver/GenericDriver.php b/Client/Driver/GenericDriver.php new file mode 100644 index 0000000..d509677 --- /dev/null +++ b/Client/Driver/GenericDriver.php @@ -0,0 +1,278 @@ +context = $context; + $this->config = $config; + $this->routeCollection = $routeCollection; + } + + public function sendToRouter(Message $message): DriverSendResult + { + if ($message->getProperty(Config::COMMAND)) { + throw new \LogicException('Command must not be send to router but go directly to its processor.'); + } + if (false == $message->getProperty(Config::TOPIC)) { + throw new \LogicException('Topic name parameter is required but is not set'); + } + + $topic = $this->createRouterTopic(); + $transportMessage = $this->createTransportMessage($message); + $producer = $this->getContext()->createProducer(); + + $this->doSendToRouter($producer, $topic, $transportMessage); + + return new DriverSendResult($topic, $transportMessage); + } + + public function sendToProcessor(Message $message): DriverSendResult + { + $topic = $message->getProperty(Config::TOPIC); + $command = $message->getProperty(Config::COMMAND); + + /** @var InteropQueue $queue */ + $queue = null; + $routerProcessor = $this->config->getRouterProcessor(); + $processor = $message->getProperty(Config::PROCESSOR); + if ($topic && $processor && $processor !== $routerProcessor) { + $route = $this->routeCollection->topicAndProcessor($topic, $processor); + if (false == $route) { + throw new \LogicException(sprintf('There is no route for topic "%s" and processor "%s"', $topic, $processor)); + } + + $message->setProperty(Config::PROCESSOR, $route->getProcessor()); + $queue = $this->createRouteQueue($route); + } elseif ($topic && (false == $processor || $processor === $routerProcessor)) { + $message->setProperty(Config::PROCESSOR, $routerProcessor); + + $queue = $this->createQueue($this->config->getRouterQueue()); + } elseif ($command) { + $route = $this->routeCollection->command($command); + if (false == $route) { + throw new \LogicException(sprintf('There is no route for command "%s".', $command)); + } + + $message->setProperty(Config::PROCESSOR, $route->getProcessor()); + $queue = $this->createRouteQueue($route); + } else { + throw new \LogicException('Either topic or command parameter must be set.'); + } + + $transportMessage = $this->createTransportMessage($message); + + $producer = $this->context->createProducer(); + + if (null !== $delay = $transportMessage->getProperty(Config::DELAY)) { + $producer->setDeliveryDelay($delay * 1000); + } + + if (null !== $expire = $transportMessage->getProperty(Config::EXPIRE)) { + $producer->setTimeToLive($expire * 1000); + } + + if (null !== $priority = $transportMessage->getProperty(Config::PRIORITY)) { + $priorityMap = $this->getPriorityMap(); + + $producer->setPriority($priorityMap[$priority]); + } + + $this->doSendToProcessor($producer, $queue, $transportMessage); + + return new DriverSendResult($queue, $transportMessage); + } + + public function setupBroker(?LoggerInterface $logger = null): void + { + } + + public function createQueue(string $clientQueueName, bool $prefix = true): InteropQueue + { + $transportName = $this->createTransportQueueName($clientQueueName, $prefix); + + return $this->doCreateQueue($transportName); + } + + public function createRouteQueue(Route $route): InteropQueue + { + $transportName = $this->createTransportQueueName( + $route->getQueue() ?: $this->config->getDefaultQueue(), + $route->isPrefixQueue() + ); + + return $this->doCreateQueue($transportName); + } + + public function createTransportMessage(Message $clientMessage): InteropMessage + { + $headers = $clientMessage->getHeaders(); + $properties = $clientMessage->getProperties(); + + $transportMessage = $this->context->createMessage(); + $transportMessage->setBody($clientMessage->getBody()); + $transportMessage->setHeaders($headers); + $transportMessage->setProperties($properties); + $transportMessage->setMessageId($clientMessage->getMessageId()); + $transportMessage->setTimestamp($clientMessage->getTimestamp()); + $transportMessage->setReplyTo($clientMessage->getReplyTo()); + $transportMessage->setCorrelationId($clientMessage->getCorrelationId()); + + if ($contentType = $clientMessage->getContentType()) { + $transportMessage->setProperty(Config::CONTENT_TYPE, $contentType); + } + + if ($priority = $clientMessage->getPriority()) { + $transportMessage->setProperty(Config::PRIORITY, $priority); + } + + if ($expire = $clientMessage->getExpire()) { + $transportMessage->setProperty(Config::EXPIRE, $expire); + } + + if ($delay = $clientMessage->getDelay()) { + $transportMessage->setProperty(Config::DELAY, $delay); + } + + return $transportMessage; + } + + public function createClientMessage(InteropMessage $transportMessage): Message + { + $clientMessage = new Message(); + + $clientMessage->setBody($transportMessage->getBody()); + $clientMessage->setHeaders($transportMessage->getHeaders()); + $clientMessage->setProperties($transportMessage->getProperties()); + $clientMessage->setMessageId($transportMessage->getMessageId()); + $clientMessage->setTimestamp($transportMessage->getTimestamp()); + $clientMessage->setReplyTo($transportMessage->getReplyTo()); + $clientMessage->setCorrelationId($transportMessage->getCorrelationId()); + + if ($contentType = $transportMessage->getProperty(Config::CONTENT_TYPE)) { + $clientMessage->setContentType($contentType); + } + + if ($priority = $transportMessage->getProperty(Config::PRIORITY)) { + $clientMessage->setPriority($priority); + } + + if ($delay = $transportMessage->getProperty(Config::DELAY)) { + $clientMessage->setDelay((int) $delay); + } + + if ($expire = $transportMessage->getProperty(Config::EXPIRE)) { + $clientMessage->setExpire((int) $expire); + } + + return $clientMessage; + } + + public function getConfig(): Config + { + return $this->config; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getRouteCollection(): RouteCollection + { + return $this->routeCollection; + } + + protected function doSendToRouter(InteropProducer $producer, Destination $topic, InteropMessage $transportMessage): void + { + $producer->send($topic, $transportMessage); + } + + protected function doSendToProcessor(InteropProducer $producer, InteropQueue $queue, InteropMessage $transportMessage): void + { + $producer->send($queue, $transportMessage); + } + + protected function createRouterTopic(): Destination + { + return $this->createQueue($this->getConfig()->getRouterQueue()); + } + + protected function createTransportRouterTopicName(string $name, bool $prefix): string + { + $clientPrefix = $prefix ? $this->config->getPrefix() : ''; + + return strtolower(implode($this->config->getSeparator(), array_filter([$clientPrefix, $name]))); + } + + protected function createTransportQueueName(string $name, bool $prefix): string + { + $clientPrefix = $prefix ? $this->config->getPrefix() : ''; + $clientAppName = $prefix ? $this->config->getApp() : ''; + + return strtolower(implode($this->config->getSeparator(), array_filter([$clientPrefix, $clientAppName, $name]))); + } + + protected function doCreateQueue(string $transportQueueName): InteropQueue + { + return $this->context->createQueue($transportQueueName); + } + + protected function doCreateTopic(string $transportTopicName): InteropTopic + { + return $this->context->createTopic($transportTopicName); + } + + /** + * [client message priority => transport message priority]. + * + * @return int[] + */ + protected function getPriorityMap(): array + { + return [ + MessagePriority::VERY_LOW => 0, + MessagePriority::LOW => 1, + MessagePriority::NORMAL => 2, + MessagePriority::HIGH => 3, + MessagePriority::VERY_HIGH => 4, + ]; + } +} diff --git a/Client/Driver/GpsDriver.php b/Client/Driver/GpsDriver.php new file mode 100644 index 0000000..32d14f7 --- /dev/null +++ b/Client/Driver/GpsDriver.php @@ -0,0 +1,64 @@ +debug(sprintf('[GpsDriver] '.$text, ...$args)); + }; + + // setup router + $routerTopic = $this->createRouterTopic(); + $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue()); + + $log('Subscribe router topic to queue: %s -> %s', $routerTopic->getTopicName(), $routerQueue->getQueueName()); + $this->getContext()->subscribe($routerTopic, $routerQueue); + + // setup queues + $declaredQueues = []; + foreach ($this->getRouteCollection()->all() as $route) { + /** @var GpsQueue $queue */ + $queue = $this->createRouteQueue($route); + if (array_key_exists($queue->getQueueName(), $declaredQueues)) { + continue; + } + + $topic = $this->getContext()->createTopic($queue->getQueueName()); + + $log('Subscribe processor topic to queue: %s -> %s', $topic->getTopicName(), $queue->getQueueName()); + $this->getContext()->subscribe($topic, $queue); + + $declaredQueues[$queue->getQueueName()] = true; + } + } + + /** + * @return GpsTopic + */ + protected function createRouterTopic(): Destination + { + return $this->doCreateTopic( + $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true) + ); + } +} diff --git a/Client/Driver/MongodbDriver.php b/Client/Driver/MongodbDriver.php new file mode 100644 index 0000000..1c9cff4 --- /dev/null +++ b/Client/Driver/MongodbDriver.php @@ -0,0 +1,30 @@ +debug(sprintf('[MongodbDriver] '.$text, ...$args)); + }; + + $contextConfig = $this->getContext()->getConfig(); + $log('Creating database and collection: "%s" "%s"', $contextConfig['dbname'], $contextConfig['collection_name']); + $this->getContext()->createCollection(); + } +} diff --git a/Client/Driver/RabbitMqDriver.php b/Client/Driver/RabbitMqDriver.php new file mode 100644 index 0000000..f215d55 --- /dev/null +++ b/Client/Driver/RabbitMqDriver.php @@ -0,0 +1,20 @@ +setArguments(['x-max-priority' => 4]); + + return $queue; + } +} diff --git a/Client/Driver/RabbitMqStompDriver.php b/Client/Driver/RabbitMqStompDriver.php new file mode 100644 index 0000000..7af2db8 --- /dev/null +++ b/Client/Driver/RabbitMqStompDriver.php @@ -0,0 +1,191 @@ +management = $management; + } + + /** + * @return StompMessage + */ + public function createTransportMessage(Message $message): InteropMessage + { + $transportMessage = parent::createTransportMessage($message); + + if ($message->getExpire()) { + $transportMessage->setHeader('expiration', (string) ($message->getExpire() * 1000)); + } + + if ($priority = $message->getPriority()) { + $priorityMap = $this->getPriorityMap(); + + if (false == array_key_exists($priority, $priorityMap)) { + throw new \LogicException(sprintf('Cant convert client priority to transport: "%s"', $priority)); + } + + $transportMessage->setHeader('priority', $priorityMap[$priority]); + } + + if ($message->getDelay()) { + if (false == $this->getConfig()->getTransportOption('delay_plugin_installed', false)) { + throw new \LogicException('The message delaying is not supported. In order to use delay feature install RabbitMQ delay plugin.'); + } + + $transportMessage->setHeader('x-delay', (string) ($message->getDelay() * 1000)); + } + + return $transportMessage; + } + + public function setupBroker(?LoggerInterface $logger = null): void + { + $logger = $logger ?: new NullLogger(); + $log = function ($text, ...$args) use ($logger) { + $logger->debug(sprintf('[RabbitMqStompDriver] '.$text, ...$args)); + }; + + if (false == $this->getConfig()->getTransportOption('management_plugin_installed', false)) { + $log('Could not setup broker. The option `management_plugin_installed` is not enabled. Please enable that option and install rabbit management plugin'); + + return; + } + + // setup router + $routerExchange = $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true); + $log('Declare router exchange: %s', $routerExchange); + $this->management->declareExchange($routerExchange, [ + 'type' => 'fanout', + 'durable' => true, + 'auto_delete' => false, + ]); + + $routerQueue = $this->createTransportQueueName($this->getConfig()->getRouterQueue(), true); + $log('Declare router queue: %s', $routerQueue); + $this->management->declareQueue($routerQueue, [ + 'auto_delete' => false, + 'durable' => true, + 'arguments' => [ + 'x-max-priority' => 4, + ], + ]); + + $log('Bind router queue to exchange: %s -> %s', $routerQueue, $routerExchange); + $this->management->bind($routerExchange, $routerQueue, $routerQueue); + + // setup queues + foreach ($this->getRouteCollection()->all() as $route) { + $queue = $this->createRouteQueue($route); + + $log('Declare processor queue: %s', $queue->getStompName()); + $this->management->declareQueue($queue->getStompName(), [ + 'auto_delete' => false, + 'durable' => true, + 'arguments' => [ + 'x-max-priority' => 4, + ], + ]); + } + + // setup delay exchanges + if ($this->getConfig()->getTransportOption('delay_plugin_installed', false)) { + foreach ($this->getRouteCollection()->all() as $route) { + $queue = $this->createRouteQueue($route); + $delayExchange = $queue->getStompName().'.delayed'; + + $log('Declare delay exchange: %s', $delayExchange); + $this->management->declareExchange($delayExchange, [ + 'type' => 'x-delayed-message', + 'durable' => true, + 'auto_delete' => false, + 'arguments' => [ + 'x-delayed-type' => 'direct', + ], + ]); + + $log('Bind processor queue to delay exchange: %s -> %s', $queue->getStompName(), $delayExchange); + $this->management->bind($delayExchange, $queue->getStompName(), $queue->getStompName()); + } + } else { + $log('Delay exchange and bindings are not setup. if you\'d like to use delays please install delay rabbitmq plugin and set delay_plugin_installed option to true'); + } + } + + /** + * @return StompDestination + */ + protected function doCreateQueue(string $transportQueueName): InteropQueue + { + $queue = parent::doCreateQueue($transportQueueName); + $queue->setHeader('x-max-priority', 4); + + return $queue; + } + + /** + * @param StompProducer $producer + * @param StompDestination $topic + * @param StompMessage $transportMessage + */ + protected function doSendToRouter(InteropProducer $producer, Destination $topic, InteropMessage $transportMessage): void + { + // We should not handle priority, expiration, and delay at this stage. + // The router will take care of it while re-sending the message to the final destinations. + $transportMessage->setHeader('expiration', null); + $transportMessage->setHeader('priority', null); + $transportMessage->setHeader('x-delay', null); + + $producer->send($topic, $transportMessage); + } + + /** + * @param StompProducer $producer + * @param StompDestination $destination + * @param StompMessage $transportMessage + */ + protected function doSendToProcessor(InteropProducer $producer, InteropQueue $destination, InteropMessage $transportMessage): void + { + if ($delay = $transportMessage->getProperty(Config::DELAY)) { + $producer->setDeliveryDelay(null); + $destination = $this->createDelayedTopic($destination); + } + + $producer->send($destination, $transportMessage); + } + + private function createDelayedTopic(StompDestination $queue): StompDestination + { + // in order to use delay feature make sure the rabbitmq_delayed_message_exchange plugin is installed. + $destination = $this->getContext()->createTopic($queue->getStompName().'.delayed'); + $destination->setType(StompDestination::TYPE_EXCHANGE); + $destination->setDurable(true); + $destination->setAutoDelete(false); + $destination->setRoutingKey($queue->getStompName()); + + return $destination; + } +} diff --git a/Client/Driver/RdKafkaDriver.php b/Client/Driver/RdKafkaDriver.php new file mode 100644 index 0000000..2609e3f --- /dev/null +++ b/Client/Driver/RdKafkaDriver.php @@ -0,0 +1,48 @@ +debug('[RdKafkaDriver] setup broker'); + $log = function ($text, ...$args) use ($logger) { + $logger->debug(sprintf('[RdKafkaDriver] '.$text, ...$args)); + }; + + // setup router + $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue()); + $log('Create router queue: %s', $routerQueue->getQueueName()); + $this->getContext()->createConsumer($routerQueue); + + // setup queues + $declaredQueues = []; + foreach ($this->getRouteCollection()->all() as $route) { + /** @var RdKafkaTopic $queue */ + $queue = $this->createRouteQueue($route); + if (array_key_exists($queue->getQueueName(), $declaredQueues)) { + continue; + } + + $log('Create processor queue: %s', $queue->getQueueName()); + $this->getContext()->createConsumer($queue); + } + } +} diff --git a/Client/Driver/RedisDriver.php b/Client/Driver/RedisDriver.php new file mode 100644 index 0000000..493cb7c --- /dev/null +++ b/Client/Driver/RedisDriver.php @@ -0,0 +1,20 @@ +debug(sprintf('[SqsQsDriver] '.$text, ...$args)); + }; + + // setup router + $routerTopic = $this->createRouterTopic(); + $log('Declare router topic: %s', $routerTopic->getTopicName()); + $this->getContext()->declareTopic($routerTopic); + + $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue()); + $log('Declare router queue: %s', $routerQueue->getQueueName()); + $this->getContext()->declareQueue($routerQueue); + + $log('Bind router queue to topic: %s -> %s', $routerQueue->getQueueName(), $routerTopic->getTopicName()); + $this->getContext()->bind($routerTopic, $routerQueue); + + // setup queues + $declaredQueues = []; + $declaredTopics = []; + foreach ($this->getRouteCollection()->all() as $route) { + $queue = $this->createRouteQueue($route); + if (false === array_key_exists($queue->getQueueName(), $declaredQueues)) { + $log('Declare processor queue: %s', $queue->getQueueName()); + $this->getContext()->declareQueue($queue); + + $declaredQueues[$queue->getQueueName()] = true; + } + + if ($route->isCommand()) { + continue; + } + + $topic = $this->doCreateTopic($this->createTransportQueueName($route->getSource(), true)); + if (false === array_key_exists($topic->getTopicName(), $declaredTopics)) { + $log('Declare processor topic: %s', $topic->getTopicName()); + $this->getContext()->declareTopic($topic); + + $declaredTopics[$topic->getTopicName()] = true; + } + + $log('Bind processor queue to topic: %s -> %s', $queue->getQueueName(), $topic->getTopicName()); + $this->getContext()->bind($topic, $queue); + } + } + + protected function createRouterTopic(): Destination + { + return $this->doCreateTopic( + $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true) + ); + } + + protected function createTransportRouterTopicName(string $name, bool $prefix): string + { + $name = parent::createTransportRouterTopicName($name, $prefix); + + return str_replace('.', '_dot_', $name); + } + + protected function createTransportQueueName(string $name, bool $prefix): string + { + $name = parent::createTransportQueueName($name, $prefix); + + return str_replace('.', '_dot_', $name); + } +} diff --git a/Client/Driver/SqsDriver.php b/Client/Driver/SqsDriver.php new file mode 100644 index 0000000..49b696a --- /dev/null +++ b/Client/Driver/SqsDriver.php @@ -0,0 +1,62 @@ +debug(sprintf('[SqsDriver] '.$text, ...$args)); + }; + + // setup router + $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue()); + $log('Declare router queue: %s', $routerQueue->getQueueName()); + $this->getContext()->declareQueue($routerQueue); + + // setup queues + $declaredQueues = []; + foreach ($this->getRouteCollection()->all() as $route) { + /** @var SqsDestination $queue */ + $queue = $this->createRouteQueue($route); + if (array_key_exists($queue->getQueueName(), $declaredQueues)) { + continue; + } + + $log('Declare processor queue: %s', $queue->getQueueName()); + $this->getContext()->declareQueue($queue); + + $declaredQueues[$queue->getQueueName()] = true; + } + } + + protected function createTransportRouterTopicName(string $name, bool $prefix): string + { + $name = parent::createTransportRouterTopicName($name, $prefix); + + return str_replace('.', '_dot_', $name); + } + + protected function createTransportQueueName(string $name, bool $prefix): string + { + $name = parent::createTransportQueueName($name, $prefix); + + return str_replace('.', '_dot_', $name); + } +} diff --git a/Client/Driver/StompDriver.php b/Client/Driver/StompDriver.php new file mode 100644 index 0000000..811ad76 --- /dev/null +++ b/Client/Driver/StompDriver.php @@ -0,0 +1,71 @@ +debug('[StompDriver] Stomp protocol does not support broker configuration'); + } + + /** + * @return StompMessage + */ + public function createTransportMessage(Message $message): InteropMessage + { + /** @var StompMessage $transportMessage */ + $transportMessage = parent::createTransportMessage($message); + $transportMessage->setPersistent(true); + + return $transportMessage; + } + + /** + * @return StompDestination + */ + protected function doCreateQueue(string $transportQueueName): InteropQueue + { + /** @var StompDestination $queue */ + $queue = parent::doCreateQueue($transportQueueName); + $queue->setDurable(true); + $queue->setAutoDelete(false); + $queue->setExclusive(false); + + return $queue; + } + + /** + * @return StompDestination + */ + protected function createRouterTopic(): Destination + { + /** @var StompDestination $topic */ + $topic = $this->doCreateTopic( + $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true) + ); + $topic->setDurable(true); + $topic->setAutoDelete(false); + + return $topic; + } +} diff --git a/Client/Driver/StompManagementClient.php b/Client/Driver/StompManagementClient.php new file mode 100644 index 0000000..0d64450 --- /dev/null +++ b/Client/Driver/StompManagementClient.php @@ -0,0 +1,44 @@ +client = $client; + $this->vhost = $vhost; + } + + public static function create(string $vhost = '/', string $host = 'localhost', int $port = 15672, string $login = 'guest', string $password = 'guest'): self + { + return new self(new Client(null, 'http://'.$host.':'.$port, $login, $password), $vhost); + } + + public function declareQueue(string $name, array $options) + { + return $this->client->queues()->create($this->vhost, $name, $options); + } + + public function declareExchange(string $name, array $options) + { + return $this->client->exchanges()->create($this->vhost, $name, $options); + } + + public function bind(string $exchange, string $queue, ?string $routingKey = null, $arguments = null) + { + return $this->client->bindings()->create($this->vhost, $exchange, $queue, $routingKey, $arguments); + } +} diff --git a/Client/DriverFactory.php b/Client/DriverFactory.php new file mode 100644 index 0000000..5c827e7 --- /dev/null +++ b/Client/DriverFactory.php @@ -0,0 +1,91 @@ +getTransportOption('dsn'); + + if (empty($dsn)) { + throw new \LogicException('This driver factory relies on dsn option from transport config. The option is empty or not set.'); + } + + $dsn = Dsn::parseFirst($dsn); + + if ($driverInfo = $this->findDriverInfo($dsn, Resources::getAvailableDrivers())) { + $driverClass = $driverInfo['driverClass']; + + if (RabbitMqStompDriver::class === $driverClass) { + return $this->createRabbitMqStompDriver($factory, $dsn, $config, $collection); + } + + return new $driverClass($factory->createContext(), $config, $collection); + } + + $knownDrivers = Resources::getKnownDrivers(); + if ($driverInfo = $this->findDriverInfo($dsn, $knownDrivers)) { + throw new \LogicException(sprintf('To use given scheme "%s" a package has to be installed. Run "composer req %s" to add it.', $dsn->getScheme(), implode(' ', $driverInfo['packages']))); + } + + throw new \LogicException(sprintf('A given scheme "%s" is not supported. Maybe it is a custom driver, make sure you registered it with "%s::addDriver".', $dsn->getScheme(), Resources::class)); + } + + private function findDriverInfo(Dsn $dsn, array $factories): ?array + { + $protocol = $dsn->getSchemeProtocol(); + + if ($dsn->getSchemeExtensions()) { + foreach ($factories as $info) { + if (empty($info['requiredSchemeExtensions'])) { + continue; + } + + if (false == in_array($protocol, $info['schemes'], true)) { + continue; + } + + $diff = array_diff($dsn->getSchemeExtensions(), $info['requiredSchemeExtensions']); + if (empty($diff)) { + return $info; + } + } + } + + foreach ($factories as $driverClass => $info) { + if (false == empty($info['requiredSchemeExtensions'])) { + continue; + } + + if (false == in_array($protocol, $info['schemes'], true)) { + continue; + } + + return $info; + } + + return null; + } + + private function createRabbitMqStompDriver(ConnectionFactory $factory, Dsn $dsn, Config $config, RouteCollection $collection): RabbitMqStompDriver + { + $defaultManagementHost = $dsn->getHost() ?: $config->getTransportOption('host', 'localhost'); + $managementVast = ltrim($dsn->getPath() ?? '', '/') ?: $config->getTransportOption('vhost', '/'); + + $managementClient = StompManagementClient::create( + urldecode($managementVast), + $config->getDriverOption('rabbitmq_management_host', $defaultManagementHost), + $config->getDriverOption('rabbitmq_management_port', 15672), + (string) $dsn->getUser() ?: $config->getTransportOption('user', 'guest'), + (string) $dsn->getPassword() ?: $config->getTransportOption('pass', 'guest') + ); + + return new RabbitMqStompDriver($factory->createContext(), $config, $collection, $managementClient); + } +} diff --git a/Client/DriverFactoryInterface.php b/Client/DriverFactoryInterface.php new file mode 100644 index 0000000..698ad05 --- /dev/null +++ b/Client/DriverFactoryInterface.php @@ -0,0 +1,10 @@ +message = $message; + $this->producer = $producer; + $this->driver = $driver; + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getProducer(): ProducerInterface + { + return $this->producer; + } + + public function getDriver(): DriverInterface + { + return $this->driver; + } + + public function isEvent(): bool + { + return (bool) $this->message->getProperty(Config::TOPIC); + } + + public function isCommand(): bool + { + return (bool) $this->message->getProperty(Config::COMMAND); + } + + public function getCommand(): string + { + return $this->message->getProperty(Config::COMMAND); + } + + public function getTopic(): string + { + return $this->message->getProperty(Config::TOPIC); + } +} diff --git a/Client/DriverPreSendExtensionInterface.php b/Client/DriverPreSendExtensionInterface.php new file mode 100644 index 0000000..fd95c93 --- /dev/null +++ b/Client/DriverPreSendExtensionInterface.php @@ -0,0 +1,8 @@ +transportDestination = $transportDestination; + $this->transportMessage = $transportMessage; + } + + public function getTransportDestination(): Destination + { + return $this->transportDestination; + } + + public function getTransportMessage(): TransportMessage + { + return $this->transportMessage; + } +} diff --git a/Client/Extension/PrepareBodyExtension.php b/Client/Extension/PrepareBodyExtension.php new file mode 100644 index 0000000..e792454 --- /dev/null +++ b/Client/Extension/PrepareBodyExtension.php @@ -0,0 +1,51 @@ +prepareBody($context->getMessage()); + } + + public function onPreSendCommand(PreSend $context): void + { + $this->prepareBody($context->getMessage()); + } + + private function prepareBody(Message $message): void + { + $body = $message->getBody(); + $contentType = $message->getContentType(); + + if (is_scalar($body) || null === $body) { + $contentType = $contentType ?: 'text/plain'; + $body = (string) $body; + } elseif (is_array($body)) { + // only array of scalars is allowed. + array_walk_recursive($body, function ($value) { + if (!is_scalar($value) && null !== $value) { + throw new \LogicException(sprintf('The message\'s body must be an array of scalars. Found not scalar in the array: %s', is_object($value) ? $value::class : gettype($value))); + } + }); + + $contentType = $contentType ?: 'application/json'; + $body = JSON::encode($body); + } elseif ($body instanceof \JsonSerializable) { + $contentType = $contentType ?: 'application/json'; + $body = JSON::encode($body); + } else { + throw new \InvalidArgumentException(sprintf('The message\'s body must be either null, scalar, array or object (implements \JsonSerializable). Got: %s', is_object($body) ? $body::class : gettype($body))); + } + + $message->setContentType($contentType); + $message->setBody($body); + } +} diff --git a/Client/ExtensionInterface.php b/Client/ExtensionInterface.php index 4f9fd66..596b1b9 100644 --- a/Client/ExtensionInterface.php +++ b/Client/ExtensionInterface.php @@ -2,21 +2,6 @@ namespace Enqueue\Client; -interface ExtensionInterface +interface ExtensionInterface extends PreSendEventExtensionInterface, PreSendCommandExtensionInterface, DriverPreSendExtensionInterface, PostSendExtensionInterface { - /** - * @param string $topic - * @param Message $message - * - * @return - */ - public function onPreSend($topic, Message $message); - - /** - * @param string $topic - * @param Message $message - * - * @return - */ - public function onPostSend($topic, Message $message); } diff --git a/Client/Message.php b/Client/Message.php index d58371b..7e51ea1 100644 --- a/Client/Message.php +++ b/Client/Message.php @@ -7,12 +7,12 @@ class Message /** * @const string */ - const SCOPE_MESSAGE_BUS = 'enqueue.scope.message_bus'; + public const SCOPE_MESSAGE_BUS = 'enqueue.scope.message_bus'; /** * @const string */ - const SCOPE_APP = 'enqueue.scope.app'; + public const SCOPE_APP = 'enqueue.scope.app'; /** * @var string|null @@ -88,7 +88,7 @@ public function __construct($body = '', array $properties = [], array $headers = } /** - * @return null|string + * @return string|null */ public function getBody() { @@ -96,7 +96,7 @@ public function getBody() } /** - * @param null|string|int|float|array|\JsonSerializable $body + * @param string|int|float|array|\JsonSerializable|null $body */ public function setBody($body) { @@ -205,18 +205,12 @@ public function setDelay($delay) $this->delay = $delay; } - /** - * @param string $scope - */ - public function setScope($scope) + public function setScope(string $scope): void { $this->scope = $scope; } - /** - * @return string - */ - public function getScope() + public function getScope(): string { return $this->scope; } @@ -262,10 +256,8 @@ public function getHeaders() } /** - * @param string $name - * @param mixed $default - * - * @return mixed + * @param string $name + * @param mixed|null $default */ public function getHeader($name, $default = null) { @@ -274,16 +266,12 @@ public function getHeader($name, $default = null) /** * @param string $name - * @param mixed $value */ public function setHeader($name, $value) { $this->headers[$name] = $value; } - /** - * @param array $headers - */ public function setHeaders(array $headers) { $this->headers = $headers; @@ -297,19 +285,14 @@ public function getProperties() return $this->properties; } - /** - * @param array $properties - */ public function setProperties(array $properties) { $this->properties = $properties; } /** - * @param string $name - * @param mixed $default - * - * @return mixed + * @param string $name + * @param mixed|null $default */ public function getProperty($name, $default = null) { @@ -318,7 +301,6 @@ public function getProperty($name, $default = null) /** * @param string $name - * @param mixed $value */ public function setProperty($name, $value) { diff --git a/Client/MessagePriority.php b/Client/MessagePriority.php index efa658c..e14be9a 100644 --- a/Client/MessagePriority.php +++ b/Client/MessagePriority.php @@ -4,9 +4,9 @@ class MessagePriority { - const VERY_LOW = 'enqueue.message_queue.client.very_low_message_priority'; - const LOW = 'enqueue.message_queue.client.low_message_priority'; - const NORMAL = 'enqueue.message_queue.client.normal_message_priority'; - const HIGH = 'enqueue.message_queue.client.high_message_priority'; - const VERY_HIGH = 'enqueue.message_queue.client.very_high_message_priority'; + public const VERY_LOW = 'enqueue.message_queue.client.very_low_message_priority'; + public const LOW = 'enqueue.message_queue.client.low_message_priority'; + public const NORMAL = 'enqueue.message_queue.client.normal_message_priority'; + public const HIGH = 'enqueue.message_queue.client.high_message_priority'; + public const VERY_HIGH = 'enqueue.message_queue.client.very_high_message_priority'; } diff --git a/Client/Meta/QueueMeta.php b/Client/Meta/QueueMeta.php deleted file mode 100644 index bee32bd..0000000 --- a/Client/Meta/QueueMeta.php +++ /dev/null @@ -1,57 +0,0 @@ -clientName = $clientName; - $this->transportName = $transportName; - $this->processors = $processors; - } - - /** - * @return string - */ - public function getClientName() - { - return $this->clientName; - } - - /** - * @return string - */ - public function getTransportName() - { - return $this->transportName; - } - - /** - * @return string[] - */ - public function getProcessors() - { - return $this->processors; - } -} diff --git a/Client/Meta/QueueMetaRegistry.php b/Client/Meta/QueueMetaRegistry.php deleted file mode 100644 index 29c2d69..0000000 --- a/Client/Meta/QueueMetaRegistry.php +++ /dev/null @@ -1,95 +0,0 @@ - [ - * 'transportName' => 'aTransportQueueName', - * 'processors' => ['aFooProcessorName', 'aBarProcessorName'], - * ] - * ]. - * - * - * @param Config $config - * @param array $meta - */ - public function __construct(Config $config, array $meta) - { - $this->config = $config; - $this->meta = $meta; - } - - /** - * @param string $queueName - * @param string|null $transportName - */ - public function add($queueName, $transportName = null) - { - $this->meta[$queueName] = [ - 'transportName' => $transportName, - 'processors' => [], - ]; - } - - /** - * @param string $queueName - * @param string $processorName - */ - public function addProcessor($queueName, $processorName) - { - if (false == array_key_exists($queueName, $this->meta)) { - $this->add($queueName); - } - - $this->meta[$queueName]['processors'][] = $processorName; - } - - /** - * @param string $queueName - * - * @return QueueMeta - */ - public function getQueueMeta($queueName) - { - if (false == array_key_exists($queueName, $this->meta)) { - throw new \InvalidArgumentException(sprintf( - 'The queue meta not found. Requested name `%s`', - $queueName - )); - } - - $transportName = $this->config->createTransportQueueName($queueName); - - $meta = array_replace([ - 'processors' => [], - 'transportName' => $transportName, - ], array_filter($this->meta[$queueName])); - - return new QueueMeta($queueName, $meta['transportName'], $meta['processors']); - } - - /** - * @return \Generator|QueueMeta[] - */ - public function getQueuesMeta() - { - foreach (array_keys($this->meta) as $queueName) { - yield $this->getQueueMeta($queueName); - } - } -} diff --git a/Client/Meta/TopicMeta.php b/Client/Meta/TopicMeta.php deleted file mode 100644 index abb0e33..0000000 --- a/Client/Meta/TopicMeta.php +++ /dev/null @@ -1,57 +0,0 @@ -name = $name; - $this->description = $description; - $this->processors = $processors; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * @return string - */ - public function getDescription() - { - return $this->description; - } - - /** - * @return string[] - */ - public function getProcessors() - { - return $this->processors; - } -} diff --git a/Client/Meta/TopicMetaRegistry.php b/Client/Meta/TopicMetaRegistry.php deleted file mode 100644 index efceb9a..0000000 --- a/Client/Meta/TopicMetaRegistry.php +++ /dev/null @@ -1,80 +0,0 @@ - [ - * 'description' => 'A desc', - * 'processors' => ['aProcessorNameFoo', 'aProcessorNameBar], - * ], - * ]. - * - * @param array $meta - */ - public function __construct(array $meta) - { - $this->meta = $meta; - } - - /** - * @param string $topicName - * @param string $description - */ - public function add($topicName, $description = null) - { - $this->meta[$topicName] = [ - 'description' => $description, - 'processors' => [], - ]; - } - - /** - * @param string $topicName - * @param string $processorName - */ - public function addProcessor($topicName, $processorName) - { - if (false == array_key_exists($topicName, $this->meta)) { - $this->add($topicName); - } - - $this->meta[$topicName]['processors'][] = $processorName; - } - - /** - * @param string $topicName - * - * @return TopicMeta - */ - public function getTopicMeta($topicName) - { - if (false == array_key_exists($topicName, $this->meta)) { - throw new \InvalidArgumentException(sprintf('The topic meta not found. Requested name `%s`', $topicName)); - } - - $topic = array_replace([ - 'description' => '', - 'processors' => [], - ], $this->meta[$topicName]); - - return new TopicMeta($topicName, $topic['description'], $topic['processors']); - } - - /** - * @return \Generator|TopicMeta[] - */ - public function getTopicsMeta() - { - foreach (array_keys($this->meta) as $topicName) { - yield $this->getTopicMeta($topicName); - } - } -} diff --git a/Client/PostSend.php b/Client/PostSend.php new file mode 100644 index 0000000..5d9526e --- /dev/null +++ b/Client/PostSend.php @@ -0,0 +1,78 @@ +message = $message; + $this->producer = $producer; + $this->driver = $driver; + $this->transportDestination = $transportDestination; + $this->transportMessage = $transportMessage; + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getProducer(): ProducerInterface + { + return $this->producer; + } + + public function getDriver(): DriverInterface + { + return $this->driver; + } + + public function getTransportDestination(): Destination + { + return $this->transportDestination; + } + + public function getTransportMessage(): TransportMessage + { + return $this->transportMessage; + } + + public function isEvent(): bool + { + return (bool) $this->message->getProperty(Config::TOPIC); + } + + public function isCommand(): bool + { + return (bool) $this->message->getProperty(Config::COMMAND); + } + + public function getCommand(): string + { + return $this->message->getProperty(Config::COMMAND); + } + + public function getTopic(): string + { + return $this->message->getProperty(Config::TOPIC); + } +} diff --git a/Client/PostSendExtensionInterface.php b/Client/PostSendExtensionInterface.php new file mode 100644 index 0000000..dd3ca8b --- /dev/null +++ b/Client/PostSendExtensionInterface.php @@ -0,0 +1,8 @@ +message = $message; + $this->commandOrTopic = $commandOrTopic; + $this->producer = $producer; + $this->driver = $driver; + + $this->originalMessage = clone $message; + } + + public function getCommand(): string + { + return $this->commandOrTopic; + } + + public function getTopic(): string + { + return $this->commandOrTopic; + } + + public function changeCommand(string $newCommand): void + { + $this->commandOrTopic = $newCommand; + } + + public function changeTopic(string $newTopic): void + { + $this->commandOrTopic = $newTopic; + } + + public function changeBody($body, ?string $contentType = null): void + { + $this->message->setBody($body); + + if (null !== $contentType) { + $this->message->setContentType($contentType); + } + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getOriginalMessage(): Message + { + return $this->originalMessage; + } + + public function getProducer(): ProducerInterface + { + return $this->producer; + } + + public function getDriver(): DriverInterface + { + return $this->driver; + } +} diff --git a/Client/PreSendCommandExtensionInterface.php b/Client/PreSendCommandExtensionInterface.php new file mode 100644 index 0000000..cefec09 --- /dev/null +++ b/Client/PreSendCommandExtensionInterface.php @@ -0,0 +1,11 @@ +driver = $driver; $this->rpcFactory = $rpcFactory; - $this->extension = $extension ?: new ChainExtension([]); + + $this->extension = $extension ? + new ChainExtension([$extension, new PrepareBodyExtension()]) : + new ChainExtension([new PrepareBodyExtension()]) + ; } - /** - * {@inheritdoc} - */ - public function sendEvent($topic, $message) + public function sendEvent(string $topic, $message): void { if (false == $message instanceof Message) { - $body = $message; - $message = new Message(); - $message->setBody($body); - } - - $this->prepareBody($message); - - $message->setProperty(Config::PARAMETER_TOPIC_NAME, $topic); - - if (!$message->getMessageId()) { - $message->setMessageId(UUID::generate()); - } - - if (!$message->getTimestamp()) { - $message->setTimestamp(time()); + $message = new Message($message); } - if (!$message->getPriority()) { - $message->setPriority(MessagePriority::NORMAL); - } + $preSend = new PreSend($topic, $message, $this, $this->driver); + $this->extension->onPreSendEvent($preSend); - if (Message::SCOPE_MESSAGE_BUS == $message->getScope()) { - if ($message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException(sprintf('The %s property must not be set for messages that are sent to message bus.', Config::PARAMETER_PROCESSOR_QUEUE_NAME)); - } - if ($message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException(sprintf('The %s property must not be set for messages that are sent to message bus.', Config::PARAMETER_PROCESSOR_NAME)); - } + $message = $preSend->getMessage(); + $message->setProperty(Config::TOPIC, $preSend->getTopic()); - $this->extension->onPreSend($topic, $message); - $this->driver->sendToRouter($message); - $this->extension->onPostSend($topic, $message); - } elseif (Message::SCOPE_APP == $message->getScope()) { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, $this->driver->getConfig()->getRouterProcessorName()); - } - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $this->driver->getConfig()->getRouterQueueName()); - } - - $this->extension->onPreSend($topic, $message); - $this->driver->sendToProcessor($message); - $this->extension->onPostSend($topic, $message); - } else { - throw new \LogicException(sprintf('The message scope "%s" is not supported.', $message->getScope())); - } + $this->doSend($message); } - /** - * {@inheritdoc} - */ - public function sendCommand($command, $message, $needReply = false) + public function sendCommand(string $command, $message, bool $needReply = false): ?Promise { if (false == $message instanceof Message) { $message = new Message($message); } + $preSend = new PreSend($command, $message, $this, $this->driver); + $this->extension->onPreSendCommand($preSend); + + $command = $preSend->getCommand(); + $message = $preSend->getMessage(); + $deleteReplyQueue = false; $replyTo = $message->getReplyTo(); @@ -117,11 +79,10 @@ public function sendCommand($command, $message, $needReply = false) } } - $message->setProperty(Config::PARAMETER_TOPIC_NAME, Config::COMMAND_TOPIC); - $message->setProperty(Config::PARAMETER_COMMAND_NAME, $command); + $message->setProperty(Config::COMMAND, $command); $message->setScope(Message::SCOPE_APP); - $this->sendEvent(Config::COMMAND_TOPIC, $message); + $this->doSend($message); if ($needReply) { $promise = $this->rpcFactory->createPromise($replyTo, $message->getCorrelationId(), 60000); @@ -129,59 +90,38 @@ public function sendCommand($command, $message, $needReply = false) return $promise; } - } - /** - * {@inheritdoc} - */ - public function send($topic, $message) - { - $this->sendEvent($topic, $message); + return null; } - /** - * @param Message $message - */ - private function prepareBody(Message $message) + private function doSend(Message $message): void { - $body = $message->getBody(); - $contentType = $message->getContentType(); - - if (is_scalar($body) || null === $body) { - $contentType = $contentType ?: 'text/plain'; - $body = (string) $body; - } elseif (is_array($body)) { - if ($contentType && 'application/json' !== $contentType) { - throw new \LogicException(sprintf('Content type "application/json" only allowed when body is array')); - } + if (false === is_string($message->getBody())) { + throw new \LogicException(sprintf('The message body must be string at this stage, got "%s". Make sure you passed string as message or there is an extension that converts custom input to string.', is_object($message->getBody()) ? get_class($message->getBody()) : gettype($message->getBody()))); + } - // only array of scalars is allowed. - array_walk_recursive($body, function ($value) { - if (!is_scalar($value) && null !== $value) { - throw new \LogicException(sprintf( - 'The message\'s body must be an array of scalars. Found not scalar in the array: %s', - is_object($value) ? get_class($value) : gettype($value) - )); - } - }); - - $contentType = 'application/json'; - $body = JSON::encode($body); - } elseif ($body instanceof \JsonSerializable) { - if ($contentType && 'application/json' !== $contentType) { - throw new \LogicException(sprintf('Content type "application/json" only allowed when body is array')); - } + if ($message->getProperty(Config::PROCESSOR)) { + throw new \LogicException(sprintf('The %s property must not be set.', Config::PROCESSOR)); + } - $contentType = 'application/json'; - $body = JSON::encode($body); + if (!$message->getMessageId()) { + $message->setMessageId(UUID::generate()); + } + + if (!$message->getTimestamp()) { + $message->setTimestamp(time()); + } + + $this->extension->onDriverPreSend(new DriverPreSend($message, $this, $this->driver)); + + if (Message::SCOPE_MESSAGE_BUS == $message->getScope()) { + $result = $this->driver->sendToRouter($message); + } elseif (Message::SCOPE_APP == $message->getScope()) { + $result = $this->driver->sendToProcessor($message); } else { - throw new \InvalidArgumentException(sprintf( - 'The message\'s body must be either null, scalar, array or object (implements \JsonSerializable). Got: %s', - is_object($body) ? get_class($body) : gettype($body) - )); + throw new \LogicException(sprintf('The message scope "%s" is not supported.', $message->getScope())); } - $message->setContentType($contentType); - $message->setBody($body); + $this->extension->onPostSend(new PostSend($message, $this, $this->driver, $result->getTransportDestination(), $result->getTransportMessage())); } } diff --git a/Client/ProducerInterface.php b/Client/ProducerInterface.php index 2fc829e..3c88480 100644 --- a/Client/ProducerInterface.php +++ b/Client/ProducerInterface.php @@ -7,17 +7,21 @@ interface ProducerInterface { /** - * @param string $topic + * The message could be pretty much everything as long as you have a client extension that transforms a body to string on onPreSendEvent. + * * @param string|array|Message $message + * + * @throws \Exception */ - public function sendEvent($topic, $message); + public function sendEvent(string $topic, $message): void; /** - * @param string $command + * The message could be pretty much everything as long as you have a client extension that transforms a body to string on onPreSendCommand. + * The promise is returned if needReply argument is true. + * * @param string|array|Message $message - * @param bool $needReply * - * @return Promise|null the promise is returned if needReply argument is true + * @throws \Exception */ - public function sendCommand($command, $message, $needReply = false); + public function sendCommand(string $command, $message, bool $needReply = false): ?Promise; } diff --git a/Client/Resources.php b/Client/Resources.php new file mode 100644 index 0000000..a5cc684 --- /dev/null +++ b/Client/Resources.php @@ -0,0 +1,194 @@ + [ + * schemes => [schemes strings], + * package => package name, + * ]. + * + * @var array + */ + private static $knownDrivers; + + private function __construct() + { + } + + public static function getAvailableDrivers(): array + { + $map = self::getKnownDrivers(); + + $availableMap = []; + foreach ($map as $item) { + if (class_exists($item['driverClass'])) { + $availableMap[] = $item; + } + } + + return $availableMap; + } + + public static function getKnownDrivers(): array + { + if (null === self::$knownDrivers) { + $map = []; + + $map[] = [ + 'schemes' => ['amqp', 'amqps'], + 'driverClass' => AmqpDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/amqp-bunny'], + ]; + $map[] = [ + 'schemes' => ['amqp', 'amqps'], + 'driverClass' => RabbitMqDriver::class, + 'requiredSchemeExtensions' => ['rabbitmq'], + 'packages' => ['enqueue/enqueue', 'enqueue/amqp-bunny'], + ]; + $map[] = [ + 'schemes' => ['file'], + 'driverClass' => FsDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/fs'], + ]; + $map[] = [ + 'schemes' => ['null'], + 'driverClass' => GenericDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/null'], + ]; + $map[] = [ + 'schemes' => ['gps'], + 'driverClass' => GpsDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/gps'], + ]; + $map[] = [ + 'schemes' => ['redis', 'rediss'], + 'driverClass' => RedisDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/redis'], + ]; + $map[] = [ + 'schemes' => ['sqs'], + 'driverClass' => SqsDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/sqs'], + ]; + $map[] = [ + 'schemes' => ['sns'], + 'driverClass' => GenericDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/sns'], + ]; + $map[] = [ + 'schemes' => ['snsqs'], + 'driverClass' => SnsQsDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/sqs', 'enqueue/sns', 'enqueue/snsqs'], + ]; + $map[] = [ + 'schemes' => ['stomp'], + 'driverClass' => StompDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/stomp'], + ]; + $map[] = [ + 'schemes' => ['stomp'], + 'driverClass' => RabbitMqStompDriver::class, + 'requiredSchemeExtensions' => ['rabbitmq'], + 'packages' => ['enqueue/enqueue', 'enqueue/stomp'], + ]; + $map[] = [ + 'schemes' => ['kafka', 'rdkafka'], + 'driverClass' => RdKafkaDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/rdkafka'], + ]; + $map[] = [ + 'schemes' => ['mongodb'], + 'driverClass' => MongodbDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/mongodb'], + ]; + $map[] = [ + 'schemes' => [ + 'db2', + 'ibm-db2', + 'mssql', + 'sqlsrv', + 'mysql', + 'mysql2', + 'mysql', + 'pgsql', + 'postgres', + 'pgsql', + 'sqlite', + 'sqlite3', + 'sqlite', + ], + 'driverClass' => DbalDriver::class, + 'requiredSchemeExtensions' => [], + 'package' => ['enqueue/enqueue', 'enqueue/dbal'], + ]; + $map[] = [ + 'schemes' => ['gearman'], + 'driverClass' => GenericDriver::class, + 'requiredSchemeExtensions' => [], + 'package' => ['enqueue/enqueue', 'enqueue/gearman'], + ]; + $map[] = [ + 'schemes' => ['beanstalk'], + 'driverClass' => GenericDriver::class, + 'requiredSchemeExtensions' => [], + 'package' => ['enqueue/enqueue', 'enqueue/pheanstalk'], + ]; + + self::$knownDrivers = $map; + } + + return self::$knownDrivers; + } + + public static function addDriver(string $driverClass, array $schemes, array $requiredExtensions, array $packages): void + { + if (class_exists($driverClass)) { + if (false == (new \ReflectionClass($driverClass))->implementsInterface(DriverInterface::class)) { + throw new \InvalidArgumentException(sprintf('The driver class "%s" must implement "%s" interface.', $driverClass, DriverInterface::class)); + } + } + + if (empty($schemes)) { + throw new \InvalidArgumentException('Schemes could not be empty.'); + } + if (empty($packages)) { + throw new \InvalidArgumentException('Packages could not be empty.'); + } + + self::getKnownDrivers(); + self::$knownDrivers[] = [ + 'schemes' => $schemes, + 'driverClass' => $driverClass, + 'requiredSchemeExtensions' => $requiredExtensions, + 'packages' => $packages, + ]; + } +} diff --git a/Client/Route.php b/Client/Route.php new file mode 100644 index 0000000..8b9e31e --- /dev/null +++ b/Client/Route.php @@ -0,0 +1,114 @@ +source = $source; + $this->sourceType = $sourceType; + $this->processor = $processor; + $this->options = $options; + } + + public function getSource(): string + { + return $this->source; + } + + public function isCommand(): bool + { + return self::COMMAND === $this->sourceType; + } + + public function isTopic(): bool + { + return self::TOPIC === $this->sourceType; + } + + public function getProcessor(): string + { + return $this->processor; + } + + public function isProcessorExclusive(): bool + { + return (bool) $this->getOption('exclusive', false); + } + + public function isProcessorExternal(): bool + { + return (bool) $this->getOption('external', false); + } + + public function getQueue(): ?string + { + return $this->getOption('queue'); + } + + public function isPrefixQueue(): bool + { + return (bool) $this->getOption('prefix_queue', true); + } + + public function getOptions(): array + { + return $this->options; + } + + public function getOption(string $name, $default = null) + { + return array_key_exists($name, $this->options) ? $this->options[$name] : $default; + } + + public function toArray(): array + { + return array_replace($this->options, [ + 'source' => $this->source, + 'source_type' => $this->sourceType, + 'processor' => $this->processor, + ]); + } + + public static function fromArray(array $route): self + { + list( + 'source' => $source, + 'source_type' => $sourceType, + 'processor' => $processor) = $route; + + unset($route['source'], $route['source_type'], $route['processor']); + $options = $route; + + return new self($source, $sourceType, $processor, $options); + } +} diff --git a/Client/RouteCollection.php b/Client/RouteCollection.php new file mode 100644 index 0000000..76bcbe4 --- /dev/null +++ b/Client/RouteCollection.php @@ -0,0 +1,114 @@ +routes = $routes; + } + + public function add(Route $route): void + { + $this->routes[] = $route; + $this->topicRoutes = null; + $this->commandRoutes = null; + } + + /** + * @return Route[] + */ + public function all(): array + { + return $this->routes; + } + + /** + * @return Route[] + */ + public function command(string $command): ?Route + { + if (null === $this->commandRoutes) { + $commandRoutes = []; + foreach ($this->routes as $route) { + if ($route->isCommand()) { + $commandRoutes[$route->getSource()] = $route; + } + } + + $this->commandRoutes = $commandRoutes; + } + + return array_key_exists($command, $this->commandRoutes) ? $this->commandRoutes[$command] : null; + } + + /** + * @return Route[] + */ + public function topic(string $topic): array + { + if (null === $this->topicRoutes) { + $topicRoutes = []; + foreach ($this->routes as $route) { + if ($route->isTopic()) { + $topicRoutes[$route->getSource()][$route->getProcessor()] = $route; + } + } + + $this->topicRoutes = $topicRoutes; + } + + return array_key_exists($topic, $this->topicRoutes) ? $this->topicRoutes[$topic] : []; + } + + public function topicAndProcessor(string $topic, string $processor): ?Route + { + $routes = $this->topic($topic); + foreach ($routes as $route) { + if ($route->getProcessor() === $processor) { + return $route; + } + } + + return null; + } + + public function toArray(): array + { + $rawRoutes = []; + foreach ($this->routes as $route) { + $rawRoutes[] = $route->toArray(); + } + + return $rawRoutes; + } + + public static function fromArray(array $rawRoutes): self + { + $routes = []; + foreach ($rawRoutes as $rawRoute) { + $routes[] = Route::fromArray($rawRoute); + } + + return new self($routes); + } +} diff --git a/Client/RouterProcessor.php b/Client/RouterProcessor.php index 35efe1b..c441ceb 100644 --- a/Client/RouterProcessor.php +++ b/Client/RouterProcessor.php @@ -3,119 +3,62 @@ namespace Enqueue\Client; use Enqueue\Consumption\Result; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message as InteropMessage; +use Interop\Queue\Processor; -class RouterProcessor implements PsrProcessor +final class RouterProcessor implements Processor { /** - * @var DriverInterface - */ - private $driver; - - /** - * @var array + * compatibility with 0.8x. */ - private $eventRoutes; + private const COMMAND_TOPIC_08X = '__command__'; /** - * @var array + * @var DriverInterface */ - private $commandRoutes; + private $driver; - /** - * @param DriverInterface $driver - * @param array $eventRoutes - * @param array $commandRoutes - */ - public function __construct(DriverInterface $driver, array $eventRoutes = [], array $commandRoutes = []) + public function __construct(DriverInterface $driver) { $this->driver = $driver; - - $this->eventRoutes = $eventRoutes; - $this->commandRoutes = $commandRoutes; } - /** - * @param string $topicName - * @param string $queueName - * @param string $processorName - */ - public function add($topicName, $queueName, $processorName) + public function process(InteropMessage $message, Context $context): Result { - if (Config::COMMAND_TOPIC === $topicName) { - $this->commandRoutes[$processorName] = $queueName; - } else { - $this->eventRoutes[$topicName][] = [$processorName, $queueName]; + // compatibility with 0.8x + if (self::COMMAND_TOPIC_08X === $message->getProperty(Config::TOPIC)) { + $clientMessage = $this->driver->createClientMessage($message); + $clientMessage->setProperty(Config::TOPIC, null); + + $this->driver->sendToProcessor($clientMessage); + + return Result::ack('Legacy 0.8x message routed to processor'); } - } + // compatibility with 0.8x - /** - * {@inheritdoc} - */ - public function process(PsrMessage $message, PsrContext $context) - { - $topicName = $message->getProperty(Config::PARAMETER_TOPIC_NAME); - if (false == $topicName) { + if ($message->getProperty(Config::COMMAND)) { return Result::reject(sprintf( - 'Got message without required parameter: "%s"', - Config::PARAMETER_TOPIC_NAME + 'Unexpected command "%s" got. Command must not go to the router.', + $message->getProperty(Config::COMMAND) )); } - if (Config::COMMAND_TOPIC === $topicName) { - return $this->routeCommand($message); + $topic = $message->getProperty(Config::TOPIC); + if (false == $topic) { + return Result::reject(sprintf('Topic property "%s" is required but not set or empty.', Config::TOPIC)); } - return $this->routeEvent($message); - } - - /** - * @param PsrMessage $message - * - * @return string|Result - */ - private function routeEvent(PsrMessage $message) - { - $topicName = $message->getProperty(Config::PARAMETER_TOPIC_NAME); - - if (array_key_exists($topicName, $this->eventRoutes)) { - foreach ($this->eventRoutes[$topicName] as $route) { - $processorMessage = clone $message; - $processorMessage->setProperty(Config::PARAMETER_PROCESSOR_NAME, $route[0]); - $processorMessage->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $route[1]); - - $this->driver->sendToProcessor($this->driver->createClientMessage($processorMessage)); - } - } - - return self::ACK; - } - - /** - * @param PsrMessage $message - * - * @return string|Result - */ - private function routeCommand(PsrMessage $message) - { - $commandName = $message->getProperty(Config::PARAMETER_COMMAND_NAME); - if (false == $commandName) { - return Result::reject(sprintf( - 'Got message without required parameter: "%s"', - Config::PARAMETER_COMMAND_NAME - )); - } + $count = 0; + foreach ($this->driver->getRouteCollection()->topic($topic) as $route) { + $clientMessage = $this->driver->createClientMessage($message); + $clientMessage->setProperty(Config::PROCESSOR, $route->getProcessor()); - if (isset($this->commandRoutes[$commandName])) { - $processorMessage = clone $message; - $processorMessage->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $this->commandRoutes[$commandName]); - $processorMessage->setProperty(Config::PARAMETER_PROCESSOR_NAME, $commandName); + $this->driver->sendToProcessor($clientMessage); - $this->driver->sendToProcessor($this->driver->createClientMessage($processorMessage)); + ++$count; } - return self::ACK; + return Result::ack(sprintf('Routed to "%d" event subscribers', $count)); } } diff --git a/Client/SpoolProducer.php b/Client/SpoolProducer.php index c538877..8ad0940 100644 --- a/Client/SpoolProducer.php +++ b/Client/SpoolProducer.php @@ -2,6 +2,8 @@ namespace Enqueue\Client; +use Enqueue\Rpc\Promise; + class SpoolProducer implements ProducerInterface { /** @@ -19,9 +21,6 @@ class SpoolProducer implements ProducerInterface */ private $commands; - /** - * @param ProducerInterface $realProducer - */ public function __construct(ProducerInterface $realProducer) { $this->realProducer = $realProducer; @@ -30,38 +29,26 @@ public function __construct(ProducerInterface $realProducer) $this->commands = new \SplQueue(); } - /** - * {@inheritdoc} - */ - public function sendCommand($command, $message, $needReply = false) + public function sendCommand(string $command, $message, bool $needReply = false): ?Promise { if ($needReply) { return $this->realProducer->sendCommand($command, $message, $needReply); } $this->commands->enqueue([$command, $message]); - } - /** - * {@inheritdoc} - */ - public function sendEvent($topic, $message) - { - $this->events->enqueue([$topic, $message]); + return null; } - /** - * {@inheritdoc} - */ - public function send($topic, $message) + public function sendEvent(string $topic, $message): void { - $this->sendEvent($topic, $message); + $this->events->enqueue([$topic, $message]); } /** * When it is called it sends all previously queued messages. */ - public function flush() + public function flush(): void { while (false == $this->events->isEmpty()) { list($topic, $message) = $this->events->dequeue(); diff --git a/Client/TopicSubscriberInterface.php b/Client/TopicSubscriberInterface.php index bdaa43f..849a782 100644 --- a/Client/TopicSubscriberInterface.php +++ b/Client/TopicSubscriberInterface.php @@ -7,21 +7,35 @@ interface TopicSubscriberInterface /** * The result maybe either:. * - * ['aTopicName'] + * 'aTopicName' * * or * - * ['aTopicName' => [ - * 'processorName' => 'processor', - * 'queueName' => 'a_client_queue_name', - * 'queueNameHardcoded' => true, - * ]] + * ['aTopicName', 'anotherTopicName'] * - * processorName, queueName and queueNameHardcoded are optional. + * or + * + * [ + * [ + * 'topic' => 'aTopicName', + * 'processor' => 'fooProcessor', + * 'queue' => 'a_client_queue_name', + * + * 'aCustomOption' => 'aVal', + * ], + * [ + * 'topic' => 'anotherTopicName', + * 'processor' => 'barProcessor', + * 'queue' => 'a_client_queue_name', + * + * 'aCustomOption' => 'aVal', + * ], + * ] * - * Note: If you set queueNameHardcoded to true then the queueName is used as is and therefor the driver is not used to create a transport queue name. + * Note: If you set prefix_queue to true then the queue is used as is and therefor the driver is not used to prepare a transport queue name. + * It is possible to pass other options, they could be accessible on a route instance through options. * - * @return array + * @return string|array */ public static function getSubscribedTopics(); } diff --git a/Client/TraceableProducer.php b/Client/TraceableProducer.php index cd55a91..b0bd613 100644 --- a/Client/TraceableProducer.php +++ b/Client/TraceableProducer.php @@ -2,61 +2,42 @@ namespace Enqueue\Client; -class TraceableProducer implements ProducerInterface +use Enqueue\Rpc\Promise; + +final class TraceableProducer implements ProducerInterface { /** * @var array */ - protected $traces = []; + private $traces = []; + /** * @var ProducerInterface */ private $producer; - /** - * @param ProducerInterface $producer - */ public function __construct(ProducerInterface $producer) { $this->producer = $producer; } - /** - * {@inheritdoc} - */ - public function sendEvent($topic, $message) + public function sendEvent(string $topic, $message): void { $this->producer->sendEvent($topic, $message); $this->collectTrace($topic, null, $message); } - /** - * {@inheritdoc} - */ - public function sendCommand($command, $message, $needReply = false) + public function sendCommand(string $command, $message, bool $needReply = false): ?Promise { $result = $this->producer->sendCommand($command, $message, $needReply); - $this->collectTrace(Config::COMMAND_TOPIC, $command, $message); + $this->collectTrace(null, $command, $message); return $result; } - /** - * {@inheritdoc} - */ - public function send($topic, $message) - { - $this->sendEvent($topic, $message); - } - - /** - * @param string $topic - * - * @return array - */ - public function getTopicTraces($topic) + public function getTopicTraces(string $topic): array { $topicTraces = []; foreach ($this->traces as $trace) { @@ -68,12 +49,7 @@ public function getTopicTraces($topic) return $topicTraces; } - /** - * @param string $command - * - * @return array - */ - public function getCommandTraces($command) + public function getCommandTraces(string $command): array { $commandTraces = []; foreach ($this->traces as $trace) { @@ -85,25 +61,17 @@ public function getCommandTraces($command) return $commandTraces; } - /** - * @return array - */ - public function getTraces() + public function getTraces(): array { return $this->traces; } - public function clearTraces() + public function clearTraces(): void { $this->traces = []; } - /** - * @param string|null $topic - * @param string|null $command - * @param mixed $message - */ - private function collectTrace($topic, $command, $message) + private function collectTrace(?string $topic, ?string $command, $message): void { $trace = [ 'topic' => $topic, @@ -117,7 +85,9 @@ private function collectTrace($topic, $command, $message) 'timestamp' => null, 'contentType' => null, 'messageId' => null, + 'sentAt' => (new \DateTime())->format('Y-m-d H:i:s.u'), ]; + if ($message instanceof Message) { $trace['body'] = $message->getBody(); $trace['headers'] = $message->getHeaders(); diff --git a/ConnectionFactoryFactory.php b/ConnectionFactoryFactory.php new file mode 100644 index 0000000..d23518c --- /dev/null +++ b/ConnectionFactoryFactory.php @@ -0,0 +1,69 @@ + $config]; + } + + if (false == is_array($config)) { + throw new \InvalidArgumentException('The config must be either array or DSN string.'); + } + + if (false == array_key_exists('dsn', $config)) { + throw new \InvalidArgumentException('The config must have dsn key set.'); + } + + $dsn = Dsn::parseFirst($config['dsn']); + + if ($factoryClass = $this->findFactoryClass($dsn, Resources::getAvailableConnections())) { + return new $factoryClass(1 === count($config) ? $config['dsn'] : $config); + } + + $knownConnections = Resources::getKnownConnections(); + if ($factoryClass = $this->findFactoryClass($dsn, $knownConnections)) { + throw new \LogicException(sprintf('To use given scheme "%s" a package has to be installed. Run "composer req %s" to add it.', $dsn->getScheme(), $knownConnections[$factoryClass]['package'])); + } + + throw new \LogicException(sprintf('A given scheme "%s" is not supported. Maybe it is a custom connection, make sure you registered it with "%s::addConnection".', $dsn->getScheme(), Resources::class)); + } + + private function findFactoryClass(Dsn $dsn, array $factories): ?string + { + $protocol = $dsn->getSchemeProtocol(); + + if ($dsn->getSchemeExtensions()) { + foreach ($factories as $connectionClass => $info) { + if (empty($info['supportedSchemeExtensions'])) { + continue; + } + + if (false == in_array($protocol, $info['schemes'], true)) { + continue; + } + + $diff = array_diff($info['supportedSchemeExtensions'], $dsn->getSchemeExtensions()); + if (empty($diff)) { + return $connectionClass; + } + } + } + + foreach ($factories as $driverClass => $info) { + if (false == in_array($protocol, $info['schemes'], true)) { + continue; + } + + return $driverClass; + } + + return null; + } +} diff --git a/ConnectionFactoryFactoryInterface.php b/ConnectionFactoryFactoryInterface.php new file mode 100644 index 0000000..f4ca4a6 --- /dev/null +++ b/ConnectionFactoryFactoryInterface.php @@ -0,0 +1,21 @@ +queue = $queue; + $this->processor = $processor; + } + + public function getQueue(): Queue + { + return $this->queue; + } + + public function getProcessor(): Processor + { + return $this->processor; + } +} diff --git a/Consumption/CallbackProcessor.php b/Consumption/CallbackProcessor.php index b002235..d15978f 100644 --- a/Consumption/CallbackProcessor.php +++ b/Consumption/CallbackProcessor.php @@ -2,29 +2,23 @@ namespace Enqueue\Consumption; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message as InteropMessage; +use Interop\Queue\Processor; -class CallbackProcessor implements PsrProcessor +class CallbackProcessor implements Processor { /** * @var callable */ private $callback; - /** - * @param callable $callback - */ public function __construct(callable $callback) { $this->callback = $callback; } - /** - * {@inheritdoc} - */ - public function process(PsrMessage $message, PsrContext $context) + public function process(InteropMessage $message, Context $context) { return call_user_func($this->callback, $message, $context); } diff --git a/Consumption/ChainExtension.php b/Consumption/ChainExtension.php index d7a24ad..83b4eba 100644 --- a/Consumption/ChainExtension.php +++ b/Consumption/ChainExtension.php @@ -2,90 +2,193 @@ namespace Enqueue\Consumption; +use Enqueue\Consumption\Context\End; +use Enqueue\Consumption\Context\InitLogger; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\Context\MessageResult; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\Context\PreSubscribe; +use Enqueue\Consumption\Context\ProcessorException; +use Enqueue\Consumption\Context\Start; + class ChainExtension implements ExtensionInterface { - use EmptyExtensionTrait; - - /** - * @var ExtensionInterface[] - */ - private $extensions; + private $startExtensions; + private $initLoggerExtensions; + private $preSubscribeExtensions; + private $preConsumeExtensions; + private $messageReceivedExtensions; + private $messageResultExtensions; + private $postMessageReceivedExtensions; + private $processorExceptionExtensions; + private $postConsumeExtensions; + private $endExtensions; - /** - * @param ExtensionInterface[] $extensions - */ public function __construct(array $extensions) { - $this->extensions = $extensions; + $this->startExtensions = []; + $this->initLoggerExtensions = []; + $this->preSubscribeExtensions = []; + $this->preConsumeExtensions = []; + $this->messageReceivedExtensions = []; + $this->messageResultExtensions = []; + $this->postMessageReceivedExtensions = []; + $this->processorExceptionExtensions = []; + $this->postConsumeExtensions = []; + $this->endExtensions = []; + + array_walk($extensions, function ($extension) { + if ($extension instanceof ExtensionInterface) { + $this->startExtensions[] = $extension; + $this->initLoggerExtensions[] = $extension; + $this->preSubscribeExtensions[] = $extension; + $this->preConsumeExtensions[] = $extension; + $this->messageReceivedExtensions[] = $extension; + $this->messageResultExtensions[] = $extension; + $this->postMessageReceivedExtensions[] = $extension; + $this->processorExceptionExtensions[] = $extension; + $this->postConsumeExtensions[] = $extension; + $this->endExtensions[] = $extension; + + return; + } + + $extensionValid = false; + if ($extension instanceof StartExtensionInterface) { + $this->startExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof InitLoggerExtensionInterface) { + $this->initLoggerExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof PreSubscribeExtensionInterface) { + $this->preSubscribeExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof PreConsumeExtensionInterface) { + $this->preConsumeExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof MessageReceivedExtensionInterface) { + $this->messageReceivedExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof MessageResultExtensionInterface) { + $this->messageResultExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof ProcessorExceptionExtensionInterface) { + $this->processorExceptionExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof PostMessageReceivedExtensionInterface) { + $this->postMessageReceivedExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof PostConsumeExtensionInterface) { + $this->postConsumeExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof EndExtensionInterface) { + $this->endExtensions[] = $extension; + + $extensionValid = true; + } + + if (false == $extensionValid) { + throw new \LogicException(sprintf('Invalid extension given %s', $extension::class)); + } + }); + } + + public function onInitLogger(InitLogger $context): void + { + foreach ($this->initLoggerExtensions as $extension) { + $extension->onInitLogger($context); + } } - /** - * @param Context $context - */ - public function onStart(Context $context) + public function onStart(Start $context): void { - foreach ($this->extensions as $extension) { + foreach ($this->startExtensions as $extension) { $extension->onStart($context); } } - /** - * @param Context $context - */ - public function onBeforeReceive(Context $context) + public function onPreSubscribe(PreSubscribe $context): void + { + foreach ($this->preSubscribeExtensions as $extension) { + $extension->onPreSubscribe($context); + } + } + + public function onPreConsume(PreConsume $context): void { - foreach ($this->extensions as $extension) { - $extension->onBeforeReceive($context); + foreach ($this->preConsumeExtensions as $extension) { + $extension->onPreConsume($context); } } - /** - * @param Context $context - */ - public function onPreReceived(Context $context) + public function onMessageReceived(MessageReceived $context): void { - foreach ($this->extensions as $extension) { - $extension->onPreReceived($context); + foreach ($this->messageReceivedExtensions as $extension) { + $extension->onMessageReceived($context); } } - /** - * @param Context $context - */ - public function onResult(Context $context) + public function onResult(MessageResult $context): void { - foreach ($this->extensions as $extension) { + foreach ($this->messageResultExtensions as $extension) { $extension->onResult($context); } } - /** - * @param Context $context - */ - public function onPostReceived(Context $context) + public function onProcessorException(ProcessorException $context): void + { + foreach ($this->processorExceptionExtensions as $extension) { + $extension->onProcessorException($context); + } + } + + public function onPostMessageReceived(PostMessageReceived $context): void { - foreach ($this->extensions as $extension) { - $extension->onPostReceived($context); + foreach ($this->postMessageReceivedExtensions as $extension) { + $extension->onPostMessageReceived($context); } } - /** - * @param Context $context - */ - public function onIdle(Context $context) + public function onPostConsume(PostConsume $context): void { - foreach ($this->extensions as $extension) { - $extension->onIdle($context); + foreach ($this->postConsumeExtensions as $extension) { + $extension->onPostConsume($context); } } - /** - * @param Context $context - */ - public function onInterrupted(Context $context) + public function onEnd(End $context): void { - foreach ($this->extensions as $extension) { - $extension->onInterrupted($context); + foreach ($this->endExtensions as $extension) { + $extension->onEnd($context); } } } diff --git a/Consumption/Context.php b/Consumption/Context.php deleted file mode 100644 index 09332b9..0000000 --- a/Consumption/Context.php +++ /dev/null @@ -1,233 +0,0 @@ -psrContext = $psrContext; - - $this->executionInterrupted = false; - } - - /** - * @return PsrMessage - */ - public function getPsrMessage() - { - return $this->psrMessage; - } - - /** - * @param PsrMessage $psrMessage - */ - public function setPsrMessage(PsrMessage $psrMessage) - { - if ($this->psrMessage) { - throw new IllegalContextModificationException('The message could be set once'); - } - - $this->psrMessage = $psrMessage; - } - - /** - * @return PsrContext - */ - public function getPsrContext() - { - return $this->psrContext; - } - - /** - * @return PsrConsumer - */ - public function getPsrConsumer() - { - return $this->psrConsumer; - } - - /** - * @param PsrConsumer $psrConsumer - */ - public function setPsrConsumer(PsrConsumer $psrConsumer) - { - if ($this->psrConsumer) { - throw new IllegalContextModificationException('The message consumer could be set once'); - } - - $this->psrConsumer = $psrConsumer; - } - - /** - * @return PsrProcessor - */ - public function getPsrProcessor() - { - return $this->psrProcessor; - } - - /** - * @param PsrProcessor $psrProcessor - */ - public function setPsrProcessor(PsrProcessor $psrProcessor) - { - if ($this->psrProcessor) { - throw new IllegalContextModificationException('The message processor could be set once'); - } - - $this->psrProcessor = $psrProcessor; - } - - /** - * @return \Exception - */ - public function getException() - { - return $this->exception; - } - - /** - * @param \Exception $exception - */ - public function setException(\Exception $exception) - { - $this->exception = $exception; - } - - /** - * @return Result|string - */ - public function getResult() - { - return $this->result; - } - - /** - * @param Result|string $result - */ - public function setResult($result) - { - if ($this->result) { - throw new IllegalContextModificationException('The result modification is not allowed'); - } - - $this->result = $result; - } - - /** - * @return bool - */ - public function isExecutionInterrupted() - { - return $this->executionInterrupted; - } - - /** - * @param bool $executionInterrupted - */ - public function setExecutionInterrupted($executionInterrupted) - { - if (false == $executionInterrupted && $this->executionInterrupted) { - throw new IllegalContextModificationException('The execution once interrupted could not be roll backed'); - } - - $this->executionInterrupted = $executionInterrupted; - } - - /** - * @return LoggerInterface - */ - public function getLogger() - { - return $this->logger; - } - - /** - * @param LoggerInterface $logger - */ - public function setLogger(LoggerInterface $logger) - { - if ($this->logger) { - throw new IllegalContextModificationException('The logger modification is not allowed'); - } - - $this->logger = $logger; - } - - /** - * @return PsrQueue - */ - public function getPsrQueue() - { - return $this->psrQueue; - } - - /** - * @param PsrQueue $psrQueue - */ - public function setPsrQueue(PsrQueue $psrQueue) - { - if ($this->psrQueue) { - throw new IllegalContextModificationException('The queue modification is not allowed'); - } - - $this->psrQueue = $psrQueue; - } -} diff --git a/Consumption/Context/End.php b/Consumption/Context/End.php new file mode 100644 index 0000000..07853b3 --- /dev/null +++ b/Consumption/Context/End.php @@ -0,0 +1,79 @@ +context = $context; + $this->logger = $logger; + $this->startTime = $startTime; + $this->endTime = $endTime; + $this->exitStatus = $exitStatus; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + /** + * In milliseconds. + */ + public function getStartTime(): int + { + return $this->startTime; + } + + /** + * In milliseconds. + */ + public function getEndTime(): int + { + return $this->startTime; + } + + public function getExitStatus(): ?int + { + return $this->exitStatus; + } +} diff --git a/Consumption/Context/InitLogger.php b/Consumption/Context/InitLogger.php new file mode 100644 index 0000000..c480572 --- /dev/null +++ b/Consumption/Context/InitLogger.php @@ -0,0 +1,28 @@ +logger = $logger; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function changeLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } +} diff --git a/Consumption/Context/MessageReceived.php b/Consumption/Context/MessageReceived.php new file mode 100644 index 0000000..35abf1c --- /dev/null +++ b/Consumption/Context/MessageReceived.php @@ -0,0 +1,109 @@ +context = $context; + $this->consumer = $consumer; + $this->message = $message; + $this->processor = $processor; + $this->receivedAt = $receivedAt; + $this->logger = $logger; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getConsumer(): Consumer + { + return $this->consumer; + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getProcessor(): Processor + { + return $this->processor; + } + + public function changeProcessor(Processor $processor): void + { + $this->processor = $processor; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function getReceivedAt(): int + { + return $this->receivedAt; + } + + public function getResult(): ?Result + { + return $this->result; + } + + public function setResult(Result $result): void + { + $this->result = $result; + } +} diff --git a/Consumption/Context/MessageResult.php b/Consumption/Context/MessageResult.php new file mode 100644 index 0000000..4fa8f7d --- /dev/null +++ b/Consumption/Context/MessageResult.php @@ -0,0 +1,93 @@ +context = $context; + $this->consumer = $consumer; + $this->message = $message; + $this->logger = $logger; + $this->result = $result; + $this->receivedAt = $receivedAt; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getConsumer(): Consumer + { + return $this->consumer; + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function getReceivedAt(): int + { + return $this->receivedAt; + } + + /** + * @return Result|object|string|null + */ + public function getResult() + { + return $this->result; + } + + /** + * @param Result|string|object|null $result + */ + public function changeResult($result): void + { + $this->result = $result; + } +} diff --git a/Consumption/Context/PostConsume.php b/Consumption/Context/PostConsume.php new file mode 100644 index 0000000..a6f1d83 --- /dev/null +++ b/Consumption/Context/PostConsume.php @@ -0,0 +1,108 @@ +context = $context; + $this->subscriptionConsumer = $subscriptionConsumer; + $this->receivedMessagesCount = $receivedMessagesCount; + $this->cycle = $cycle; + $this->startTime = $startTime; + $this->logger = $logger; + + $this->executionInterrupted = false; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getSubscriptionConsumer(): SubscriptionConsumer + { + return $this->subscriptionConsumer; + } + + public function getReceivedMessagesCount(): int + { + return $this->receivedMessagesCount; + } + + public function getCycle(): int + { + return $this->cycle; + } + + public function getStartTime(): int + { + return $this->startTime; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function getExitStatus(): ?int + { + return $this->exitStatus; + } + + public function isExecutionInterrupted(): bool + { + return $this->executionInterrupted; + } + + public function interruptExecution(?int $exitStatus = null): void + { + $this->exitStatus = $exitStatus; + $this->executionInterrupted = true; + } +} diff --git a/Consumption/Context/PostMessageReceived.php b/Consumption/Context/PostMessageReceived.php new file mode 100644 index 0000000..23df2c8 --- /dev/null +++ b/Consumption/Context/PostMessageReceived.php @@ -0,0 +1,119 @@ +context = $context; + $this->consumer = $consumer; + $this->message = $message; + $this->result = $result; + $this->receivedAt = $receivedAt; + $this->logger = $logger; + + $this->executionInterrupted = false; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getConsumer(): Consumer + { + return $this->consumer; + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function getReceivedAt(): int + { + return $this->receivedAt; + } + + /** + * @return Result|object|string|null + */ + public function getResult() + { + return $this->result; + } + + public function getExitStatus(): ?int + { + return $this->exitStatus; + } + + public function isExecutionInterrupted(): bool + { + return $this->executionInterrupted; + } + + public function interruptExecution(?int $exitStatus = null): void + { + $this->exitStatus = $exitStatus; + $this->executionInterrupted = true; + } +} diff --git a/Consumption/Context/PreConsume.php b/Consumption/Context/PreConsume.php new file mode 100644 index 0000000..77cc7d0 --- /dev/null +++ b/Consumption/Context/PreConsume.php @@ -0,0 +1,108 @@ +context = $context; + $this->subscriptionConsumer = $subscriptionConsumer; + $this->logger = $logger; + $this->cycle = $cycle; + $this->receiveTimeout = $receiveTimeout; + $this->startTime = $startTime; + + $this->executionInterrupted = false; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getSubscriptionConsumer(): SubscriptionConsumer + { + return $this->subscriptionConsumer; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function getCycle(): int + { + return $this->cycle; + } + + public function getReceiveTimeout(): int + { + return $this->receiveTimeout; + } + + public function getStartTime(): int + { + return $this->startTime; + } + + public function getExitStatus(): ?int + { + return $this->exitStatus; + } + + public function isExecutionInterrupted(): bool + { + return $this->executionInterrupted; + } + + public function interruptExecution(?int $exitStatus = null): void + { + $this->exitStatus = $exitStatus; + $this->executionInterrupted = true; + } +} diff --git a/Consumption/Context/PreSubscribe.php b/Consumption/Context/PreSubscribe.php new file mode 100644 index 0000000..dbc74bb --- /dev/null +++ b/Consumption/Context/PreSubscribe.php @@ -0,0 +1,59 @@ +context = $context; + $this->processor = $processor; + $this->consumer = $consumer; + $this->logger = $logger; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getProcessor(): Processor + { + return $this->processor; + } + + public function getConsumer(): Consumer + { + return $this->consumer; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } +} diff --git a/Consumption/Context/ProcessorException.php b/Consumption/Context/ProcessorException.php new file mode 100644 index 0000000..329b13d --- /dev/null +++ b/Consumption/Context/ProcessorException.php @@ -0,0 +1,96 @@ +context = $context; + $this->consumer = $consumer; + $this->message = $message; + $this->exception = $exception; + $this->logger = $logger; + $this->receivedAt = $receivedAt; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getConsumer(): Consumer + { + return $this->consumer; + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getException(): \Throwable + { + return $this->exception; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function getReceivedAt(): int + { + return $this->receivedAt; + } + + public function getResult(): ?Result + { + return $this->result; + } + + public function setResult(Result $result): void + { + $this->result = $result; + } +} diff --git a/Consumption/Context/Start.php b/Consumption/Context/Start.php new file mode 100644 index 0000000..84db29c --- /dev/null +++ b/Consumption/Context/Start.php @@ -0,0 +1,128 @@ +context = $context; + $this->logger = $logger; + $this->processors = $processors; + $this->receiveTimeout = $receiveTimeout; + $this->startTime = $startTime; + + $this->executionInterrupted = false; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + /** + * In milliseconds. + */ + public function getReceiveTimeout(): int + { + return $this->receiveTimeout; + } + + /** + * In milliseconds. + */ + public function changeReceiveTimeout(int $timeout): void + { + $this->receiveTimeout = $timeout; + } + + /** + * In milliseconds. + */ + public function getStartTime(): int + { + return $this->startTime; + } + + /** + * @return BoundProcessor[] + */ + public function getBoundProcessors(): array + { + return $this->processors; + } + + /** + * @param BoundProcessor[] $processors + */ + public function changeBoundProcessors(array $processors): void + { + $this->processors = []; + array_walk($processors, function (BoundProcessor $processor) { + $this->processors[] = $processor; + }); + } + + public function getExitStatus(): ?int + { + return $this->exitStatus; + } + + public function isExecutionInterrupted(): bool + { + return $this->executionInterrupted; + } + + public function interruptExecution(?int $exitStatus = null): void + { + $this->exitStatus = $exitStatus; + $this->executionInterrupted = true; + } +} diff --git a/Consumption/EmptyExtensionTrait.php b/Consumption/EmptyExtensionTrait.php deleted file mode 100644 index 0f6b849..0000000 --- a/Consumption/EmptyExtensionTrait.php +++ /dev/null @@ -1,55 +0,0 @@ -exitStatus = $context->getExitStatus(); + } + + public function getExitStatus(): ?int + { + return $this->exitStatus; + } +} diff --git a/Consumption/Extension/LimitConsumedMessagesExtension.php b/Consumption/Extension/LimitConsumedMessagesExtension.php index ef6ec52..0dc6fec 100644 --- a/Consumption/Extension/LimitConsumedMessagesExtension.php +++ b/Consumption/Extension/LimitConsumedMessagesExtension.php @@ -2,14 +2,14 @@ namespace Enqueue\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; +use Enqueue\Consumption\PreConsumeExtensionInterface; +use Psr\Log\LoggerInterface; -class LimitConsumedMessagesExtension implements ExtensionInterface +class LimitConsumedMessagesExtension implements PreConsumeExtensionInterface, PostMessageReceivedExtensionInterface { - use EmptyExtensionTrait; - /** * @var int */ @@ -20,54 +20,41 @@ class LimitConsumedMessagesExtension implements ExtensionInterface */ protected $messageConsumed; - /** - * @param int $messageLimit - */ - public function __construct($messageLimit) + public function __construct(int $messageLimit) { - if (false == is_int($messageLimit)) { - throw new \InvalidArgumentException(sprintf( - 'Expected message limit is int but got: "%s"', - is_object($messageLimit) ? get_class($messageLimit) : gettype($messageLimit) - )); - } - $this->messageLimit = $messageLimit; $this->messageConsumed = 0; } - /** - * {@inheritdoc} - */ - public function onBeforeReceive(Context $context) + public function onPreConsume(PreConsume $context): void { // this is added here to handle an edge case. when a user sets zero as limit. - $this->checkMessageLimit($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) + public function onPostMessageReceived(PostMessageReceived $context): void { ++$this->messageConsumed; - $this->checkMessageLimit($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * @param Context $context - */ - protected function checkMessageLimit(Context $context) + protected function shouldBeStopped(LoggerInterface $logger): bool { if ($this->messageConsumed >= $this->messageLimit) { - $context->getLogger()->debug(sprintf( + $logger->debug(sprintf( '[LimitConsumedMessagesExtension] Message consumption is interrupted since the message limit reached.'. ' limit: "%s"', $this->messageLimit )); - $context->setExecutionInterrupted(true); + return true; } + + return false; } } diff --git a/Consumption/Extension/LimitConsumerMemoryExtension.php b/Consumption/Extension/LimitConsumerMemoryExtension.php index c03686f..7edbf23 100644 --- a/Consumption/Extension/LimitConsumerMemoryExtension.php +++ b/Consumption/Extension/LimitConsumerMemoryExtension.php @@ -2,14 +2,16 @@ namespace Enqueue\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\PostConsumeExtensionInterface; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; +use Enqueue\Consumption\PreConsumeExtensionInterface; +use Psr\Log\LoggerInterface; -class LimitConsumerMemoryExtension implements ExtensionInterface +class LimitConsumerMemoryExtension implements PreConsumeExtensionInterface, PostMessageReceivedExtensionInterface, PostConsumeExtensionInterface { - use EmptyExtensionTrait; - /** * @var int */ @@ -21,53 +23,46 @@ class LimitConsumerMemoryExtension implements ExtensionInterface public function __construct($memoryLimit) { if (false == is_int($memoryLimit)) { - throw new \InvalidArgumentException(sprintf( - 'Expected memory limit is int but got: "%s"', - is_object($memoryLimit) ? get_class($memoryLimit) : gettype($memoryLimit) - )); + throw new \InvalidArgumentException(sprintf('Expected memory limit is int but got: "%s"', is_object($memoryLimit) ? $memoryLimit::class : gettype($memoryLimit))); } $this->memoryLimit = $memoryLimit * 1024 * 1024; } - /** - * {@inheritdoc} - */ - public function onBeforeReceive(Context $context) + public function onPreConsume(PreConsume $context): void { - $this->checkMemory($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) + public function onPostMessageReceived(PostMessageReceived $context): void { - $this->checkMemory($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * {@inheritdoc} - */ - public function onIdle(Context $context) + public function onPostConsume(PostConsume $context): void { - $this->checkMemory($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * @param Context $context - */ - protected function checkMemory(Context $context) + protected function shouldBeStopped(LoggerInterface $logger): bool { $memoryUsage = memory_get_usage(true); if ($memoryUsage >= $this->memoryLimit) { - $context->getLogger()->debug(sprintf( + $logger->debug(sprintf( '[LimitConsumerMemoryExtension] Interrupt execution as memory limit reached. limit: "%s", used: "%s"', $this->memoryLimit, $memoryUsage )); - $context->setExecutionInterrupted(true); + return true; } + + return false; } } diff --git a/Consumption/Extension/LimitConsumptionTimeExtension.php b/Consumption/Extension/LimitConsumptionTimeExtension.php index 65221f5..1953aa2 100644 --- a/Consumption/Extension/LimitConsumptionTimeExtension.php +++ b/Consumption/Extension/LimitConsumptionTimeExtension.php @@ -2,66 +2,61 @@ namespace Enqueue\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; - -class LimitConsumptionTimeExtension implements ExtensionInterface +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\PostConsumeExtensionInterface; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; +use Enqueue\Consumption\PreConsumeExtensionInterface; +use Psr\Log\LoggerInterface; + +class LimitConsumptionTimeExtension implements PreConsumeExtensionInterface, PostConsumeExtensionInterface, PostMessageReceivedExtensionInterface { - use EmptyExtensionTrait; - /** * @var \DateTime */ protected $timeLimit; - /** - * @param \DateTime $timeLimit - */ public function __construct(\DateTime $timeLimit) { $this->timeLimit = $timeLimit; } - /** - * {@inheritdoc} - */ - public function onBeforeReceive(Context $context) + public function onPreConsume(PreConsume $context): void { - $this->checkTime($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * {@inheritdoc} - */ - public function onIdle(Context $context) + public function onPostConsume(PostConsume $context): void { - $this->checkTime($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) + public function onPostMessageReceived(PostMessageReceived $context): void { - $this->checkTime($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * @param Context $context - */ - protected function checkTime(Context $context) + protected function shouldBeStopped(LoggerInterface $logger): bool { $now = new \DateTime(); if ($now >= $this->timeLimit) { - $context->getLogger()->debug(sprintf( + $logger->debug(sprintf( '[LimitConsumptionTimeExtension] Execution interrupted as limit time has passed.'. ' now: "%s", time-limit: "%s"', - $now->format(DATE_ISO8601), - $this->timeLimit->format(DATE_ISO8601) + $now->format(\DATE_ISO8601), + $this->timeLimit->format(\DATE_ISO8601) )); - $context->setExecutionInterrupted(true); + return true; } + + return false; } } diff --git a/Consumption/Extension/LogExtension.php b/Consumption/Extension/LogExtension.php new file mode 100644 index 0000000..14383c4 --- /dev/null +++ b/Consumption/Extension/LogExtension.php @@ -0,0 +1,67 @@ +getLogger()->debug('Consumption has started'); + } + + public function onEnd(End $context): void + { + $context->getLogger()->debug('Consumption has ended'); + } + + public function onMessageReceived(MessageReceived $context): void + { + $message = $context->getMessage(); + + $context->getLogger()->debug("Received from {queueName}\t{body}", [ + 'queueName' => $context->getConsumer()->getQueue()->getQueueName(), + 'redelivered' => $message->isRedelivered(), + 'body' => Stringify::that($message->getBody()), + 'properties' => Stringify::that($message->getProperties()), + 'headers' => Stringify::that($message->getHeaders()), + ]); + } + + public function onPostMessageReceived(PostMessageReceived $context): void + { + $message = $context->getMessage(); + $queue = $context->getConsumer()->getQueue(); + $result = $context->getResult(); + + $reason = ''; + $logMessage = "Processed from {queueName}\t{body}\t{result}"; + if ($result instanceof Result && $result->getReason()) { + $reason = $result->getReason(); + $logMessage .= ' {reason}'; + } + $logContext = [ + 'result' => str_replace('enqueue.', '', $result), + 'reason' => $reason, + 'queueName' => $queue->getQueueName(), + 'body' => Stringify::that($message->getBody()), + 'properties' => Stringify::that($message->getProperties()), + 'headers' => Stringify::that($message->getHeaders()), + ]; + + $logLevel = Result::REJECT == ((string) $result) ? LogLevel::ERROR : LogLevel::INFO; + + $context->getLogger()->log($logLevel, $logMessage, $logContext); + } +} diff --git a/Consumption/Extension/LoggerExtension.php b/Consumption/Extension/LoggerExtension.php index 0779c90..90e92be 100644 --- a/Consumption/Extension/LoggerExtension.php +++ b/Consumption/Extension/LoggerExtension.php @@ -2,89 +2,30 @@ namespace Enqueue\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; -use Enqueue\Consumption\Result; -use Interop\Queue\PsrMessage; +use Enqueue\Consumption\Context\InitLogger; +use Enqueue\Consumption\InitLoggerExtensionInterface; use Psr\Log\LoggerInterface; -class LoggerExtension implements ExtensionInterface +class LoggerExtension implements InitLoggerExtensionInterface { - use EmptyExtensionTrait; - /** * @var LoggerInterface */ private $logger; - /** - * @param LoggerInterface $logger - */ public function __construct(LoggerInterface $logger) { $this->logger = $logger; } - /** - * {@inheritdoc} - */ - public function onStart(Context $context) + public function onInitLogger(InitLogger $context): void { - if ($context->getLogger()) { - $context->getLogger()->debug(sprintf( - 'Skip setting context\'s logger "%s". Another one "%s" has already been set.', - get_class($this->logger), - get_class($context->getLogger()) - )); - } else { - $context->setLogger($this->logger); - $this->logger->debug(sprintf('Set context\'s logger "%s"', get_class($this->logger))); - } - } + $previousLogger = $context->getLogger(); - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) - { - if (false == $context->getResult() instanceof Result) { - return; - } - - /** @var $result Result */ - $result = $context->getResult(); - - switch ($result->getStatus()) { - case Result::REJECT: - case Result::REQUEUE: - if ($result->getReason()) { - $this->logger->error($result->getReason(), $this->messageToLogContext($context->getPsrMessage())); - } + if ($previousLogger !== $this->logger) { + $context->changeLogger($this->logger); - break; - case Result::ACK: - if ($result->getReason()) { - $this->logger->info($result->getReason(), $this->messageToLogContext($context->getPsrMessage())); - } - - break; - default: - throw new \LogicException(sprintf('Got unexpected message result. "%s"', $result->getStatus())); + $this->logger->debug(sprintf('Change logger from "%s" to "%s"', $previousLogger::class, get_class($this->logger))); } } - - /** - * @param PsrMessage $message - * - * @return array - */ - private function messageToLogContext(PsrMessage $message) - { - return [ - 'body' => $message->getBody(), - 'headers' => $message->getHeaders(), - 'properties' => $message->getProperties(), - ]; - } } diff --git a/Consumption/Extension/NicenessExtension.php b/Consumption/Extension/NicenessExtension.php new file mode 100644 index 0000000..436a8ec --- /dev/null +++ b/Consumption/Extension/NicenessExtension.php @@ -0,0 +1,38 @@ +niceness = $niceness; + } + + public function onStart(Start $context): void + { + if (0 !== $this->niceness) { + $changed = @proc_nice($this->niceness); + if (!$changed) { + throw new \InvalidArgumentException(sprintf('Cannot change process niceness, got warning: "%s"', error_get_last()['message'])); + } + } + } +} diff --git a/Consumption/Extension/ReplyExtension.php b/Consumption/Extension/ReplyExtension.php index 0d7a76e..c1ac19b 100644 --- a/Consumption/Extension/ReplyExtension.php +++ b/Consumption/Extension/ReplyExtension.php @@ -2,21 +2,15 @@ namespace Enqueue\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; use Enqueue\Consumption\Result; -class ReplyExtension implements ExtensionInterface +class ReplyExtension implements PostMessageReceivedExtensionInterface { - use EmptyExtensionTrait; - - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) + public function onPostMessageReceived(PostMessageReceived $context): void { - $replyTo = $context->getPsrMessage()->getReplyTo(); + $replyTo = $context->getMessage()->getReplyTo(); if (false == $replyTo) { return; } @@ -31,13 +25,13 @@ public function onPostReceived(Context $context) return; } - $correlationId = $context->getPsrMessage()->getCorrelationId(); + $correlationId = $context->getMessage()->getCorrelationId(); $replyMessage = clone $result->getReply(); $replyMessage->setCorrelationId($correlationId); - $replyQueue = $context->getPsrContext()->createQueue($replyTo); + $replyQueue = $context->getContext()->createQueue($replyTo); $context->getLogger()->debug(sprintf('[ReplyExtension] Send reply to "%s"', $replyTo)); - $context->getPsrContext()->createProducer()->send($replyQueue, $replyMessage); + $context->getContext()->createProducer()->send($replyQueue, $replyMessage); } } diff --git a/Consumption/Extension/SignalExtension.php b/Consumption/Extension/SignalExtension.php index a8b53e8..8ea5307 100644 --- a/Consumption/Extension/SignalExtension.php +++ b/Consumption/Extension/SignalExtension.php @@ -2,16 +2,19 @@ namespace Enqueue\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\Context\Start; use Enqueue\Consumption\Exception\LogicException; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\PostConsumeExtensionInterface; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; +use Enqueue\Consumption\PreConsumeExtensionInterface; +use Enqueue\Consumption\StartExtensionInterface; use Psr\Log\LoggerInterface; -class SignalExtension implements ExtensionInterface +class SignalExtension implements StartExtensionInterface, PreConsumeExtensionInterface, PostMessageReceivedExtensionInterface, PostConsumeExtensionInterface { - use EmptyExtensionTrait; - /** * @var bool */ @@ -22,95 +25,55 @@ class SignalExtension implements ExtensionInterface */ protected $logger; - /** - * {@inheritdoc} - */ - public function onStart(Context $context) + public function onStart(Start $context): void { if (false == extension_loaded('pcntl')) { throw new LogicException('The pcntl extension is required in order to catch signals.'); } - if (function_exists('pcntl_async_signals')) { - pcntl_async_signals(true); - } + pcntl_async_signals(true); - pcntl_signal(SIGTERM, [$this, 'handleSignal']); - pcntl_signal(SIGQUIT, [$this, 'handleSignal']); - pcntl_signal(SIGINT, [$this, 'handleSignal']); + pcntl_signal(\SIGTERM, [$this, 'handleSignal']); + pcntl_signal(\SIGQUIT, [$this, 'handleSignal']); + pcntl_signal(\SIGINT, [$this, 'handleSignal']); + $this->logger = $context->getLogger(); $this->interruptConsumption = false; } - /** - * @param Context $context - */ - public function onBeforeReceive(Context $context) + public function onPreConsume(PreConsume $context): void { $this->logger = $context->getLogger(); - $this->dispatchSignal(); - - $this->interruptExecutionIfNeeded($context); - } - - /** - * {@inheritdoc} - */ - public function onPreReceived(Context $context) - { - $this->interruptExecutionIfNeeded($context); - } - - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) - { - $this->dispatchSignal(); - - $this->interruptExecutionIfNeeded($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * {@inheritdoc} - */ - public function onIdle(Context $context) + public function onPostMessageReceived(PostMessageReceived $context): void { - $this->dispatchSignal(); - - $this->interruptExecutionIfNeeded($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * @param Context $context - */ - public function interruptExecutionIfNeeded(Context $context) + public function onPostConsume(PostConsume $context): void { - if (false == $context->isExecutionInterrupted() && $this->interruptConsumption) { - if ($this->logger) { - $this->logger->debug('[SignalExtension] Interrupt execution'); - } - - $context->setExecutionInterrupted($this->interruptConsumption); - - $this->interruptConsumption = false; + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); } } - /** - * @param int $signal - */ - public function handleSignal($signal) + public function handleSignal(int $signal): void { if ($this->logger) { $this->logger->debug(sprintf('[SignalExtension] Caught signal: %s', $signal)); } switch ($signal) { - case SIGTERM: // 15 : supervisor default stop - case SIGQUIT: // 3 : kill -s QUIT - case SIGINT: // 2 : ctrl+c + case \SIGTERM: // 15 : supervisor default stop + case \SIGQUIT: // 3 : kill -s QUIT + case \SIGINT: // 2 : ctrl+c if ($this->logger) { $this->logger->debug('[SignalExtension] Interrupt consumption'); } @@ -122,10 +85,16 @@ public function handleSignal($signal) } } - private function dispatchSignal() + private function shouldBeStopped(LoggerInterface $logger): bool { - if (false == function_exists('pcntl_async_signals')) { - pcntl_signal_dispatch(); + if ($this->interruptConsumption) { + $logger->debug('[SignalExtension] Interrupt execution'); + + $this->interruptConsumption = false; + + return true; } + + return false; } } diff --git a/Consumption/ExtensionInterface.php b/Consumption/ExtensionInterface.php index 2a5d7bb..326a98f 100644 --- a/Consumption/ExtensionInterface.php +++ b/Consumption/ExtensionInterface.php @@ -2,65 +2,6 @@ namespace Enqueue\Consumption; -interface ExtensionInterface +interface ExtensionInterface extends StartExtensionInterface, PreSubscribeExtensionInterface, PreConsumeExtensionInterface, MessageReceivedExtensionInterface, PostMessageReceivedExtensionInterface, MessageResultExtensionInterface, ProcessorExceptionExtensionInterface, PostConsumeExtensionInterface, EndExtensionInterface, InitLoggerExtensionInterface { - /** - * Executed only once at the very begining of the consumption. - * At this stage the context does not contain processor, consumer and queue. - * - * @param Context $context - */ - public function onStart(Context $context); - - /** - * Executed at every new cycle before we asked a broker for a new message. - * At this stage the context already contains processor, consumer and queue. - * The consumption could be interrupted at this step. - * - * @param Context $context - */ - public function onBeforeReceive(Context $context); - - /** - * Executed when a new message is received from a broker but before it was passed to processor - * The context contains a message. - * The extension may set a status. If the status is set the exception is thrown - * The consumption could be interrupted at this step but it exits after the message is processed. - * - * @param Context $context - */ - public function onPreReceived(Context $context); - - /** - * Executed when a message is processed by a processor or a result was set in onPreReceived method. - * BUT before the message status was sent to the broker - * The consumption could be interrupted at this step but it exits after the message is processed. - * - * @param Context $context - */ - public function onResult(Context $context); - - /** - * Executed when a message is processed by a processor. - * The context contains a status, which could not be changed. - * The consumption could be interrupted at this step but it exits after the message is processed. - * - * @param Context $context - */ - public function onPostReceived(Context $context); - - /** - * Called each time at the end of the cycle if nothing was done. - * - * @param Context $context - */ - public function onIdle(Context $context); - - /** - * Called when the consumption was interrupted by an extension or exception - * In case of exception it will be present in the context. - * - * @param Context $context - */ - public function onInterrupted(Context $context); } diff --git a/Consumption/FallbackSubscriptionConsumer.php b/Consumption/FallbackSubscriptionConsumer.php new file mode 100644 index 0000000..15e2f27 --- /dev/null +++ b/Consumption/FallbackSubscriptionConsumer.php @@ -0,0 +1,109 @@ +subscribers = []; + } + + public function consume(int $timeoutMs = 0): void + { + if (!$subscriberCount = \count($this->subscribers)) { + throw new \LogicException('No subscribers'); + } + + $timeout = $timeoutMs / 1000; + $endAt = microtime(true) + $timeout; + + while (true) { + /** + * @var string + * @var Consumer $consumer + * @var callable $processor + */ + foreach ($this->subscribers as $queueName => list($consumer, $callback)) { + $message = 1 === $subscriberCount ? $consumer->receive($timeoutMs) : $consumer->receiveNoWait(); + + if ($message) { + if (false === call_user_func($callback, $message, $consumer)) { + return; + } + } elseif (1 !== $subscriberCount) { + if ($timeout && microtime(true) >= $endAt) { + return; + } + + $this->idleTime && usleep($this->idleTime); + } + + if ($timeout && microtime(true) >= $endAt) { + return; + } + } + } + } + + public function subscribe(Consumer $consumer, callable $callback): void + { + $queueName = $consumer->getQueue()->getQueueName(); + if (array_key_exists($queueName, $this->subscribers)) { + if ($this->subscribers[$queueName][0] === $consumer && $this->subscribers[$queueName][1] === $callback) { + return; + } + + throw new \InvalidArgumentException(sprintf('There is a consumer subscribed to queue: "%s"', $queueName)); + } + + $this->subscribers[$queueName] = [$consumer, $callback]; + } + + public function unsubscribe(Consumer $consumer): void + { + if (false == array_key_exists($consumer->getQueue()->getQueueName(), $this->subscribers)) { + return; + } + + if ($this->subscribers[$consumer->getQueue()->getQueueName()][0] !== $consumer) { + return; + } + + unset($this->subscribers[$consumer->getQueue()->getQueueName()]); + } + + public function unsubscribeAll(): void + { + $this->subscribers = []; + } + + public function getIdleTime(): int + { + return $this->idleTime; + } + + /** + * The time in milliseconds the consumer waits if no message has been received. + */ + public function setIdleTime(int $idleTime): void + { + $this->idleTime = $idleTime; + } +} diff --git a/Consumption/InitLoggerExtensionInterface.php b/Consumption/InitLoggerExtensionInterface.php new file mode 100644 index 0000000..936e32d --- /dev/null +++ b/Consumption/InitLoggerExtensionInterface.php @@ -0,0 +1,14 @@ +psrContext = $psrContext; - $this->staticExtension = $extension ?: new ChainExtension([]); - $this->idleTimeout = $idleTimeout; + $this->interopContext = $interopContext; $this->receiveTimeout = $receiveTimeout; - $this->boundProcessors = []; - $this->logger = new NullLogger(); - } + $this->staticExtension = $extension ?: new ChainExtension([]); + $this->logger = $logger ?: new NullLogger(); - /** - * @param int $timeout - */ - public function setIdleTimeout($timeout) - { - $this->idleTimeout = (int) $timeout; - } + $this->boundProcessors = []; + array_walk($boundProcessors, function (BoundProcessor $processor) { + $this->boundProcessors[] = $processor; + }); - /** - * @return int - */ - public function getIdleTimeout() - { - return $this->idleTimeout; + $this->fallbackSubscriptionConsumer = new FallbackSubscriptionConsumer(); } - /** - * @param int $timeout - */ - public function setReceiveTimeout($timeout) + public function setReceiveTimeout(int $timeout): void { - $this->receiveTimeout = (int) $timeout; + $this->receiveTimeout = $timeout; } - /** - * @return int - */ - public function getReceiveTimeout() + public function getReceiveTimeout(): int { return $this->receiveTimeout; } - /** - * @return PsrContext - */ - public function getPsrContext() + public function getContext(): InteropContext { - return $this->psrContext; + return $this->interopContext; } - /** - * @param PsrQueue|string $queue - * @param PsrProcessor|callable $processor - * - * @return QueueConsumer - */ - public function bind($queue, $processor) + public function bind($queue, Processor $processor): QueueConsumerInterface { if (is_string($queue)) { - $queue = $this->psrContext->createQueue($queue); - } - if (is_callable($processor)) { - $processor = new CallbackProcessor($processor); + $queue = $this->interopContext->createQueue($queue); } - InvalidArgumentException::assertInstanceOf($queue, PsrQueue::class); - InvalidArgumentException::assertInstanceOf($processor, PsrProcessor::class); + InvalidArgumentException::assertInstanceOf($queue, InteropQueue::class); if (empty($queue->getQueueName())) { throw new LogicException('The queue name must be not empty.'); @@ -144,247 +112,221 @@ public function bind($queue, $processor) throw new LogicException(sprintf('The queue was already bound. Queue: %s', $queue->getQueueName())); } - $this->boundProcessors[$queue->getQueueName()] = [$queue, $processor]; + $this->boundProcessors[$queue->getQueueName()] = new BoundProcessor($queue, $processor); return $this; } - /** - * Runtime extension - is an extension or a collection of extensions which could be set on runtime. - * Here's a good example: @see LimitsExtensionsCommandTrait. - * - * @param ExtensionInterface|ChainExtension|null $runtimeExtension - * - * @throws \Exception - */ - public function consume(ExtensionInterface $runtimeExtension = null) + public function bindCallback($queue, callable $processor): QueueConsumerInterface + { + return $this->bind($queue, new CallbackProcessor($processor)); + } + + public function consume(?ExtensionInterface $runtimeExtension = null): void { + $extension = $runtimeExtension ? + new ChainExtension([$this->staticExtension, $runtimeExtension]) : + $this->staticExtension + ; + + $initLogger = new InitLogger($this->logger); + $extension->onInitLogger($initLogger); + + $this->logger = $initLogger->getLogger(); + + $startTime = (int) (microtime(true) * 1000); + + $start = new Start( + $this->interopContext, + $this->logger, + $this->boundProcessors, + $this->receiveTimeout, + $startTime + ); + + $extension->onStart($start); + + if ($start->isExecutionInterrupted()) { + $this->onEnd($extension, $startTime, $start->getExitStatus()); + + return; + } + + $this->logger = $start->getLogger(); + $this->receiveTimeout = $start->getReceiveTimeout(); + $this->boundProcessors = $start->getBoundProcessors(); + if (empty($this->boundProcessors)) { throw new \LogicException('There is nothing to consume. It is required to bind something before calling consume method.'); } - /** @var PsrConsumer[] $consumers */ + /** @var Consumer[] $consumers */ $consumers = []; - /** @var PsrQueue $queue */ - foreach ($this->boundProcessors as list($queue, $processor)) { - $consumers[$queue->getQueueName()] = $this->psrContext->createConsumer($queue); + foreach ($this->boundProcessors as $queueName => $boundProcessor) { + $queue = $boundProcessor->getQueue(); + + $consumers[$queue->getQueueName()] = $this->interopContext->createConsumer($queue); } - $this->extension = $runtimeExtension ? - new ChainExtension([$this->staticExtension, $runtimeExtension]) : - $this->staticExtension - ; + try { + $subscriptionConsumer = $this->interopContext->createSubscriptionConsumer(); + } catch (SubscriptionConsumerNotSupportedException $e) { + $subscriptionConsumer = $this->fallbackSubscriptionConsumer; + } - $context = new Context($this->psrContext); - $this->extension->onStart($context); + $receivedMessagesCount = 0; + $interruptExecution = false; - $this->logger = $context->getLogger() ?: new NullLogger(); - $this->logger->info('Start consuming'); + $callback = function (InteropMessage $message, Consumer $consumer) use (&$receivedMessagesCount, &$interruptExecution, $extension) { + ++$receivedMessagesCount; - if ($this->psrContext instanceof AmqpContext) { - $callback = function (AmqpMessage $message, AmqpConsumer $consumer) use (&$context) { - $currentProcessor = null; + $receivedAt = (int) (microtime(true) * 1000); + $queue = $consumer->getQueue(); + if (false == array_key_exists($queue->getQueueName(), $this->boundProcessors)) { + throw new \LogicException(sprintf('The processor for the queue "%s" could not be found.', $queue->getQueueName())); + } - /** @var PsrQueue $queue */ - foreach ($this->boundProcessors as list($queue, $processor)) { - if ($queue->getQueueName() === $consumer->getQueue()->getQueueName()) { - $currentProcessor = $processor; - } - } + $processor = $this->boundProcessors[$queue->getQueueName()]->getProcessor(); - if (false == $currentProcessor) { - throw new \LogicException(sprintf('The processor for the queue "%s" could not be found.', $consumer->getQueue()->getQueueName())); + $messageReceived = new MessageReceived($this->interopContext, $consumer, $message, $processor, $receivedAt, $this->logger); + $extension->onMessageReceived($messageReceived); + $result = $messageReceived->getResult(); + $processor = $messageReceived->getProcessor(); + if (null === $result) { + try { + $result = $processor->process($message, $this->interopContext); + } catch (\Exception|\Throwable $e) { + $result = $this->onProcessorException($extension, $consumer, $message, $e, $receivedAt); } + } - $context = new Context($this->psrContext); - $context->setLogger($this->logger); - $context->setPsrQueue($consumer->getQueue()); - $context->setPsrConsumer($consumer); - $context->setPsrProcessor($currentProcessor); - $context->setPsrMessage($message); - - $this->doConsume($this->extension, $context); + $messageResult = new MessageResult($this->interopContext, $consumer, $message, $result, $receivedAt, $this->logger); + $extension->onResult($messageResult); + $result = $messageResult->getResult(); + + switch ($result) { + case Result::ACK: + $consumer->acknowledge($message); + break; + case Result::REJECT: + $consumer->reject($message, false); + break; + case Result::REQUEUE: + $consumer->reject($message, true); + break; + case Result::ALREADY_ACKNOWLEDGED: + break; + default: + throw new \LogicException(sprintf('Status is not supported: %s', $result)); + } - return true; - }; + $postMessageReceived = new PostMessageReceived($this->interopContext, $consumer, $message, $result, $receivedAt, $this->logger); + $extension->onPostMessageReceived($postMessageReceived); - foreach ($consumers as $consumer) { - /* @var AmqpConsumer $consumer */ + if ($postMessageReceived->isExecutionInterrupted()) { + $interruptExecution = true; - $this->psrContext->subscribe($consumer, $callback); + return false; } + + return true; + }; + + foreach ($consumers as $queueName => $consumer) { + /* @var Consumer $consumer */ + + $preSubscribe = new PreSubscribe( + $this->interopContext, + $this->boundProcessors[$queueName]->getProcessor(), + $consumer, + $this->logger + ); + + $extension->onPreSubscribe($preSubscribe); + + $subscriptionConsumer->subscribe($consumer, $callback); } + $cycle = 1; while (true) { - try { - if ($this->psrContext instanceof AmqpContext) { - $this->extension->onBeforeReceive($context); - - if ($context->isExecutionInterrupted()) { - throw new ConsumptionInterruptedException(); - } - - $this->psrContext->consume($this->receiveTimeout); - - usleep($this->idleTimeout * 1000); - $this->extension->onIdle($context); - } else { - /** @var PsrQueue $queue */ - foreach ($this->boundProcessors as list($queue, $processor)) { - $consumer = $consumers[$queue->getQueueName()]; - - $context = new Context($this->psrContext); - $context->setLogger($this->logger); - $context->setPsrQueue($queue); - $context->setPsrConsumer($consumer); - $context->setPsrProcessor($processor); - - $this->doConsume($this->extension, $context); - } - } - } catch (ConsumptionInterruptedException $e) { - $this->logger->info(sprintf('Consuming interrupted')); + $receivedMessagesCount = 0; + $interruptExecution = false; - if ($this->psrContext instanceof AmqpContext) { - foreach ($consumers as $consumer) { - /* @var AmqpConsumer $consumer */ + $preConsume = new PreConsume($this->interopContext, $subscriptionConsumer, $this->logger, $cycle, $this->receiveTimeout, $startTime); + $extension->onPreConsume($preConsume); - $this->psrContext->unsubscribe($consumer); - } - } + if ($preConsume->isExecutionInterrupted()) { + $this->onEnd($extension, $startTime, $preConsume->getExitStatus(), $subscriptionConsumer); - $context->setExecutionInterrupted(true); + return; + } - $this->extension->onInterrupted($context); + $subscriptionConsumer->consume($this->receiveTimeout); - return; - } catch (\Exception $exception) { - $context->setExecutionInterrupted(true); - $context->setException($exception); + $postConsume = new PostConsume($this->interopContext, $subscriptionConsumer, $receivedMessagesCount, $cycle, $startTime, $this->logger); + $extension->onPostConsume($postConsume); - try { - $this->onInterruptionByException($this->extension, $context); - } catch (\Exception $e) { - // for some reason finally does not work here on php5.5 + if ($interruptExecution || $postConsume->isExecutionInterrupted()) { + $this->onEnd($extension, $startTime, $postConsume->getExitStatus(), $subscriptionConsumer); - throw $e; - } + return; } + + ++$cycle; } } /** - * @param ExtensionInterface $extension - * @param Context $context - * - * @throws ConsumptionInterruptedException - * - * @return bool + * @internal */ - private function doConsume(ExtensionInterface $extension, Context $context) + public function setFallbackSubscriptionConsumer(SubscriptionConsumer $fallbackSubscriptionConsumer): void { - $processor = $context->getPsrProcessor(); - $consumer = $context->getPsrConsumer(); - $this->logger = $context->getLogger(); - - if ($context->isExecutionInterrupted()) { - throw new ConsumptionInterruptedException(); - } - - $message = $context->getPsrMessage(); - if (false == $message) { - $this->extension->onBeforeReceive($context); + $this->fallbackSubscriptionConsumer = $fallbackSubscriptionConsumer; + } - if ($message = $consumer->receive($this->receiveTimeout)) { - $context->setPsrMessage($message); - } - } + private function onEnd(ExtensionInterface $extension, int $startTime, ?int $exitStatus = null, ?SubscriptionConsumer $subscriptionConsumer = null): void + { + $endTime = (int) (microtime(true) * 1000); - if ($message) { - $this->processMessage($consumer, $processor, $message, $context); - } else { - usleep($this->idleTimeout * 1000); - $this->extension->onIdle($context); - } + $endContext = new End($this->interopContext, $startTime, $endTime, $this->logger, $exitStatus); + $extension->onEnd($endContext); - if ($context->isExecutionInterrupted()) { - throw new ConsumptionInterruptedException(); + if ($subscriptionConsumer) { + $subscriptionConsumer->unsubscribeAll(); } } /** - * @param ExtensionInterface $extension - * @param Context $context + * The logic is similar to one in Symfony's ExceptionListener::onKernelException(). * - * @throws \Exception + * https://github.com/symfony/symfony/blob/cbe289517470eeea27162fd2d523eb29c95f775f/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php#L77 */ - private function onInterruptionByException(ExtensionInterface $extension, Context $context) + private function onProcessorException(ExtensionInterface $extension, Consumer $consumer, Message $message, \Throwable $exception, int $receivedAt) { - $this->logger = $context->getLogger(); - $this->logger->error(sprintf('Consuming interrupted by exception')); - - $exception = $context->getException(); + $processorException = new ProcessorException($this->interopContext, $consumer, $message, $exception, $receivedAt, $this->logger); try { - $this->extension->onInterrupted($context); + $extension->onProcessorException($processorException); + + $result = $processorException->getResult(); + if (null === $result) { + throw $exception; + } + + return $result; } catch (\Exception $e) { - // logic is similar to one in Symfony's ExceptionListener::onKernelException - $this->logger->error(sprintf( - 'Exception thrown when handling an exception (%s: %s at %s line %s)', - get_class($e), - $e->getMessage(), - $e->getFile(), - $e->getLine() - )); - - $wrapper = $e; - while ($prev = $wrapper->getPrevious()) { + $prev = $e; + do { if ($exception === $wrapper = $prev) { throw $e; } - } + } while ($prev = $wrapper->getPrevious()); - $prev = new \ReflectionProperty('Exception', 'previous'); + $prev = new \ReflectionProperty($wrapper instanceof \Exception ? \Exception::class : \Error::class, 'previous'); $prev->setAccessible(true); $prev->setValue($wrapper, $exception); throw $e; } - - throw $exception; - } - - private function processMessage(PsrConsumer $consumer, PsrProcessor $processor, PsrMessage $message, Context $context) - { - $this->logger->info('Message received from the queue: '.$context->getPsrQueue()->getQueueName()); - $this->logger->debug('Headers: {headers}', ['headers' => new VarExport($message->getHeaders())]); - $this->logger->debug('Properties: {properties}', ['properties' => new VarExport($message->getProperties())]); - $this->logger->debug('Payload: {payload}', ['payload' => new VarExport($message->getBody())]); - - $this->extension->onPreReceived($context); - if (!$context->getResult()) { - $result = $processor->process($message, $this->psrContext); - $context->setResult($result); - } - - $this->extension->onResult($context); - - switch ($context->getResult()) { - case Result::ACK: - $consumer->acknowledge($message); - break; - case Result::REJECT: - $consumer->reject($message, false); - break; - case Result::REQUEUE: - $consumer->reject($message, true); - break; - default: - throw new \LogicException(sprintf('Status is not supported: %s', $context->getResult())); - } - - $this->logger->info(sprintf('Message processed: %s', $context->getResult())); - - $this->extension->onPostReceived($context); } } diff --git a/Consumption/QueueConsumerInterface.php b/Consumption/QueueConsumerInterface.php new file mode 100644 index 0000000..ee25652 --- /dev/null +++ b/Consumption/QueueConsumerInterface.php @@ -0,0 +1,40 @@ +status = (string) $status; @@ -72,17 +70,14 @@ public function getReason() } /** - * @return PsrMessage|null + * @return InteropMessage|null */ public function getReply() { return $this->reply; } - /** - * @param PsrMessage|null $reply - */ - public function setReply(PsrMessage $reply = null) + public function setReply(?InteropMessage $reply = null) { $this->reply = $reply; } @@ -94,7 +89,7 @@ public function setReply(PsrMessage $reply = null) */ public static function ack($reason = '') { - return new static(self::ACK, $reason); + return new self(self::ACK, $reason); } /** @@ -104,7 +99,7 @@ public static function ack($reason = '') */ public static function reject($reason) { - return new static(self::REJECT, $reason); + return new self(self::REJECT, $reason); } /** @@ -114,21 +109,20 @@ public static function reject($reason) */ public static function requeue($reason = '') { - return new static(self::REQUEUE, $reason); + return new self(self::REQUEUE, $reason); } /** - * @param PsrMessage $replyMessage * @param string $status * @param string|null $reason * * @return static */ - public static function reply(PsrMessage $replyMessage, $status = self::ACK, $reason = null) + public static function reply(InteropMessage $replyMessage, $status = self::ACK, $reason = null) { $status = null === $status ? self::ACK : $status; - $result = new static($status, $reason); + $result = new self($status, $reason); $result->setReply($replyMessage); return $result; diff --git a/Consumption/StartExtensionInterface.php b/Consumption/StartExtensionInterface.php new file mode 100644 index 0000000..9857106 --- /dev/null +++ b/Consumption/StartExtensionInterface.php @@ -0,0 +1,13 @@ +services = $services; + } + + public function get($id) + { + if (false == $this->has($id)) { + throw new NotFoundException(sprintf('The service "%s" not found.', $id)); + } + + return $this->services[$id]; + } + + public function has(string $id): bool + { + return array_key_exists($id, $this->services); + } +} diff --git a/Container/NotFoundException.php b/Container/NotFoundException.php new file mode 100644 index 0000000..fcc3386 --- /dev/null +++ b/Container/NotFoundException.php @@ -0,0 +1,9 @@ +doctrine = $doctrine; + $this->fallbackFactory = $fallbackFactory; + } + + public function create($config): ConnectionFactory + { + if (is_string($config)) { + $config = ['dsn' => $config]; + } + + if (false == is_array($config)) { + throw new \InvalidArgumentException('The config must be either array or DSN string.'); + } + + if (false == array_key_exists('dsn', $config)) { + throw new \InvalidArgumentException('The config must have dsn key set.'); + } + + $dsn = Dsn::parseFirst($config['dsn']); + + if ('doctrine' === $dsn->getScheme()) { + $config = $dsn->getQuery(); + $config['connection_name'] = $dsn->getHost(); + + return new ManagerRegistryConnectionFactory($this->doctrine, $config); + } + + return $this->fallbackFactory->create($config); + } +} diff --git a/Doctrine/DoctrineDriverFactory.php b/Doctrine/DoctrineDriverFactory.php new file mode 100644 index 0000000..aab6489 --- /dev/null +++ b/Doctrine/DoctrineDriverFactory.php @@ -0,0 +1,41 @@ +fallbackFactory = $fallbackFactory; + } + + public function create(ConnectionFactory $factory, Config $config, RouteCollection $collection): DriverInterface + { + $dsn = $config->getTransportOption('dsn'); + + if (empty($dsn)) { + throw new \LogicException('This driver factory relies on dsn option from transport config. The option is empty or not set.'); + } + + $dsn = Dsn::parseFirst($dsn); + + if ('doctrine' === $dsn->getScheme()) { + return new DbalDriver($factory->createContext(), $config, $collection); + } + + return $this->fallbackFactory->create($factory, $config, $collection); + } +} diff --git a/Doctrine/DoctrineSchemaCompilerPass.php b/Doctrine/DoctrineSchemaCompilerPass.php new file mode 100644 index 0000000..0eb3784 --- /dev/null +++ b/Doctrine/DoctrineSchemaCompilerPass.php @@ -0,0 +1,39 @@ +hasDefinition('doctrine')) { + return; + } + + foreach ($container->getParameter('enqueue.transports') as $name) { + $diUtils = DiUtils::create(TransportFactory::MODULE, $name); + + $container->register($diUtils->format('connection_factory_factory.outer'), DoctrineConnectionFactoryFactory::class) + ->setDecoratedService($diUtils->format('connection_factory_factory'), $diUtils->format('connection_factory_factory.inner')) + ->addArgument(new Reference('doctrine')) + ->addArgument(new Reference($diUtils->format('connection_factory_factory.inner'))) + ; + } + + foreach ($container->getParameter('enqueue.clients') as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + + $container->register($diUtils->format('driver_factory.outer'), DoctrineDriverFactory::class) + ->setDecoratedService($diUtils->format('driver_factory'), $diUtils->format('driver_factory.inner')) + ->addArgument(new Reference($diUtils->format('driver_factory.inner'))) + ; + } + } +} diff --git a/ProcessorRegistryInterface.php b/ProcessorRegistryInterface.php new file mode 100644 index 0000000..5306c30 --- /dev/null +++ b/ProcessorRegistryInterface.php @@ -0,0 +1,12 @@ +Supporting Enqueue + +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Message Queue. [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/enqueue.png?branch=master)](https://travis-ci.org/php-enqueue/enqueue) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/enqueue/ci.yml?branch=master)](https://github.com/php-enqueue/enqueue/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/enqueue/d/total.png)](https://packagist.org/packages/enqueue/enqueue) [![Latest Stable Version](https://poser.pugx.org/enqueue/enqueue/version.png)](https://packagist.org/packages/enqueue/enqueue) - -It contains advanced features build on top of a transport component. + +It contains advanced features build on top of a transport component. Client component kind of plug and play things or consumption component that simplify message processing a lot. -Read more about it in documentation. +Read more about it in documentation. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/Resources.php b/Resources.php new file mode 100644 index 0000000..4c50000 --- /dev/null +++ b/Resources.php @@ -0,0 +1,211 @@ + [ + * schemes => [schemes strings], + * package => package name, + * ]. + * + * @var array + */ + private static $knownConnections; + + private function __construct() + { + } + + public static function getAvailableConnections(): array + { + $map = self::getKnownConnections(); + + $availableMap = []; + foreach ($map as $connectionClass => $item) { + if (\class_exists($connectionClass)) { + $availableMap[$connectionClass] = $item; + } + } + + return $availableMap; + } + + public static function getKnownSchemes(): array + { + $map = self::getKnownConnections(); + + $schemes = []; + foreach ($map as $connectionClass => $item) { + foreach ($item['schemes'] as $scheme) { + $schemes[$scheme] = $connectionClass; + } + } + + return $schemes; + } + + public static function getAvailableSchemes(): array + { + $map = self::getAvailableConnections(); + + $schemes = []; + foreach ($map as $connectionClass => $item) { + foreach ($item['schemes'] as $scheme) { + $schemes[$scheme] = $connectionClass; + } + } + + return $schemes; + } + + public static function getKnownConnections(): array + { + if (null === self::$knownConnections) { + $map = []; + + $map[FsConnectionFactory::class] = [ + 'schemes' => ['file'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/fs', + ]; + $map[AmqpBunnyConnectionFactory::class] = [ + 'schemes' => ['amqp'], + 'supportedSchemeExtensions' => ['bunny'], + 'package' => 'enqueue/amqp-bunny', + ]; + $map[AmqpExtConnectionFactory::class] = [ + 'schemes' => ['amqp', 'amqps'], + 'supportedSchemeExtensions' => ['ext'], + 'package' => 'enqueue/amqp-ext', + ]; + $map[AmqpLibConnectionFactory::class] = [ + 'schemes' => ['amqp', 'amqps'], + 'supportedSchemeExtensions' => ['lib'], + 'package' => 'enqueue/amqp-lib', + ]; + + $map[DbalConnectionFactory::class] = [ + 'schemes' => [ + 'db2', + 'ibm-db2', + 'mssql', + 'sqlsrv', + 'mysql', + 'mysql2', + 'mysql', + 'pgsql', + 'postgres', + 'sqlite', + 'sqlite3', + 'sqlite', + ], + 'supportedSchemeExtensions' => ['pdo'], + 'package' => 'enqueue/dbal', + ]; + + $map[NullConnectionFactory::class] = [ + 'schemes' => ['null'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/null', + ]; + $map[GearmanConnectionFactory::class] = [ + 'schemes' => ['gearman'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/gearman', + ]; + $map[PheanstalkConnectionFactory::class] = [ + 'schemes' => ['beanstalk'], + 'supportedSchemeExtensions' => ['pheanstalk'], + 'package' => 'enqueue/pheanstalk', + ]; + $map[RdKafkaConnectionFactory::class] = [ + 'schemes' => ['kafka', 'rdkafka'], + 'supportedSchemeExtensions' => ['rdkafka'], + 'package' => 'enqueue/rdkafka', + ]; + $map[RedisConnectionFactory::class] = [ + 'schemes' => ['redis', 'rediss'], + 'supportedSchemeExtensions' => ['predis', 'phpredis'], + 'package' => 'enqueue/redis', + ]; + $map[StompConnectionFactory::class] = [ + 'schemes' => ['stomp'], + 'supportedSchemeExtensions' => ['rabbitmq'], + 'package' => 'enqueue/stomp', ]; + $map[SqsConnectionFactory::class] = [ + 'schemes' => ['sqs'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/sqs', ]; + $map[SnsConnectionFactory::class] = [ + 'schemes' => ['sns'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/sns', ]; + $map[SnsQsConnectionFactory::class] = [ + 'schemes' => ['snsqs'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/snsqs', ]; + $map[GpsConnectionFactory::class] = [ + 'schemes' => ['gps'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/gps', ]; + $map[MongodbConnectionFactory::class] = [ + 'schemes' => ['mongodb'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/mongodb', + ]; + $map[WampConnectionFactory::class] = [ + 'schemes' => ['wamp', 'ws'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/wamp', + ]; + + self::$knownConnections = $map; + } + + return self::$knownConnections; + } + + public static function addConnection(string $connectionFactoryClass, array $schemes, array $extensions, string $package): void + { + if (\class_exists($connectionFactoryClass)) { + if (false == (new \ReflectionClass($connectionFactoryClass))->implementsInterface(ConnectionFactory::class)) { + throw new \InvalidArgumentException(\sprintf('The connection factory class "%s" must implement "%s" interface.', $connectionFactoryClass, ConnectionFactory::class)); + } + } + + if (empty($schemes)) { + throw new \InvalidArgumentException('Schemes could not be empty.'); + } + if (empty($package)) { + throw new \InvalidArgumentException('Package name could not be empty.'); + } + + self::getKnownConnections(); + self::$knownConnections[$connectionFactoryClass] = [ + 'schemes' => $schemes, + 'supportedSchemeExtensions' => $extensions, + 'package' => $package, + ]; + } +} diff --git a/Router/Recipient.php b/Router/Recipient.php index dc0ab42..d2f668f 100644 --- a/Router/Recipient.php +++ b/Router/Recipient.php @@ -2,33 +2,29 @@ namespace Enqueue\Router; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrMessage; +use Interop\Queue\Destination; +use Interop\Queue\Message as InteropMessage; class Recipient { /** - * @var PsrDestination + * @var Destination */ private $destination; /** - * @var PsrMessage + * @var InteropMessage */ private $message; - /** - * @param PsrDestination $destination - * @param PsrMessage $message - */ - public function __construct(PsrDestination $destination, PsrMessage $message) + public function __construct(Destination $destination, InteropMessage $message) { $this->destination = $destination; $this->message = $message; } /** - * @return PsrDestination + * @return Destination */ public function getDestination() { @@ -36,7 +32,7 @@ public function getDestination() } /** - * @return PsrMessage + * @return InteropMessage */ public function getMessage() { diff --git a/Router/RecipientListRouterInterface.php b/Router/RecipientListRouterInterface.php index 78a354c..6bb950f 100644 --- a/Router/RecipientListRouterInterface.php +++ b/Router/RecipientListRouterInterface.php @@ -2,14 +2,12 @@ namespace Enqueue\Router; -use Interop\Queue\PsrMessage; +use Interop\Queue\Message as InteropMessage; interface RecipientListRouterInterface { /** - * @param PsrMessage $message - * * @return \Traversable|Recipient[] */ - public function route(PsrMessage $message); + public function route(InteropMessage $message); } diff --git a/Router/RouteRecipientListProcessor.php b/Router/RouteRecipientListProcessor.php index 59df355..22488e3 100644 --- a/Router/RouteRecipientListProcessor.php +++ b/Router/RouteRecipientListProcessor.php @@ -2,29 +2,23 @@ namespace Enqueue\Router; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message as InteropMessage; +use Interop\Queue\Processor; -class RouteRecipientListProcessor implements PsrProcessor +class RouteRecipientListProcessor implements Processor { /** * @var RecipientListRouterInterface */ private $router; - /** - * @param RecipientListRouterInterface $router - */ public function __construct(RecipientListRouterInterface $router) { $this->router = $router; } - /** - * {@inheritdoc} - */ - public function process(PsrMessage $message, PsrContext $context) + public function process(InteropMessage $message, Context $context) { $producer = $context->createProducer(); foreach ($this->router->route($message) as $recipient) { diff --git a/Rpc/Promise.php b/Rpc/Promise.php index 91e0aaa..01b47e1 100644 --- a/Rpc/Promise.php +++ b/Rpc/Promise.php @@ -2,7 +2,7 @@ namespace Enqueue\Rpc; -use Interop\Queue\PsrMessage; +use Interop\Queue\Message as InteropMessage; class Promise { @@ -27,15 +27,10 @@ class Promise private $deleteReplyQueue; /** - * @var PsrMessage + * @var InteropMessage */ private $message; - /** - * @param \Closure $receiveCallback - * @param \Closure $receiveNoWaitCallback - * @param \Closure $finallyCallback - */ public function __construct(\Closure $receiveCallback, \Closure $receiveNoWaitCallback, \Closure $finallyCallback) { $this->receiveCallback = $receiveCallback; @@ -52,7 +47,7 @@ public function __construct(\Closure $receiveCallback, \Closure $receiveNoWaitCa * * @throws TimeoutException if the wait timeout is reached * - * @return PsrMessage + * @return InteropMessage */ public function receive($timeout = null) { @@ -72,7 +67,7 @@ public function receive($timeout = null) /** * Non blocking function. Returns message or null. * - * @return PsrMessage|null + * @return InteropMessage|null */ public function receiveNoWait() { @@ -106,18 +101,16 @@ public function isDeleteReplyQueue() } /** - * @param \Closure $cb - * @param array $args + * @param array $args * - * @return PsrMessage + * @return InteropMessage */ private function doReceive(\Closure $cb, ...$args) { $message = call_user_func_array($cb, $args); - if (null !== $message && false == $message instanceof PsrMessage) { - throw new \RuntimeException(sprintf( - 'Expected "%s" but got: "%s"', PsrMessage::class, is_object($message) ? get_class($message) : gettype($message))); + if (null !== $message && false == $message instanceof InteropMessage) { + throw new \RuntimeException(sprintf('Expected "%s" but got: "%s"', InteropMessage::class, is_object($message) ? $message::class : gettype($message))); } return $message; diff --git a/Rpc/RpcClient.php b/Rpc/RpcClient.php index d429ac9..bd3d7ce 100644 --- a/Rpc/RpcClient.php +++ b/Rpc/RpcClient.php @@ -3,14 +3,14 @@ namespace Enqueue\Rpc; use Enqueue\Util\UUID; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrMessage; +use Interop\Queue\Context; +use Interop\Queue\Destination; +use Interop\Queue\Message as InteropMessage; class RpcClient { /** - * @var PsrContext + * @var Context */ private $context; @@ -19,38 +19,30 @@ class RpcClient */ private $rpcFactory; - /** - * @param PsrContext $context - * @param RpcFactory $promiseFactory - */ - public function __construct(PsrContext $context, RpcFactory $promiseFactory = null) + public function __construct(Context $context, ?RpcFactory $promiseFactory = null) { $this->context = $context; $this->rpcFactory = $promiseFactory ?: new RpcFactory($context); } /** - * @param PsrDestination $destination - * @param PsrMessage $message - * @param int $timeout + * @param int $timeout * * @throws TimeoutException if the wait timeout is reached * - * @return PsrMessage + * @return InteropMessage */ - public function call(PsrDestination $destination, PsrMessage $message, $timeout) + public function call(Destination $destination, InteropMessage $message, $timeout) { return $this->callAsync($destination, $message, $timeout)->receive(); } /** - * @param PsrDestination $destination - * @param PsrMessage $message - * @param int $timeout + * @param int $timeout * * @return Promise */ - public function callAsync(PsrDestination $destination, PsrMessage $message, $timeout) + public function callAsync(Destination $destination, InteropMessage $message, $timeout) { if ($timeout < 1) { throw new \InvalidArgumentException(sprintf('Timeout must be positive not zero integer. Got %s', $timeout)); diff --git a/Rpc/RpcFactory.php b/Rpc/RpcFactory.php index 195fd95..9100bab 100644 --- a/Rpc/RpcFactory.php +++ b/Rpc/RpcFactory.php @@ -2,19 +2,16 @@ namespace Enqueue\Rpc; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; class RpcFactory { /** - * @var PsrContext + * @var Context */ private $context; - /** - * @param PsrContext $context - */ - public function __construct(PsrContext $context) + public function __construct(Context $context) { $this->context = $context; } diff --git a/Rpc/TimeoutException.php b/Rpc/TimeoutException.php index a0b0655..a7f68b9 100644 --- a/Rpc/TimeoutException.php +++ b/Rpc/TimeoutException.php @@ -12,6 +12,6 @@ class TimeoutException extends \LogicException */ public static function create($timeout, $correlationId) { - return new static(sprintf('Rpc call timeout is reached without receiving a reply message. Timeout: %s, CorrelationId: %s', $timeout, $correlationId)); + return new self(sprintf('Rpc call timeout is reached without receiving a reply message. Timeout: %s, CorrelationId: %s', $timeout, $correlationId)); } } diff --git a/Symfony/AmqpTransportFactory.php b/Symfony/AmqpTransportFactory.php deleted file mode 100644 index e69bf44..0000000 --- a/Symfony/AmqpTransportFactory.php +++ /dev/null @@ -1,253 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $transportsMap = static::getAvailableTransportsMap(); - - $builder - ->beforeNormalization() - ->ifTrue(function ($v) { - return empty($v); - }) - ->then(function ($v) { - return ['dsn' => 'amqp:']; - }) - ->ifString() - ->then(function ($v) { - return ['dsn' => $v]; - }) - ->end() - ->children() - ->scalarNode('driver') - ->validate() - ->always(function ($v) use ($transportsMap) { - $drivers = array_keys($transportsMap); - if (empty($transportsMap)) { - throw new \InvalidArgumentException('There is no amqp driver available. Please consider installing one of the packages: enqueue/amqp-ext, enqueue/amqp-lib, enqueue/amqp-bunny.'); - } - - if ($v && false == in_array($v, $drivers, true)) { - throw new \InvalidArgumentException(sprintf('Unexpected driver given "%s". Available are "%s"', $v, implode('", "', $drivers))); - } - - return $v; - }) - ->end() - ->end() - ->scalarNode('dsn') - ->info('The connection to AMQP broker set as a string. Other parameters could be used as defaults') - ->end() - ->scalarNode('host') - ->info('The host to connect too. Note: Max 1024 characters') - ->end() - ->scalarNode('port') - ->info('Port on the host.') - ->end() - ->scalarNode('user') - ->info('The user name to use. Note: Max 128 characters.') - ->end() - ->scalarNode('pass') - ->info('Password. Note: Max 128 characters.') - ->end() - ->scalarNode('vhost') - ->info('The virtual host on the host. Note: Max 128 characters.') - ->end() - ->floatNode('connection_timeout') - ->min(0) - ->info('Connection timeout. Note: 0 or greater seconds. May be fractional.') - ->end() - ->floatNode('read_timeout') - ->min(0) - ->info('Timeout in for income activity. Note: 0 or greater seconds. May be fractional.') - ->end() - ->floatNode('write_timeout') - ->min(0) - ->info('Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional.') - ->end() - ->floatNode('heartbeat') - ->min(0) - ->info('How often to send heartbeat. 0 means off.') - ->end() - ->booleanNode('persisted')->end() - ->booleanNode('lazy')->end() - ->enumNode('receive_method') - ->values(['basic_get', 'basic_consume']) - ->info('The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher') - ->end() - ->floatNode('qos_prefetch_size') - ->min(0) - ->info('The server will send a message in advance if it is equal to or smaller in size than the available prefetch size. May be set to zero, meaning "no specific limit"') - ->end() - ->floatNode('qos_prefetch_count') - ->min(0) - ->info('Specifies a prefetch window in terms of whole messages') - ->end() - ->booleanNode('qos_global') - ->info('If "false" the QoS settings apply to the current channel only. If this field is "true", they are applied to the entire connection.') - ->end() - ->variableNode('driver_options') - ->info('The options that are specific to the amqp transport you chose. For example amqp+lib have insist, keepalive, stream options. amqp+bunny has tcp_nodelay extra option.') - ->end() - ->booleanNode('ssl_on') - ->info('Should be true if you want to use secure connections. False by default') - ->end() - ->booleanNode('ssl_verify') - ->info('This option determines whether ssl client verifies that the server cert is for the server it is known as. True by default.') - ->end() - ->scalarNode('ssl_cacert') - ->info('Location of Certificate Authority file on local filesystem which should be used with the verify_peer context option to authenticate the identity of the remote peer. A string.') - ->end() - ->scalarNode('ssl_cert') - ->info('Path to local certificate file on filesystem. It must be a PEM encoded file which contains your certificate and private key. A string') - ->end() - ->scalarNode('ssl_key') - ->info('Path to local private key file on filesystem in case of separate files for certificate (local_cert) and private key. A string.') - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - if (array_key_exists('driver_options', $config) && is_array($config['driver_options'])) { - $driverOptions = $config['driver_options']; - unset($config['driver_options']); - - $config = array_replace($driverOptions, $config); - } - - $factory = new Definition(AmqpConnectionFactory::class); - $factory->setFactory([self::class, 'createConnectionFactoryFactory']); - $factory->setArguments([$config]); - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - $container->setDefinition($factoryId, $factory); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $context = new Definition(AmqpContext::class); - $context->setPublic(true); - $context->setFactory([new Reference($factoryId), 'createContext']); - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - $container->setDefinition($contextId, $context); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(AmqpDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return $this->name; - } - - public static function createConnectionFactoryFactory(array $config) - { - if (false == empty($config['driver'])) { - $transportsMap = static::getAvailableTransportsMap(); - - if (false == array_key_exists($config['driver'], $transportsMap)) { - throw new \InvalidArgumentException(sprintf('Unexpected driver given "invalidDriver". Available are "%s"', implode('", "', array_keys($transportsMap)))); - } - - $connectionFactoryClass = $transportsMap[$config['driver']]; - - unset($config['driver']); - - return new $connectionFactoryClass($config); - } - - $dsn = array_key_exists('dsn', $config) ? $config['dsn'] : 'amqp:'; - $factory = dsn_to_connection_factory($dsn); - - if (false == $factory instanceof AmqpConnectionFactory) { - throw new \LogicException(sprintf('Factory must be instance of "%s" but got "%s"', AmqpConnectionFactory::class, get_class($factory))); - } - - $factoryClass = get_class($factory); - - return new $factoryClass($config); - } - - /** - * @return string[] - */ - private static function getAvailableTransportsMap() - { - $map = []; - if (class_exists(AmqpExtConnectionFactory::class)) { - $map['ext'] = AmqpExtConnectionFactory::class; - } - if (class_exists(AmqpLibConnectionFactory::class)) { - $map['lib'] = AmqpLibConnectionFactory::class; - } - if (class_exists(AmqpBunnyConnectionFactory::class)) { - $map['bunny'] = AmqpBunnyConnectionFactory::class; - } - - return $map; - } -} diff --git a/Symfony/Client/ConsumeCommand.php b/Symfony/Client/ConsumeCommand.php new file mode 100644 index 0000000..94b56ad --- /dev/null +++ b/Symfony/Client/ConsumeCommand.php @@ -0,0 +1,181 @@ +container = $container; + $this->defaultClient = $defaultClient; + $this->queueConsumerIdPattern = $queueConsumerIdPattern; + $this->driverIdPattern = $driverIdPattern; + $this->processorIdPattern = $processorIdPatter; + + parent::__construct(); + } + + protected function configure(): void + { + $this->configureLimitsExtensions(); + $this->configureSetupBrokerExtension(); + $this->configureQueueConsumerOptions(); + $this->configureLoggerExtension(); + + $this + ->setAliases(['enq:c']) + ->setDescription('A client\'s worker that processes messages. '. + 'By default it connects to default queue. '. + 'It select an appropriate message processor based on a message headers') + ->addArgument('client-queue-names', InputArgument::IS_ARRAY, 'Queues to consume messages from') + ->addOption('skip', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Queues to skip consumption of messages from', []) + ->addOption('client', 'c', InputOption::VALUE_OPTIONAL, 'The client to consume messages from.', $this->defaultClient) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $client = $input->getOption('client'); + + try { + $consumer = $this->getQueueConsumer($client); + } catch (NotFoundExceptionInterface $e) { + throw new \LogicException(sprintf('Client "%s" is not supported.', $client), previous: $e); + } + + $driver = $this->getDriver($client); + $processor = $this->getProcessor($client); + + $this->setQueueConsumerOptions($consumer, $input); + + $allQueues[$driver->getConfig()->getDefaultQueue()] = true; + $allQueues[$driver->getConfig()->getRouterQueue()] = true; + foreach ($driver->getRouteCollection()->all() as $route) { + if (false == $route->getQueue()) { + continue; + } + if ($route->isProcessorExternal()) { + continue; + } + + $allQueues[$route->getQueue()] = $route->isPrefixQueue(); + } + + $selectedQueues = $input->getArgument('client-queue-names'); + if (empty($selectedQueues)) { + $queues = $allQueues; + } else { + $queues = []; + foreach ($selectedQueues as $queue) { + if (false == array_key_exists($queue, $allQueues)) { + throw new \LogicException(sprintf('There is no such queue "%s". Available are "%s"', $queue, implode('", "', array_keys($allQueues)))); + } + + $queues[$queue] = $allQueues[$queue]; + } + } + + foreach ($input->getOption('skip') as $skipQueue) { + unset($queues[$skipQueue]); + } + + foreach ($queues as $queue => $prefix) { + $queue = $driver->createQueue($queue, $prefix); + $consumer->bind($queue, $processor); + } + + $runtimeExtensionChain = $this->getRuntimeExtensions($input, $output); + $exitStatusExtension = new ExitStatusExtension(); + + $consumer->consume(new ChainExtension([$runtimeExtensionChain, $exitStatusExtension])); + + return $exitStatusExtension->getExitStatus() ?? 0; + } + + protected function getRuntimeExtensions(InputInterface $input, OutputInterface $output): ExtensionInterface + { + $extensions = []; + $extensions = array_merge($extensions, $this->getLimitsExtensions($input, $output)); + + $driver = $this->getDriver($input->getOption('client')); + + if ($setupBrokerExtension = $this->getSetupBrokerExtension($input, $driver)) { + $extensions[] = $setupBrokerExtension; + } + + if ($loggerExtension = $this->getLoggerExtension($input, $output)) { + array_unshift($extensions, $loggerExtension); + } + + return new ChainExtension($extensions); + } + + private function getDriver(string $name): DriverInterface + { + return $this->container->get(sprintf($this->driverIdPattern, $name)); + } + + private function getQueueConsumer(string $name): QueueConsumerInterface + { + return $this->container->get(sprintf($this->queueConsumerIdPattern, $name)); + } + + private function getProcessor(string $name): Processor + { + return $this->container->get(sprintf($this->processorIdPattern, $name)); + } +} diff --git a/Symfony/Client/ConsumeMessagesCommand.php b/Symfony/Client/ConsumeMessagesCommand.php deleted file mode 100644 index a3817b3..0000000 --- a/Symfony/Client/ConsumeMessagesCommand.php +++ /dev/null @@ -1,135 +0,0 @@ -consumer = $consumer; - $this->processor = $processor; - $this->queueMetaRegistry = $queueMetaRegistry; - $this->driver = $driver; - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this->configureLimitsExtensions(); - $this->configureSetupBrokerExtension(); - $this->configureQueueConsumerOptions(); - - $this - ->setName('enqueue:consume') - ->setAliases(['enq:c']) - ->setDescription('A client\'s worker that processes messages. '. - 'By default it connects to default queue. '. - 'It select an appropriate message processor based on a message headers') - ->addArgument('client-queue-names', InputArgument::IS_ARRAY, 'Queues to consume messages from') - ->addOption('skip', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Queues to skip consumption of messages from', []) - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->setQueueConsumerOptions($this->consumer, $input); - - $queueMetas = []; - if ($clientQueueNames = $input->getArgument('client-queue-names')) { - foreach ($clientQueueNames as $clientQueueName) { - $queueMetas[] = $this->queueMetaRegistry->getQueueMeta($clientQueueName); - } - } else { - /** @var QueueMeta[] $queueMetas */ - $queueMetas = iterator_to_array($this->queueMetaRegistry->getQueuesMeta()); - - foreach ($queueMetas as $index => $queueMeta) { - if (in_array($queueMeta->getClientName(), $input->getOption('skip'), true)) { - unset($queueMetas[$index]); - } - } - } - - foreach ($queueMetas as $queueMeta) { - $queue = $this->driver->createQueue($queueMeta->getClientName()); - $this->consumer->bind($queue, $this->processor); - } - - $this->consumer->consume($this->getRuntimeExtensions($input, $output)); - } - - /** - * @param InputInterface $input - * @param OutputInterface $output - * - * @return ChainExtension - */ - protected function getRuntimeExtensions(InputInterface $input, OutputInterface $output) - { - $extensions = [new LoggerExtension(new ConsoleLogger($output))]; - $extensions = array_merge($extensions, $this->getLimitsExtensions($input, $output)); - - if ($setupBrokerExtension = $this->getSetupBrokerExtension($input, $this->driver)) { - $extensions[] = $setupBrokerExtension; - } - - return new ChainExtension($extensions); - } -} diff --git a/Symfony/Client/ContainerAwareProcessorRegistry.php b/Symfony/Client/ContainerAwareProcessorRegistry.php deleted file mode 100644 index b378eb4..0000000 --- a/Symfony/Client/ContainerAwareProcessorRegistry.php +++ /dev/null @@ -1,68 +0,0 @@ -processors = $processors; - } - - /** - * @param string $processorName - * @param string $serviceId - */ - public function set($processorName, $serviceId) - { - $this->processors[$processorName] = $serviceId; - } - - /** - * {@inheritdoc} - */ - public function get($processorName) - { - if (30300 > Kernel::VERSION_ID) { - // Symfony 3.2 and below make service identifiers lowercase, so we do the same. - // To be removed when dropping support for Symfony < 3.3. - $processorName = strtolower($processorName); - } - - if (false == isset($this->processors[$processorName])) { - throw new \LogicException(sprintf('Processor was not found. processorName: "%s"', $processorName)); - } - - if (null === $this->container) { - throw new \LogicException('Container was not set'); - } - - $processor = $this->container->get($this->processors[$processorName]); - - if (false == $processor instanceof PsrProcessor) { - throw new \LogicException(sprintf( - 'Invalid instance of message processor. expected: "%s", got: "%s"', - PsrProcessor::class, - is_object($processor) ? get_class($processor) : gettype($processor) - )); - } - - return $processor; - } -} diff --git a/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPass.php b/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPass.php new file mode 100644 index 0000000..577f159 --- /dev/null +++ b/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPass.php @@ -0,0 +1,107 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + + $routeCollectionId = $diUtils->format('route_collection'); + if (false == $container->hasDefinition($routeCollectionId)) { + throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId)); + } + + $collection = RouteCollection::fromArray($container->getDefinition($routeCollectionId)->getArgument(0)); + + $this->exclusiveCommandsCouldNotBeRunOnDefaultQueue($collection); + $this->exclusiveCommandProcessorMustBeSingleOnGivenQueue($collection); + $this->customQueueNamesUnique($collection); + $this->defaultQueueMustBePrefixed($collection); + } + } + + private function exclusiveCommandsCouldNotBeRunOnDefaultQueue(RouteCollection $collection): void + { + foreach ($collection->all() as $route) { + if ($route->isCommand() && $route->isProcessorExclusive() && false == $route->getQueue()) { + throw new \LogicException(sprintf('The command "%s" processor "%s" is exclusive but queue is not specified. Exclusive processors could not be run on a default queue.', $route->getSource(), $route->getProcessor())); + } + } + } + + private function exclusiveCommandProcessorMustBeSingleOnGivenQueue(RouteCollection $collection): void + { + $prefixedQueues = []; + $queues = []; + foreach ($collection->all() as $route) { + if (false == $route->isCommand()) { + continue; + } + if (false == $route->isProcessorExclusive()) { + continue; + } + + if ($route->isPrefixQueue()) { + if (array_key_exists($route->getQueue(), $prefixedQueues)) { + throw new \LogicException(sprintf('The command "%s" processor "%s" is exclusive. The queue "%s" already has another exclusive command processor "%s" bound to it.', $route->getSource(), $route->getProcessor(), $route->getQueue(), $prefixedQueues[$route->getQueue()])); + } + + $prefixedQueues[$route->getQueue()] = $route->getProcessor(); + } else { + if (array_key_exists($route->getQueue(), $queues)) { + throw new \LogicException(sprintf('The command "%s" processor "%s" is exclusive. The queue "%s" already has another exclusive command processor "%s" bound to it.', $route->getSource(), $route->getProcessor(), $route->getQueue(), $queues[$route->getQueue()])); + } + + $queues[$route->getQueue()] = $route->getProcessor(); + } + } + } + + private function defaultQueueMustBePrefixed(RouteCollection $collection): void + { + foreach ($collection->all() as $route) { + if (false == $route->getQueue() && false == $route->isPrefixQueue()) { + throw new \LogicException('The default queue must be prefixed.'); + } + } + } + + private function customQueueNamesUnique(RouteCollection $collection): void + { + $prefixedQueues = []; + $notPrefixedQueues = []; + + foreach ($collection->all() as $route) { + // default queue + $queueName = $route->getQueue(); + if (false == $queueName) { + return; + } + + $route->isPrefixQueue() ? + $prefixedQueues[$queueName] = $queueName : + $notPrefixedQueues[$queueName] = $queueName + ; + } + + foreach ($notPrefixedQueues as $queueName) { + if (array_key_exists($queueName, $prefixedQueues)) { + throw new \LogicException(sprintf('There are prefixed and not prefixed queue with the same name "%s". This is not allowed.', $queueName)); + } + } + } +} diff --git a/Symfony/Client/DependencyInjection/BuildClientExtensionsPass.php b/Symfony/Client/DependencyInjection/BuildClientExtensionsPass.php new file mode 100644 index 0000000..92124f2 --- /dev/null +++ b/Symfony/Client/DependencyInjection/BuildClientExtensionsPass.php @@ -0,0 +1,63 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + $defaultName = $container->getParameter('enqueue.default_client'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + + $extensionsId = $diUtils->format('client_extensions'); + if (false == $container->hasDefinition($extensionsId)) { + throw new \LogicException(sprintf('Service "%s" not found', $extensionsId)); + } + + $tags = array_merge( + $container->findTaggedServiceIds('enqueue.client_extension'), + $container->findTaggedServiceIds('enqueue.client.extension') // TODO BC + ); + + $groupByPriority = []; + foreach ($tags as $serviceId => $tagAttributes) { + foreach ($tagAttributes as $tagAttribute) { + $client = $tagAttribute['client'] ?? $defaultName; + + if ($client !== $name && 'all' !== $client) { + continue; + } + + $priority = (int) ($tagAttribute['priority'] ?? 0); + + $groupByPriority[$priority][] = new Reference($serviceId); + } + } + + krsort($groupByPriority, \SORT_NUMERIC); + + $flatExtensions = []; + foreach ($groupByPriority as $extension) { + $flatExtensions = array_merge($flatExtensions, $extension); + } + + $extensionsService = $container->getDefinition($extensionsId); + $extensionsService->replaceArgument(0, array_merge( + $extensionsService->getArgument(0), + $flatExtensions + )); + } + } +} diff --git a/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPass.php b/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPass.php new file mode 100644 index 0000000..4adc09e --- /dev/null +++ b/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPass.php @@ -0,0 +1,135 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + $defaultName = $container->getParameter('enqueue.default_client'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + $routeCollectionId = $diUtils->format('route_collection'); + if (false == $container->hasDefinition($routeCollectionId)) { + throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId)); + } + + $tag = 'enqueue.command_subscriber'; + $routeCollection = new RouteCollection([]); + foreach ($container->findTaggedServiceIds($tag) as $serviceId => $tagAttributes) { + $processorDefinition = $container->getDefinition($serviceId); + if ($processorDefinition->getFactory()) { + throw new \LogicException('The command subscriber tag could not be applied to a service created by factory.'); + } + + $processorClass = $processorDefinition->getClass() ?? $serviceId; + if (false == class_exists($processorClass)) { + throw new \LogicException(sprintf('The processor class "%s" could not be found.', $processorClass)); + } + + if (false == is_subclass_of($processorClass, CommandSubscriberInterface::class)) { + throw new \LogicException(sprintf('The processor must implement "%s" interface to be used with the tag "%s"', CommandSubscriberInterface::class, $tag)); + } + + foreach ($tagAttributes as $tagAttribute) { + $client = $tagAttribute['client'] ?? $defaultName; + + if ($client !== $name && 'all' !== $client) { + continue; + } + + /** @var CommandSubscriberInterface $processorClass */ + $commands = $processorClass::getSubscribedCommand(); + + if (empty($commands)) { + throw new \LogicException('Command subscriber must return something.'); + } + + if (is_string($commands)) { + $commands = [$commands]; + } + + if (!is_array($commands)) { + throw new \LogicException('Command subscriber configuration is invalid. Should be an array or string.'); + } + + // 0.8 command subscriber + if (isset($commands['processorName'])) { + @trigger_error('The command subscriber 0.8 syntax is deprecated since Enqueue 0.9.', \E_USER_DEPRECATED); + + $source = $commands['processorName']; + $processor = $params['processorName'] ?? $serviceId; + + $options = $commands; + unset( + $options['processorName'], + $options['queueName'], + $options['queueNameHardcoded'], + $options['exclusive'], + $options['topic'], + $options['source'], + $options['source_type'], + $options['processor'], + $options['options'] + ); + + $options['processor_service_id'] = $serviceId; + + if (isset($commands['queueName'])) { + $options['queue'] = $commands['queueName']; + } + + if (isset($commands['queueNameHardcoded']) && $commands['queueNameHardcoded']) { + $options['prefix_queue'] = false; + } + + $routeCollection->add(new Route($source, Route::COMMAND, $processor, $options)); + + continue; + } + + if (isset($commands['command'])) { + $commands = [$commands]; + } + + foreach ($commands as $key => $params) { + if (is_string($params)) { + $routeCollection->add(new Route($params, Route::COMMAND, $serviceId, ['processor_service_id' => $serviceId])); + } elseif (is_array($params)) { + $source = $params['command'] ?? null; + $processor = $params['processor'] ?? $serviceId; + unset($params['command'], $params['source'], $params['source_type'], $params['processor'], $params['options']); + $options = $params; + $options['processor_service_id'] = $serviceId; + + $routeCollection->add(new Route($source, Route::COMMAND, $processor, $options)); + } else { + throw new \LogicException(sprintf('Command subscriber configuration is invalid for "%s::getSubscribedCommand()". "%s"', $processorClass, json_encode($processorClass::getSubscribedCommand()))); + } + } + } + } + + $rawRoutes = $routeCollection->toArray(); + + $routeCollectionService = $container->getDefinition($routeCollectionId); + $routeCollectionService->replaceArgument(0, array_merge( + $routeCollectionService->getArgument(0), + $rawRoutes + )); + } + } +} diff --git a/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPass.php b/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPass.php new file mode 100644 index 0000000..274847c --- /dev/null +++ b/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPass.php @@ -0,0 +1,63 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + $defaultName = $container->getParameter('enqueue.default_client'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + + $extensionsId = $diUtils->format('consumption_extensions'); + if (false == $container->hasDefinition($extensionsId)) { + throw new \LogicException(sprintf('Service "%s" not found', $extensionsId)); + } + + $tags = array_merge( + $container->findTaggedServiceIds('enqueue.consumption_extension'), + $container->findTaggedServiceIds('enqueue.consumption.extension') // TODO BC + ); + + $groupByPriority = []; + foreach ($tags as $serviceId => $tagAttributes) { + foreach ($tagAttributes as $tagAttribute) { + $client = $tagAttribute['client'] ?? $defaultName; + + if ($client !== $name && 'all' !== $client) { + continue; + } + + $priority = (int) ($tagAttribute['priority'] ?? 0); + + $groupByPriority[$priority][] = new Reference($serviceId); + } + } + + krsort($groupByPriority, \SORT_NUMERIC); + + $flatExtensions = []; + foreach ($groupByPriority as $extension) { + $flatExtensions = array_merge($flatExtensions, $extension); + } + + $extensionsService = $container->getDefinition($extensionsId); + $extensionsService->replaceArgument(0, array_merge( + $extensionsService->getArgument(0), + $flatExtensions + )); + } + } +} diff --git a/Symfony/Client/DependencyInjection/BuildProcessorRegistryPass.php b/Symfony/Client/DependencyInjection/BuildProcessorRegistryPass.php new file mode 100644 index 0000000..3759dd2 --- /dev/null +++ b/Symfony/Client/DependencyInjection/BuildProcessorRegistryPass.php @@ -0,0 +1,57 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + + $processorRegistryId = $diUtils->format('processor_registry'); + if (false == $container->hasDefinition($processorRegistryId)) { + throw new \LogicException(sprintf('Service "%s" not found', $processorRegistryId)); + } + + $routeCollectionId = $diUtils->format('route_collection'); + if (false == $container->hasDefinition($routeCollectionId)) { + throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId)); + } + + $routerProcessorId = $diUtils->format('router_processor'); + if (false == $container->hasDefinition($routerProcessorId)) { + throw new \LogicException(sprintf('Service "%s" not found', $routerProcessorId)); + } + + $routeCollection = RouteCollection::fromArray($container->getDefinition($routeCollectionId)->getArgument(0)); + + $map = []; + foreach ($routeCollection->all() as $route) { + if (false == $processorServiceId = $route->getOption('processor_service_id')) { + throw new \LogicException('The route option "processor_service_id" is required'); + } + + $map[$route->getProcessor()] = new Reference($processorServiceId); + } + + $map[$diUtils->parameter('router_processor')] = new Reference($routerProcessorId); + + $registry = $container->getDefinition($processorRegistryId); + $registry->setArgument(0, ServiceLocatorTagPass::register($container, $map, $processorRegistryId)); + } + } +} diff --git a/Symfony/Client/DependencyInjection/BuildProcessorRoutesPass.php b/Symfony/Client/DependencyInjection/BuildProcessorRoutesPass.php new file mode 100644 index 0000000..e88cb1f --- /dev/null +++ b/Symfony/Client/DependencyInjection/BuildProcessorRoutesPass.php @@ -0,0 +1,77 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + $defaultName = $container->getParameter('enqueue.default_client'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + $routeCollectionId = $diUtils->format('route_collection'); + if (false == $container->hasDefinition($routeCollectionId)) { + throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId)); + } + + $tag = 'enqueue.processor'; + $routeCollection = new RouteCollection([]); + foreach ($container->findTaggedServiceIds($tag) as $serviceId => $tagAttributes) { + foreach ($tagAttributes as $tagAttribute) { + $client = $tagAttribute['client'] ?? $defaultName; + + if ($client !== $name && 'all' !== $client) { + continue; + } + + $topic = $tagAttribute['topic'] ?? null; + $command = $tagAttribute['command'] ?? null; + + if (false == $topic && false == $command) { + throw new \LogicException(sprintf('Either "topic" or "command" tag attribute must be set on service "%s". None is set.', $serviceId)); + } + if ($topic && $command) { + throw new \LogicException(sprintf('Either "topic" or "command" tag attribute must be set on service "%s". Both are set.', $serviceId)); + } + + $source = $command ?: $topic; + $sourceType = $command ? Route::COMMAND : Route::TOPIC; + $processor = $tagAttribute['processor'] ?? $serviceId; + + unset( + $tagAttribute['topic'], + $tagAttribute['command'], + $tagAttribute['source'], + $tagAttribute['source_type'], + $tagAttribute['processor'], + $tagAttribute['options'] + ); + $options = $tagAttribute; + $options['processor_service_id'] = $serviceId; + + $routeCollection->add(new Route($source, $sourceType, $processor, $options)); + } + } + + $rawRoutes = $routeCollection->toArray(); + + $routeCollectionService = $container->getDefinition($routeCollectionId); + $routeCollectionService->replaceArgument(0, array_merge( + $routeCollectionService->getArgument(0), + $rawRoutes + )); + } + } +} diff --git a/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPass.php b/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPass.php new file mode 100644 index 0000000..ef01e6f --- /dev/null +++ b/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPass.php @@ -0,0 +1,127 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + $defaultName = $container->getParameter('enqueue.default_client'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + $routeCollectionId = $diUtils->format('route_collection'); + if (false == $container->hasDefinition($routeCollectionId)) { + throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId)); + } + + $tag = 'enqueue.topic_subscriber'; + $routeCollection = new RouteCollection([]); + foreach ($container->findTaggedServiceIds($tag) as $serviceId => $tagAttributes) { + $processorDefinition = $container->getDefinition($serviceId); + if ($processorDefinition->getFactory()) { + throw new \LogicException('The topic subscriber tag could not be applied to a service created by factory.'); + } + + $processorClass = $processorDefinition->getClass() ?? $serviceId; + if (false == class_exists($processorClass)) { + throw new \LogicException(sprintf('The processor class "%s" could not be found.', $processorClass)); + } + + if (false == is_subclass_of($processorClass, TopicSubscriberInterface::class)) { + throw new \LogicException(sprintf('The processor must implement "%s" interface to be used with the tag "%s"', TopicSubscriberInterface::class, $tag)); + } + + foreach ($tagAttributes as $tagAttribute) { + $client = $tagAttribute['client'] ?? $defaultName; + + if ($client !== $name && 'all' !== $client) { + continue; + } + + /** @var TopicSubscriberInterface $processorClass */ + $topics = $processorClass::getSubscribedTopics(); + + if (empty($topics)) { + throw new \LogicException('Topic subscriber must return something.'); + } + + if (is_string($topics)) { + $topics = [$topics]; + } + + if (!is_array($topics)) { + throw new \LogicException('Topic subscriber configuration is invalid. Should be an array or string.'); + } + + foreach ($topics as $key => $params) { + if (is_string($params)) { + $routeCollection->add(new Route($params, Route::TOPIC, $serviceId, ['processor_service_id' => $serviceId])); + + // 0.8 topic subscriber + } elseif (is_array($params) && is_string($key)) { + @trigger_error('The topic subscriber 0.8 syntax is deprecated since Enqueue 0.9.', \E_USER_DEPRECATED); + + $source = $key; + $processor = $params['processorName'] ?? $serviceId; + + $options = $params; + unset( + $options['processorName'], + $options['queueName'], + $options['queueNameHardcoded'], + $options['topic'], + $options['source'], + $options['source_type'], + $options['processor'], + $options['options'] + ); + + $options['processor_service_id'] = $serviceId; + + if (isset($params['queueName'])) { + $options['queue'] = $params['queueName']; + } + + if (isset($params['queueNameHardcoded']) && $params['queueNameHardcoded']) { + $options['prefix_queue'] = false; + } + + $routeCollection->add(new Route($source, Route::TOPIC, $processor, $options)); + } elseif (is_array($params)) { + $source = $params['topic'] ?? null; + $processor = $params['processor'] ?? $serviceId; + unset($params['topic'], $params['source'], $params['source_type'], $params['processor'], $params['options']); + $options = $params; + $options['processor_service_id'] = $serviceId; + + $routeCollection->add(new Route($source, Route::TOPIC, $processor, $options)); + } else { + throw new \LogicException(sprintf('Topic subscriber configuration is invalid for "%s::getSubscribedTopics()". Got "%s"', $processorClass, json_encode($processorClass::getSubscribedTopics()))); + } + } + } + } + + $rawRoutes = $routeCollection->toArray(); + + $routeCollectionService = $container->getDefinition($routeCollectionId); + $routeCollectionService->replaceArgument(0, array_merge( + $routeCollectionService->getArgument(0), + $rawRoutes + )); + } + } +} diff --git a/Symfony/Client/DependencyInjection/ClientFactory.php b/Symfony/Client/DependencyInjection/ClientFactory.php new file mode 100644 index 0000000..be020dc --- /dev/null +++ b/Symfony/Client/DependencyInjection/ClientFactory.php @@ -0,0 +1,254 @@ +default = $default; + $this->diUtils = DiUtils::create(self::MODULE, $name); + } + + public static function getConfiguration(bool $debug, string $name = 'client'): NodeDefinition + { + $builder = new ArrayNodeDefinition($name); + + $builder->children() + ->booleanNode('traceable_producer')->defaultValue($debug)->end() + ->scalarNode('prefix')->defaultValue('enqueue')->end() + ->scalarNode('separator')->defaultValue('.')->end() + ->scalarNode('app_name')->defaultValue('app')->end() + ->scalarNode('router_topic')->defaultValue('default')->cannotBeEmpty()->end() + ->scalarNode('router_queue')->defaultValue('default')->cannotBeEmpty()->end() + ->scalarNode('router_processor')->defaultNull()->end() + ->integerNode('redelivered_delay_time')->min(0)->defaultValue(0)->end() + ->scalarNode('default_queue')->defaultValue('default')->cannotBeEmpty()->end() + ->arrayNode('driver_options')->addDefaultsIfNotSet()->info('The array contains driver specific options')->ignoreExtraKeys(false)->end() + ->end() + ; + + return $builder; + } + + public function build(ContainerBuilder $container, array $config): void + { + $container->register($this->diUtils->format('context'), Context::class) + ->setFactory([$this->diUtils->reference('driver'), 'getContext']) + ; + + $container->register($this->diUtils->format('driver_factory'), DriverFactory::class); + + $routerProcessor = empty($config['router_processor']) + ? $this->diUtils->format('router_processor') + : $config['router_processor'] + ; + + $container->register($this->diUtils->format('config'), Config::class) + ->setArguments([ + $config['prefix'], + $config['separator'], + $config['app_name'], + $config['router_topic'], + $config['router_queue'], + $config['default_queue'], + $routerProcessor, + $config['transport'], + $config['driver_options'] ?? [], + ]); + + $container->setParameter($this->diUtils->format('router_processor'), $routerProcessor); + $container->setParameter($this->diUtils->format('router_queue_name'), $config['router_queue']); + $container->setParameter($this->diUtils->format('default_queue_name'), $config['default_queue']); + + $container->register($this->diUtils->format('route_collection'), RouteCollection::class) + ->addArgument([]) + ->setFactory([RouteCollection::class, 'fromArray']) + ; + + $container->register($this->diUtils->format('producer'), Producer::class) + // @deprecated + ->setPublic(true) + ->addArgument($this->diUtils->reference('driver')) + ->addArgument($this->diUtils->reference('rpc_factory')) + ->addArgument($this->diUtils->reference('client_extensions')) + ; + + $lazyProducer = $container->register($this->diUtils->format('lazy_producer'), LazyProducer::class); + $lazyProducer->addArgument(ServiceLocatorTagPass::register($container, [ + $this->diUtils->format('producer') => new Reference($this->diUtils->format('producer')), + ])); + $lazyProducer->addArgument($this->diUtils->format('producer')); + + $container->register($this->diUtils->format('spool_producer'), SpoolProducer::class) + ->addArgument($this->diUtils->reference('lazy_producer')) + ; + + $container->register($this->diUtils->format('client_extensions'), ChainExtension::class) + ->addArgument([]) + ; + + $container->register($this->diUtils->format('rpc_factory'), RpcFactory::class) + ->addArgument($this->diUtils->reference('context')) + ; + + $container->register($this->diUtils->format('router_processor'), RouterProcessor::class) + ->addArgument($this->diUtils->reference('driver')) + ; + + $container->register($this->diUtils->format('processor_registry'), ContainerProcessorRegistry::class); + + $container->register($this->diUtils->format('delegate_processor'), DelegateProcessor::class) + ->addArgument($this->diUtils->reference('processor_registry')) + ; + + $container->register($this->diUtils->format('set_router_properties_extension'), SetRouterPropertiesExtension::class) + ->addArgument($this->diUtils->reference('driver')) + ->addTag('enqueue.consumption_extension', ['priority' => 100, 'client' => $this->diUtils->getConfigName()]) + ; + + $container->register($this->diUtils->format('queue_consumer'), QueueConsumer::class) + ->addArgument($this->diUtils->reference('context')) + ->addArgument($this->diUtils->reference('consumption_extensions')) + ->addArgument([]) + ->addArgument(new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE)) + ->addArgument($config['consumption']['receive_timeout']) + ; + + $container->register($this->diUtils->format('consumption_extensions'), ConsumptionChainExtension::class) + ->addArgument([]) + ; + + $container->register($this->diUtils->format('flush_spool_producer_extension'), FlushSpoolProducerExtension::class) + ->addArgument($this->diUtils->reference('spool_producer')) + ->addTag('enqueue.consumption.extension', ['priority' => -100, 'client' => $this->diUtils->getConfigName()]) + ; + + $container->register($this->diUtils->format('exclusive_command_extension'), ExclusiveCommandExtension::class) + ->addArgument($this->diUtils->reference('driver')) + ->addTag('enqueue.consumption.extension', ['priority' => 100, 'client' => $this->diUtils->getConfigName()]) + ; + + if ($config['traceable_producer']) { + $container->register($this->diUtils->format('traceable_producer'), TraceableProducer::class) + ->setDecoratedService($this->diUtils->format('producer')) + ->addArgument($this->diUtils->reference('traceable_producer.inner')) + ; + } + + if ($config['redelivered_delay_time']) { + $container->register($this->diUtils->format('delay_redelivered_message_extension'), DelayRedeliveredMessageExtension::class) + ->addArgument($this->diUtils->reference('driver')) + ->addArgument($config['redelivered_delay_time']) + ->addTag('enqueue.consumption_extension', ['priority' => 10, 'client' => $this->diUtils->getConfigName()]) + ; + + $container->getDefinition($this->diUtils->format('delay_redelivered_message_extension')) + ->replaceArgument(1, $config['redelivered_delay_time']) + ; + } + + $locatorId = 'enqueue.locator'; + if ($container->hasDefinition($locatorId)) { + $locator = $container->getDefinition($locatorId); + $locator->replaceArgument(0, array_replace($locator->getArgument(0), [ + $this->diUtils->format('queue_consumer') => $this->diUtils->reference('queue_consumer'), + $this->diUtils->format('driver') => $this->diUtils->reference('driver'), + $this->diUtils->format('delegate_processor') => $this->diUtils->reference('delegate_processor'), + $this->diUtils->format('producer') => $this->diUtils->reference('lazy_producer'), + ])); + } + + if ($this->default) { + $container->setAlias(ProducerInterface::class, $this->diUtils->format('lazy_producer')); + $container->setAlias(SpoolProducer::class, $this->diUtils->format('spool_producer')); + + if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) { + $container->setAlias($this->diUtils->formatDefault('producer'), $this->diUtils->format('producer')); + $container->setAlias($this->diUtils->formatDefault('spool_producer'), $this->diUtils->format('spool_producer')); + } + } + } + + public function createDriver(ContainerBuilder $container, array $config): string + { + $factoryId = DiUtils::create(TransportFactory::MODULE, $this->diUtils->getConfigName())->format('connection_factory'); + $driverId = $this->diUtils->format('driver'); + $driverFactoryId = $this->diUtils->format('driver_factory'); + + $container->register($driverId, DriverInterface::class) + ->setFactory([new Reference($driverFactoryId), 'create']) + ->addArgument(new Reference($factoryId)) + ->addArgument($this->diUtils->reference('config')) + ->addArgument($this->diUtils->reference('route_collection')) + ; + + if ($this->default) { + $container->setAlias(DriverInterface::class, $driverId); + + if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) { + $container->setAlias($this->diUtils->formatDefault('driver'), $driverId); + } + } + + return $driverId; + } + + public function createFlushSpoolProducerListener(ContainerBuilder $container): void + { + $container->register($this->diUtils->format('flush_spool_producer_listener'), FlushSpoolProducerListener::class) + ->addArgument($this->diUtils->reference('spool_producer')) + ->addTag('kernel.event_subscriber') + ; + } +} diff --git a/Symfony/Client/FlushSpoolProducerListener.php b/Symfony/Client/FlushSpoolProducerListener.php index 1f5fdcb..00543f6 100644 --- a/Symfony/Client/FlushSpoolProducerListener.php +++ b/Symfony/Client/FlushSpoolProducerListener.php @@ -14,9 +14,6 @@ class FlushSpoolProducerListener implements EventSubscriberInterface */ private $producer; - /** - * @param SpoolProducer $producer - */ public function __construct(SpoolProducer $producer) { $this->producer = $producer; @@ -27,10 +24,7 @@ public function flushMessages() $this->producer->flush(); } - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { $events = []; diff --git a/Symfony/Client/LazyProducer.php b/Symfony/Client/LazyProducer.php new file mode 100644 index 0000000..8dd3aad --- /dev/null +++ b/Symfony/Client/LazyProducer.php @@ -0,0 +1,37 @@ +container = $container; + $this->producerId = $producerId; + } + + public function sendEvent(string $topic, $message): void + { + $this->getRealProducer()->sendEvent($topic, $message); + } + + public function sendCommand(string $command, $message, bool $needReply = false): ?Promise + { + return $this->getRealProducer()->sendCommand($command, $message, $needReply); + } + + private function getRealProducer(): ProducerInterface + { + return $this->container->get($this->producerId); + } +} diff --git a/Symfony/Client/Meta/QueuesCommand.php b/Symfony/Client/Meta/QueuesCommand.php deleted file mode 100644 index 02d263d..0000000 --- a/Symfony/Client/Meta/QueuesCommand.php +++ /dev/null @@ -1,73 +0,0 @@ -queueMetaRegistry = $queueRegistry; - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this - ->setName('enqueue:queues') - ->setAliases([ - 'enq:m:q', - 'debug:enqueue:queues', - ]) - ->setDescription('A command shows all available queues and some information about them.') - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $table = new Table($output); - $table->setHeaders(['Client Name', 'Transport Name', 'processors']); - - $count = 0; - $firstRow = true; - foreach ($this->queueMetaRegistry->getQueuesMeta() as $queueMeta) { - if (false == $firstRow) { - $table->addRow(new TableSeparator()); - } - - $table->addRow([ - $queueMeta->getClientName(), - $queueMeta->getClientName() == $queueMeta->getTransportName() ? '(same)' : $queueMeta->getTransportName(), - implode(PHP_EOL, $queueMeta->getProcessors()), - ]); - - ++$count; - $firstRow = false; - } - - $output->writeln(sprintf('Found %s destinations', $count)); - $output->writeln(''); - $table->render(); - } -} diff --git a/Symfony/Client/Meta/TopicsCommand.php b/Symfony/Client/Meta/TopicsCommand.php deleted file mode 100644 index 8314fb9..0000000 --- a/Symfony/Client/Meta/TopicsCommand.php +++ /dev/null @@ -1,69 +0,0 @@ -topicRegistry = $topicRegistry; - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this - ->setName('enqueue:topics') - ->setAliases([ - 'enq:m:t', - 'debug:enqueue:topics', - ]) - ->setDescription('A command shows all available topics and some information about them.') - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $table = new Table($output); - $table->setHeaders(['Topic', 'Description', 'processors']); - - $count = 0; - $firstRow = true; - foreach ($this->topicRegistry->getTopicsMeta() as $topic) { - if (false == $firstRow) { - $table->addRow(new TableSeparator()); - } - - $table->addRow([$topic->getName(), $topic->getDescription(), implode(PHP_EOL, $topic->getProcessors())]); - - ++$count; - $firstRow = false; - } - - $output->writeln(sprintf('Found %s topics', $count)); - $output->writeln(''); - $table->render(); - } -} diff --git a/Symfony/Client/ProduceCommand.php b/Symfony/Client/ProduceCommand.php new file mode 100644 index 0000000..953a766 --- /dev/null +++ b/Symfony/Client/ProduceCommand.php @@ -0,0 +1,92 @@ +container = $container; + $this->defaultClient = $defaultClient; + $this->producerIdPattern = $producerIdPattern; + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setDescription('Sends an event to the topic') + ->addArgument('message', InputArgument::REQUIRED, 'A message') + ->addOption('header', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'The message headers') + ->addOption('client', 'c', InputOption::VALUE_OPTIONAL, 'The client to consume messages from.', $this->defaultClient) + ->addOption('topic', null, InputOption::VALUE_OPTIONAL, 'The topic to send a message to') + ->addOption('command', null, InputOption::VALUE_OPTIONAL, 'The command to send a message to') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $topic = $input->getOption('topic'); + $command = $input->getOption('command'); + $message = $input->getArgument('message'); + $headers = (array) $input->getOption('header'); + $client = $input->getOption('client'); + + if ($topic && $command) { + throw new \LogicException('Either topic or command option should be set, both are set.'); + } + + try { + $producer = $this->getProducer($client); + } catch (NotFoundExceptionInterface $e) { + throw new \LogicException(sprintf('Client "%s" is not supported.', $client), previous: $e); + } + + if ($topic) { + $producer->sendEvent($topic, new Message($message, [], $headers)); + + $output->writeln('An event is sent'); + } elseif ($command) { + $producer->sendCommand($command, $message); + + $output->writeln('A command is sent'); + } else { + throw new \LogicException('Either topic or command option should be set, none is set.'); + } + + return 0; + } + + private function getProducer(string $client): ProducerInterface + { + return $this->container->get(sprintf($this->producerIdPattern, $client)); + } +} diff --git a/Symfony/Client/ProduceMessageCommand.php b/Symfony/Client/ProduceMessageCommand.php deleted file mode 100644 index a9f9ef9..0000000 --- a/Symfony/Client/ProduceMessageCommand.php +++ /dev/null @@ -1,54 +0,0 @@ -producer = $producer; - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this - ->setName('enqueue:produce') - ->setAliases(['enq:p']) - ->setDescription('A command to send a message to topic') - ->addArgument('topic', InputArgument::REQUIRED, 'A topic to send message to') - ->addArgument('message', InputArgument::REQUIRED, 'A message to send') - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->producer->sendEvent( - $input->getArgument('topic'), - $input->getArgument('message') - ); - - $output->writeln('Message is sent'); - } -} diff --git a/Symfony/Client/RoutesCommand.php b/Symfony/Client/RoutesCommand.php new file mode 100644 index 0000000..04b657e --- /dev/null +++ b/Symfony/Client/RoutesCommand.php @@ -0,0 +1,162 @@ +container = $container; + $this->defaultClient = $defaultClient; + $this->driverIdPatter = $driverIdPatter; + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setAliases(['debug:enqueue:routes']) + ->setDescription('A command lists all registered routes.') + ->addOption('show-route-options', null, InputOption::VALUE_NONE, 'Adds ability to hide options.') + ->addOption('client', 'c', InputOption::VALUE_OPTIONAL, 'The client to consume messages from.', $this->defaultClient) + ; + + $this->driver = null; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + try { + $this->driver = $this->getDriver($input->getOption('client')); + } catch (NotFoundExceptionInterface $e) { + throw new \LogicException(sprintf('Client "%s" is not supported.', $input->getOption('client')), previous: $e); + } + + $routes = $this->driver->getRouteCollection()->all(); + $output->writeln(sprintf('Found %s routes', count($routes))); + $output->writeln(''); + + if ($routes) { + $table = new Table($output); + $table->setHeaders(['Type', 'Source', 'Queue', 'Processor', 'Options']); + + $firstRow = true; + foreach ($routes as $route) { + if (false == $firstRow) { + $table->addRow(new TableSeparator()); + + $firstRow = false; + } + + if ($route->isCommand()) { + continue; + } + + $table->addRow([ + $this->formatSourceType($route), + $route->getSource(), + $this->formatQueue($route), + $this->formatProcessor($route), + $input->getOption('show-route-options') ? $this->formatOptions($route) : '(hidden)', + ]); + } + + foreach ($routes as $route) { + if ($route->isTopic()) { + continue; + } + + $table->addRow([ + $this->formatSourceType($route), + $route->getSource(), + $this->formatQueue($route), + $this->formatProcessor($route), + $input->getOption('show-route-options') ? $this->formatOptions($route) : '(hidden)', + ]); + } + + $table->render(); + } + + return 0; + } + + private function formatSourceType(Route $route): string + { + if ($route->isCommand()) { + return 'command'; + } + + if ($route->isTopic()) { + return 'topic'; + } + + return 'unknown'; + } + + private function formatProcessor(Route $route): string + { + if ($route->isProcessorExternal()) { + return 'n\a (external)'; + } + + $processor = $route->getProcessor(); + + return $route->isProcessorExclusive() ? $processor.' (exclusive)' : $processor; + } + + private function formatQueue(Route $route): string + { + $queue = $route->getQueue() ?: $this->driver->getConfig()->getDefaultQueue(); + + return $route->isPrefixQueue() ? $queue.' (prefixed)' : $queue.' (as is)'; + } + + private function formatOptions(Route $route): string + { + return var_export($route->getOptions(), true); + } + + private function getDriver(string $client): DriverInterface + { + return $this->container->get(sprintf($this->driverIdPatter, $client)); + } +} + +function enqueue() +{ +} diff --git a/Symfony/Client/SetupBrokerCommand.php b/Symfony/Client/SetupBrokerCommand.php index c825bd2..92d5ad0 100644 --- a/Symfony/Client/SetupBrokerCommand.php +++ b/Symfony/Client/SetupBrokerCommand.php @@ -3,47 +3,68 @@ namespace Enqueue\Symfony\Client; use Enqueue\Client\DriverInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; +#[AsCommand('enqueue:setup-broker')] class SetupBrokerCommand extends Command { /** - * @var DriverInterface + * @var ContainerInterface */ - private $driver; + private $container; /** - * @param DriverInterface $driver + * @var string */ - public function __construct(DriverInterface $driver) + private $defaultClient; + + /** + * @var string + */ + private $driverIdPattern; + + public function __construct(ContainerInterface $container, string $defaultClient, string $driverIdPattern = 'enqueue.client.%s.driver') { - parent::__construct(null); + $this->container = $container; + $this->defaultClient = $defaultClient; + $this->driverIdPattern = $driverIdPattern; - $this->driver = $driver; + parent::__construct(); } - /** - * {@inheritdoc} - */ - protected function configure() + protected function configure(): void { $this - ->setName('enqueue:setup-broker') ->setAliases(['enq:sb']) - ->setDescription('Creates all required queues') + ->setDescription('Setup broker. Configure the broker, creates queues, topics and so on.') + ->addOption('client', 'c', InputOption::VALUE_OPTIONAL, 'The client to consume messages from.', $this->defaultClient) ; } - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - $output->writeln('Setup Broker'); + $client = $input->getOption('client'); + + try { + $this->getDriver($client)->setupBroker(new ConsoleLogger($output)); + } catch (NotFoundExceptionInterface $e) { + throw new \LogicException(sprintf('Client "%s" is not supported.', $client), previous: $e); + } + + $output->writeln('Broker set up'); - $this->driver->setupBroker(new ConsoleLogger($output)); + return 0; + } + + private function getDriver(string $client): DriverInterface + { + return $this->container->get(sprintf($this->driverIdPattern, $client)); } } diff --git a/Symfony/Client/SetupBrokerExtensionCommandTrait.php b/Symfony/Client/SetupBrokerExtensionCommandTrait.php index 2888f56..bcc4f7b 100644 --- a/Symfony/Client/SetupBrokerExtensionCommandTrait.php +++ b/Symfony/Client/SetupBrokerExtensionCommandTrait.php @@ -10,9 +10,6 @@ trait SetupBrokerExtensionCommandTrait { - /** - * {@inheritdoc} - */ protected function configureSetupBrokerExtension() { $this @@ -21,10 +18,7 @@ protected function configureSetupBrokerExtension() } /** - * @param InputInterface $input - * @param DriverInterface $driver - * - * @return ExtensionInterface + * @return ExtensionInterface|null */ protected function getSetupBrokerExtension(InputInterface $input, DriverInterface $driver) { diff --git a/Symfony/Client/SimpleConsumeCommand.php b/Symfony/Client/SimpleConsumeCommand.php new file mode 100644 index 0000000..fafc35d --- /dev/null +++ b/Symfony/Client/SimpleConsumeCommand.php @@ -0,0 +1,26 @@ + $queueConsumer, + 'driver' => $driver, + 'processor' => $processor, + ]), + 'default', + 'queue_consumer', + 'driver', + 'processor' + ); + } +} diff --git a/Symfony/Client/SimpleProduceCommand.php b/Symfony/Client/SimpleProduceCommand.php new file mode 100644 index 0000000..5d7f765 --- /dev/null +++ b/Symfony/Client/SimpleProduceCommand.php @@ -0,0 +1,18 @@ + $producer]), + 'default', + 'producer' + ); + } +} diff --git a/Symfony/Client/SimpleRoutesCommand.php b/Symfony/Client/SimpleRoutesCommand.php new file mode 100644 index 0000000..0023f14 --- /dev/null +++ b/Symfony/Client/SimpleRoutesCommand.php @@ -0,0 +1,18 @@ + $driver]), + 'default', + 'driver' + ); + } +} diff --git a/Symfony/Client/SimpleSetupBrokerCommand.php b/Symfony/Client/SimpleSetupBrokerCommand.php new file mode 100644 index 0000000..aae19f8 --- /dev/null +++ b/Symfony/Client/SimpleSetupBrokerCommand.php @@ -0,0 +1,18 @@ + $driver]), + 'default', + 'driver' + ); + } +} diff --git a/Symfony/Consumption/ChooseLoggerCommandTrait.php b/Symfony/Consumption/ChooseLoggerCommandTrait.php new file mode 100644 index 0000000..c229c14 --- /dev/null +++ b/Symfony/Consumption/ChooseLoggerCommandTrait.php @@ -0,0 +1,35 @@ +addOption('logger', null, InputOption::VALUE_OPTIONAL, 'A logger to be used. Could be "default", "null", "stdout".', 'default') + ; + } + + protected function getLoggerExtension(InputInterface $input, OutputInterface $output): ?LoggerExtension + { + $logger = $input->getOption('logger'); + switch ($logger) { + case 'null': + return new LoggerExtension(new NullLogger()); + case 'stdout': + return new LoggerExtension(new ConsoleLogger($output)); + case 'default': + return null; + default: + throw new \LogicException(sprintf('The logger "%s" is not supported', $logger)); + } + } +} diff --git a/Symfony/Consumption/ConfigurableConsumeCommand.php b/Symfony/Consumption/ConfigurableConsumeCommand.php new file mode 100644 index 0000000..34cb66d --- /dev/null +++ b/Symfony/Consumption/ConfigurableConsumeCommand.php @@ -0,0 +1,122 @@ +container = $container; + $this->defaultTransport = $defaultTransport; + $this->queueConsumerIdPattern = $queueConsumerIdPattern; + $this->processorRegistryIdPattern = $processorRegistryIdPattern; + + parent::__construct(); + } + + protected function configure(): void + { + $this->configureLimitsExtensions(); + $this->configureQueueConsumerOptions(); + $this->configureLoggerExtension(); + + $this + ->setDescription('A worker that consumes message from a broker. '. + 'To use this broker you have to explicitly set a queue to consume from '. + 'and a message processor service') + ->addArgument('processor', InputArgument::REQUIRED, 'A message processor.') + ->addArgument('queues', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'A queue to consume from', []) + ->addOption('transport', 't', InputOption::VALUE_OPTIONAL, 'The transport to consume messages from.', $this->defaultTransport) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $transport = $input->getOption('transport'); + + try { + $consumer = $this->getQueueConsumer($transport); + } catch (NotFoundExceptionInterface $e) { + throw new \LogicException(sprintf('Transport "%s" is not supported.', $transport), previous: $e); + } + + $this->setQueueConsumerOptions($consumer, $input); + + $processor = $this->getProcessorRegistry($transport)->get($input->getArgument('processor')); + + $queues = $input->getArgument('queues'); + if (empty($queues) && $processor instanceof QueueSubscriberInterface) { + $queues = $processor::getSubscribedQueues(); + } + + if (empty($queues)) { + throw new \LogicException(sprintf('The queue is not provided. The processor must implement "%s" interface and it must return not empty array of queues or a queue set using as a second argument.', QueueSubscriberInterface::class)); + } + + $extensions = $this->getLimitsExtensions($input, $output); + + if ($loggerExtension = $this->getLoggerExtension($input, $output)) { + array_unshift($extensions, $loggerExtension); + } + + foreach ($queues as $queue) { + $consumer->bind($queue, $processor); + } + + $consumer->consume(new ChainExtension($extensions)); + + return 0; + } + + private function getQueueConsumer(string $name): QueueConsumerInterface + { + return $this->container->get(sprintf($this->queueConsumerIdPattern, $name)); + } + + private function getProcessorRegistry(string $name): ProcessorRegistryInterface + { + return $this->container->get(sprintf($this->processorRegistryIdPattern, $name)); + } +} diff --git a/Symfony/Consumption/ConsumeCommand.php b/Symfony/Consumption/ConsumeCommand.php new file mode 100644 index 0000000..b69ae72 --- /dev/null +++ b/Symfony/Consumption/ConsumeCommand.php @@ -0,0 +1,91 @@ +container = $container; + $this->defaultTransport = $defaultTransport; + $this->queueConsumerIdPattern = $queueConsumerIdPattern; + + parent::__construct(); + } + + protected function configure(): void + { + $this->configureLimitsExtensions(); + $this->configureQueueConsumerOptions(); + $this->configureLoggerExtension(); + + $this + ->addOption('transport', 't', InputOption::VALUE_OPTIONAL, 'The transport to consume messages from.', $this->defaultTransport) + ->setDescription('A worker that consumes message from a broker. '. + 'To use this broker you have to configure queue consumer before adding to the command') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $transport = $input->getOption('transport'); + + try { + // QueueConsumer must be pre configured outside of the command! + $consumer = $this->getQueueConsumer($transport); + } catch (NotFoundExceptionInterface $e) { + throw new \LogicException(sprintf('Transport "%s" is not supported.', $transport), previous: $e); + } + + $this->setQueueConsumerOptions($consumer, $input); + + $extensions = $this->getLimitsExtensions($input, $output); + + if ($loggerExtension = $this->getLoggerExtension($input, $output)) { + array_unshift($extensions, $loggerExtension); + } + + $exitStatusExtension = new ExitStatusExtension(); + array_unshift($extensions, $exitStatusExtension); + + $consumer->consume(new ChainExtension($extensions)); + + return $exitStatusExtension->getExitStatus() ?? 0; + } + + private function getQueueConsumer(string $name): QueueConsumerInterface + { + return $this->container->get(sprintf($this->queueConsumerIdPattern, $name)); + } +} diff --git a/Symfony/Consumption/ConsumeMessagesCommand.php b/Symfony/Consumption/ConsumeMessagesCommand.php deleted file mode 100644 index c2c7695..0000000 --- a/Symfony/Consumption/ConsumeMessagesCommand.php +++ /dev/null @@ -1,65 +0,0 @@ -consumer = $consumer; - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this->configureLimitsExtensions(); - $this->configureQueueConsumerOptions(); - - $this - ->setName('enqueue:transport:consume') - ->setDescription('A worker that consumes message from a broker. '. - 'To use this broker you have to configure queue consumer before adding to the command') - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->setQueueConsumerOptions($this->consumer, $input); - - $extensions = $this->getLimitsExtensions($input, $output); - array_unshift($extensions, new LoggerExtension(new ConsoleLogger($output))); - - $runtimeExtensions = new ChainExtension($extensions); - - $this->consumer->consume($runtimeExtensions); - } -} diff --git a/Symfony/Consumption/ContainerAwareConsumeMessagesCommand.php b/Symfony/Consumption/ContainerAwareConsumeMessagesCommand.php deleted file mode 100644 index 297dfa5..0000000 --- a/Symfony/Consumption/ContainerAwareConsumeMessagesCommand.php +++ /dev/null @@ -1,100 +0,0 @@ -consumer = $consumer; - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this->configureLimitsExtensions(); - $this->configureQueueConsumerOptions(); - - $this - ->setName('enqueue:transport:consume') - ->setDescription('A worker that consumes message from a broker. '. - 'To use this broker you have to explicitly set a queue to consume from '. - 'and a message processor service') - ->addArgument('processor-service', InputArgument::REQUIRED, 'A message processor service') - ->addOption('queue', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Queues to consume from', []) - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->setQueueConsumerOptions($this->consumer, $input); - - /** @var PsrProcessor $processor */ - $processor = $this->container->get($input->getArgument('processor-service')); - if (false == $processor instanceof PsrProcessor) { - throw new \LogicException(sprintf( - 'Invalid message processor service given. It must be an instance of %s but %s', - PsrProcessor::class, - get_class($processor) - )); - } - - $queues = $input->getOption('queue'); - if (empty($queues) && $processor instanceof QueueSubscriberInterface) { - $queues = $processor::getSubscribedQueues(); - } - - if (empty($queues)) { - throw new \LogicException(sprintf( - 'The queues are not provided. The processor must implement "%s" interface and it must return not empty array of queues or queues set using --queue option.', - QueueSubscriberInterface::class - )); - } - - $extensions = $this->getLimitsExtensions($input, $output); - array_unshift($extensions, new LoggerExtension(new ConsoleLogger($output))); - - $runtimeExtensions = new ChainExtension($extensions); - - foreach ($queues as $queue) { - $this->consumer->bind($queue, $processor); - } - - $this->consumer->consume($runtimeExtensions); - } -} diff --git a/Symfony/Consumption/LimitsExtensionsCommandTrait.php b/Symfony/Consumption/LimitsExtensionsCommandTrait.php index 34bc76d..d8351ac 100644 --- a/Symfony/Consumption/LimitsExtensionsCommandTrait.php +++ b/Symfony/Consumption/LimitsExtensionsCommandTrait.php @@ -5,6 +5,7 @@ use Enqueue\Consumption\Extension\LimitConsumedMessagesExtension; use Enqueue\Consumption\Extension\LimitConsumerMemoryExtension; use Enqueue\Consumption\Extension\LimitConsumptionTimeExtension; +use Enqueue\Consumption\Extension\NicenessExtension; use Enqueue\Consumption\ExtensionInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -12,21 +13,16 @@ trait LimitsExtensionsCommandTrait { - /** - * {@inheritdoc} - */ protected function configureLimitsExtensions() { $this ->addOption('message-limit', null, InputOption::VALUE_REQUIRED, 'Consume n messages and exit') ->addOption('time-limit', null, InputOption::VALUE_REQUIRED, 'Consume messages during this time') - ->addOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Consume messages until process reaches this memory limit in MB'); + ->addOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Consume messages until process reaches this memory limit in MB') + ->addOption('niceness', null, InputOption::VALUE_REQUIRED, 'Set process niceness'); } /** - * @param InputInterface $input - * @param OutputInterface $output - * * @throws \Exception * * @return ExtensionInterface[] @@ -58,6 +54,11 @@ protected function getLimitsExtensions(InputInterface $input, OutputInterface $o $extensions[] = new LimitConsumerMemoryExtension($memoryLimit); } + $niceness = $input->getOption('niceness'); + if (!empty($niceness) && is_numeric($niceness)) { + $extensions[] = new NicenessExtension((int) $niceness); + } + return $extensions; } } diff --git a/Symfony/Consumption/QueueConsumerOptionsCommandTrait.php b/Symfony/Consumption/QueueConsumerOptionsCommandTrait.php index c6ffd98..fd736f2 100644 --- a/Symfony/Consumption/QueueConsumerOptionsCommandTrait.php +++ b/Symfony/Consumption/QueueConsumerOptionsCommandTrait.php @@ -2,33 +2,21 @@ namespace Enqueue\Symfony\Consumption; -use Enqueue\Consumption\QueueConsumer; +use Enqueue\Consumption\QueueConsumerInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; trait QueueConsumerOptionsCommandTrait { - /** - * {@inheritdoc} - */ protected function configureQueueConsumerOptions() { $this - ->addOption('idle-timeout', null, InputOption::VALUE_REQUIRED, 'The time in milliseconds queue consumer idle if no message has been received.') ->addOption('receive-timeout', null, InputOption::VALUE_REQUIRED, 'The time in milliseconds queue consumer waits for a message.') ; } - /** - * @param QueueConsumer $consumer - * @param InputInterface $input - */ - protected function setQueueConsumerOptions(QueueConsumer $consumer, InputInterface $input) + protected function setQueueConsumerOptions(QueueConsumerInterface $consumer, InputInterface $input) { - if (null !== $idleTimeout = $input->getOption('idle-timeout')) { - $consumer->setIdleTimeout((int) $idleTimeout); - } - if (null !== $receiveTimeout = $input->getOption('receive-timeout')) { $consumer->setReceiveTimeout((int) $receiveTimeout); } diff --git a/Symfony/Consumption/SimpleConsumeCommand.php b/Symfony/Consumption/SimpleConsumeCommand.php new file mode 100644 index 0000000..90d0e36 --- /dev/null +++ b/Symfony/Consumption/SimpleConsumeCommand.php @@ -0,0 +1,18 @@ + $consumer]), + 'default', + 'queue_consumer' + ); + } +} diff --git a/Symfony/ContainerProcessorRegistry.php b/Symfony/ContainerProcessorRegistry.php new file mode 100644 index 0000000..b259d23 --- /dev/null +++ b/Symfony/ContainerProcessorRegistry.php @@ -0,0 +1,29 @@ +locator = $locator; + } + + public function get(string $processorName): Processor + { + if (false == $this->locator->has($processorName)) { + throw new \LogicException(sprintf('Service locator does not have a processor with name "%s".', $processorName)); + } + + return $this->locator->get($processorName); + } +} diff --git a/Symfony/DefaultTransportFactory.php b/Symfony/DefaultTransportFactory.php deleted file mode 100644 index 20fa1c9..0000000 --- a/Symfony/DefaultTransportFactory.php +++ /dev/null @@ -1,218 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->beforeNormalization() - ->always(function ($v) { - if (is_array($v)) { - if (empty($v['dsn']) && empty($v['alias'])) { - throw new \LogicException('Either dsn or alias option must be set'); - } - - return $v; - } - - if (empty($v)) { - return ['dsn' => 'null:']; - } - - if (is_string($v)) { - return false !== strpos($v, ':') || false !== strpos($v, 'env_') ? - ['dsn' => $v] : - ['alias' => $v]; - } - }) - ->end() - ->children() - ->scalarNode('alias')->cannotBeEmpty()->end() - ->scalarNode('dsn')->cannotBeEmpty()->end() - ->end() - ->end() - ; - } - - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - if (isset($config['alias'])) { - $aliasId = sprintf('enqueue.transport.%s.connection_factory', $config['alias']); - } else { - $dsn = $this->resolveDSN($container, $config['dsn']); - - $aliasId = $this->findFactory($dsn)->createConnectionFactory($container, $config); - } - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $container->setAlias($factoryId, new Alias($aliasId, true)); - $container->setAlias('enqueue.transport.connection_factory', new Alias($factoryId, true)); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - if (isset($config['alias'])) { - $aliasId = sprintf('enqueue.transport.%s.context', $config['alias']); - } else { - $dsn = $this->resolveDSN($container, $config['dsn']); - - $aliasId = $this->findFactory($dsn)->createContext($container, $config); - } - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - - $container->setAlias($contextId, new Alias($aliasId, true)); - $container->setAlias('enqueue.transport.context', new Alias($contextId, true)); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - if (isset($config['alias'])) { - $aliasId = sprintf('enqueue.client.%s.driver', $config['alias']); - } else { - $dsn = $this->resolveDSN($container, $config['dsn']); - - $aliasId = $this->findFactory($dsn)->createDriver($container, $config); - } - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - - $container->setAlias($driverId, new Alias($aliasId, true)); - $container->setAlias('enqueue.client.driver', new Alias($driverId, true)); - - return $driverId; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * This is a quick fix to the exception "Incompatible use of dynamic environment variables "ENQUEUE_DSN" found in parameters." - * TODO: We'll have to come up with a better solution. - * - * @param ContainerBuilder $container - * @param $dsn - * - * @return array|false|string - */ - private function resolveDSN(ContainerBuilder $container, $dsn) - { - if (method_exists($container, 'resolveEnvPlaceholders')) { - $dsn = $container->resolveEnvPlaceholders($dsn); - - $matches = []; - if (preg_match('/%env\((.*?)\)/', $dsn, $matches)) { - if (false === $realDsn = getenv($matches[1])) { - throw new \LogicException(sprintf('The env "%s" var is not defined', $matches[1])); - } - - return $realDsn; - } - } - - return $dsn; - } - - /** - * @param string - * @param mixed $dsn - * - * @return TransportFactoryInterface - */ - private function findFactory($dsn) - { - $factory = dsn_to_connection_factory($dsn); - - if ($factory instanceof AmqpConnectionFactory) { - return new AmqpTransportFactory('default_amqp'); - } - - if ($factory instanceof FsConnectionFactory) { - return new FsTransportFactory('default_fs'); - } - - if ($factory instanceof DbalConnectionFactory) { - return new DbalTransportFactory('default_dbal'); - } - - if ($factory instanceof NullConnectionFactory) { - return new NullTransportFactory('default_null'); - } - - if ($factory instanceof GpsConnectionFactory) { - return new GpsTransportFactory('default_gps'); - } - - if ($factory instanceof RedisConnectionFactory) { - return new RedisTransportFactory('default_redis'); - } - - if ($factory instanceof SqsConnectionFactory) { - return new SqsTransportFactory('default_sqs'); - } - - if ($factory instanceof StompConnectionFactory) { - return new StompTransportFactory('default_stomp'); - } - - throw new \LogicException(sprintf( - 'There is no supported transport factory for the connection factory "%s" created from DSN "%s"', - get_class($factory), - $dsn - )); - } -} diff --git a/Symfony/DependencyInjection/BuildConsumptionExtensionsPass.php b/Symfony/DependencyInjection/BuildConsumptionExtensionsPass.php new file mode 100644 index 0000000..99f274e --- /dev/null +++ b/Symfony/DependencyInjection/BuildConsumptionExtensionsPass.php @@ -0,0 +1,60 @@ +hasParameter('enqueue.transports')) { + throw new \LogicException('The "enqueue.transports" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.transports'); + $defaultName = $container->getParameter('enqueue.default_transport'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(TransportFactory::MODULE, $name); + + $extensionsId = $diUtils->format('consumption_extensions'); + if (false == $container->hasDefinition($extensionsId)) { + throw new \LogicException(sprintf('Service "%s" not found', $extensionsId)); + } + + $tags = $container->findTaggedServiceIds('enqueue.transport.consumption_extension'); + + $groupByPriority = []; + foreach ($tags as $serviceId => $tagAttributes) { + foreach ($tagAttributes as $tagAttribute) { + $transport = $tagAttribute['transport'] ?? $defaultName; + + if ($transport !== $name && 'all' !== $transport) { + continue; + } + + $priority = (int) ($tagAttribute['priority'] ?? 0); + + $groupByPriority[$priority][] = new Reference($serviceId); + } + } + + krsort($groupByPriority, \SORT_NUMERIC); + + $flatExtensions = []; + foreach ($groupByPriority as $extension) { + $flatExtensions = array_merge($flatExtensions, $extension); + } + + $extensionsService = $container->getDefinition($extensionsId); + $extensionsService->replaceArgument(0, array_merge( + $extensionsService->getArgument(0), + $flatExtensions + )); + } + } +} diff --git a/Symfony/DependencyInjection/BuildProcessorRegistryPass.php b/Symfony/DependencyInjection/BuildProcessorRegistryPass.php new file mode 100644 index 0000000..cc6e042 --- /dev/null +++ b/Symfony/DependencyInjection/BuildProcessorRegistryPass.php @@ -0,0 +1,50 @@ +hasParameter('enqueue.transports')) { + throw new \LogicException('The "enqueue.transports" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.transports'); + $defaultName = $container->getParameter('enqueue.default_transport'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(TransportFactory::MODULE, $name); + + $processorRegistryId = $diUtils->format('processor_registry'); + if (false == $container->hasDefinition($processorRegistryId)) { + throw new \LogicException(sprintf('Service "%s" not found', $processorRegistryId)); + } + + $tag = 'enqueue.transport.processor'; + $map = []; + foreach ($container->findTaggedServiceIds($tag) as $serviceId => $tagAttributes) { + foreach ($tagAttributes as $tagAttribute) { + $transport = $tagAttribute['transport'] ?? $defaultName; + + if ($transport !== $name && 'all' !== $transport) { + continue; + } + + $processor = $tagAttribute['processor'] ?? $serviceId; + + $map[$processor] = new Reference($serviceId); + } + } + + $registry = $container->getDefinition($processorRegistryId); + $registry->setArgument(0, ServiceLocatorTagPass::register($container, $map, $processorRegistryId)); + } + } +} diff --git a/Symfony/DependencyInjection/TransportFactory.php b/Symfony/DependencyInjection/TransportFactory.php new file mode 100644 index 0000000..944b1a3 --- /dev/null +++ b/Symfony/DependencyInjection/TransportFactory.php @@ -0,0 +1,266 @@ +default = $default; + $this->diUtils = DiUtils::create(self::MODULE, $name); + } + + public static function getConfiguration(string $name = 'transport'): NodeDefinition + { + $knownSchemes = array_keys(Resources::getKnownSchemes()); + $availableSchemes = array_keys(Resources::getAvailableSchemes()); + + $builder = new ArrayNodeDefinition($name); + $builder + ->info('The transport option could accept a string DSN, an array with DSN key, or null. It accept extra options. To find out what option you can set, look at connection factory constructor docblock.') + ->beforeNormalization() + ->always(function ($v) { + if (empty($v)) { + return ['dsn' => 'null:']; + } + + if (is_array($v)) { + if (isset($v['factory_class']) && isset($v['factory_service'])) { + throw new \LogicException('Both options factory_class and factory_service are set. Please choose one.'); + } + + if (isset($v['connection_factory_class']) && (isset($v['factory_class']) || isset($v['factory_service']))) { + throw new \LogicException('The option connection_factory_class must not be used with factory_class or factory_service at the same time. Please choose one.'); + } + + return $v; + } + + if (is_string($v)) { + return ['dsn' => $v]; + } + + throw new \LogicException(sprintf('The value must be array, null or string. Got "%s"', gettype($v))); + }) + ->end() + ->isRequired() + ->ignoreExtraKeys(false) + ->children() + ->scalarNode('dsn') + ->cannotBeEmpty() + ->isRequired() + ->info(sprintf( + 'The MQ broker DSN. These schemes are supported: "%s", to use these "%s" you have to install a package.', + implode('", "', $knownSchemes), + implode('", "', $availableSchemes) + )) + ->end() + ->scalarNode('connection_factory_class') + ->info(sprintf('The connection factory class should implement "%s" interface', ConnectionFactory::class)) + ->end() + ->scalarNode('factory_service') + ->info(sprintf('The factory class should implement "%s" interface', ConnectionFactoryFactoryInterface::class)) + ->end() + ->scalarNode('factory_class') + ->info(sprintf('The factory service should be a class that implements "%s" interface', ConnectionFactoryFactoryInterface::class)) + ->end() + ->end() + ; + + return $builder; + } + + public static function getQueueConsumerConfiguration(string $name = 'consumption'): ArrayNodeDefinition + { + $builder = new ArrayNodeDefinition($name); + + $builder + ->addDefaultsIfNotSet()->children() + ->integerNode('receive_timeout') + ->min(0) + ->defaultValue(10000) + ->info('the time in milliseconds queue consumer waits for a message (10000 ms by default)') + ->end() + ; + + return $builder; + } + + public function buildConnectionFactory(ContainerBuilder $container, array $config): void + { + $factoryId = $this->diUtils->format('connection_factory'); + + $factoryFactoryId = $this->diUtils->format('connection_factory_factory'); + $container->register($factoryFactoryId, $config['factory_class'] ?? ConnectionFactoryFactory::class); + + $factoryFactoryService = new Reference( + $config['factory_service'] ?? $factoryFactoryId + ); + + unset($config['factory_service'], $config['factory_class']); + + $connectionFactoryClass = $config['connection_factory_class'] ?? null; + unset($config['connection_factory_class']); + + if (isset($connectionFactoryClass)) { + $container->register($factoryId, $connectionFactoryClass) + ->addArgument($config) + ; + } else { + $container->register($factoryId, ConnectionFactory::class) + ->setFactory([$factoryFactoryService, 'create']) + ->addArgument($config) + ; + } + + if ($this->default) { + $container->setAlias(ConnectionFactory::class, $factoryId); + + if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) { + $container->setAlias($this->diUtils->formatDefault('connection_factory'), $factoryId); + } + } + } + + public function buildContext(ContainerBuilder $container, array $config): void + { + $factoryId = $this->diUtils->format('connection_factory'); + $this->assertServiceExists($container, $factoryId); + + $contextId = $this->diUtils->format('context'); + + $container->register($contextId, Context::class) + ->setFactory([new Reference($factoryId), 'createContext']) + ; + + $this->addServiceToLocator($container, 'context'); + + if ($this->default) { + $container->setAlias(Context::class, $contextId); + + if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) { + $container->setAlias($this->diUtils->formatDefault('context'), $contextId); + } + } + } + + public function buildQueueConsumer(ContainerBuilder $container, array $config): void + { + $contextId = $this->diUtils->format('context'); + $this->assertServiceExists($container, $contextId); + + $container->setParameter($this->diUtils->format('receive_timeout'), $config['receive_timeout'] ?? 10000); + + $logExtensionId = $this->diUtils->format('log_extension'); + $container->register($logExtensionId, LogExtension::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => $this->diUtils->getConfigName(), 'priority' => -100]) + ; + + $container->register($this->diUtils->format('consumption_extensions'), ChainExtension::class) + ->addArgument([]) + ; + + $container->register($this->diUtils->format('queue_consumer'), QueueConsumer::class) + ->addArgument(new Reference($contextId)) + ->addArgument(new Reference($this->diUtils->format('consumption_extensions'))) + ->addArgument([]) + ->addArgument(new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE)) + ->addArgument($this->diUtils->parameter('receive_timeout')) + ; + + $container->register($this->diUtils->format('processor_registry'), ContainerProcessorRegistry::class); + + $this->addServiceToLocator($container, 'queue_consumer'); + $this->addServiceToLocator($container, 'processor_registry'); + + if ($this->default) { + $container->setAlias(QueueConsumerInterface::class, $this->diUtils->format('queue_consumer')); + + if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) { + $container->setAlias($this->diUtils->formatDefault('queue_consumer'), $this->diUtils->format('queue_consumer')); + } + } + } + + public function buildRpcClient(ContainerBuilder $container, array $config): void + { + $contextId = $this->diUtils->format('context'); + $this->assertServiceExists($container, $contextId); + + $container->register($this->diUtils->format('rpc_factory'), RpcFactory::class) + ->addArgument(new Reference($contextId)) + ; + + $container->register($this->diUtils->format('rpc_client'), RpcClient::class) + ->addArgument(new Reference($contextId)) + ->addArgument(new Reference($this->diUtils->format('rpc_factory'))) + ; + + if ($this->default) { + $container->setAlias(RpcClient::class, $this->diUtils->format('rpc_client')); + + if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) { + $container->setAlias($this->diUtils->formatDefault('rpc_client'), $this->diUtils->format('rpc_client')); + } + } + } + + private function assertServiceExists(ContainerBuilder $container, string $serviceId): void + { + if (false == $container->hasDefinition($serviceId)) { + throw new \InvalidArgumentException(sprintf('The service "%s" does not exist.', $serviceId)); + } + } + + private function addServiceToLocator(ContainerBuilder $container, string $serviceName): void + { + $locatorId = 'enqueue.locator'; + + if ($container->hasDefinition($locatorId)) { + $locator = $container->getDefinition($locatorId); + + $map = $locator->getArgument(0); + $map[$this->diUtils->format($serviceName)] = $this->diUtils->reference($serviceName); + + $locator->replaceArgument(0, $map); + } + } +} diff --git a/Symfony/DiUtils.php b/Symfony/DiUtils.php new file mode 100644 index 0000000..be45287 --- /dev/null +++ b/Symfony/DiUtils.php @@ -0,0 +1,81 @@ +moduleName = $moduleName; + $this->configName = $configName; + } + + public static function create(string $moduleName, string $configName): self + { + return new self($moduleName, $configName); + } + + public function getModuleName(): string + { + return $this->moduleName; + } + + public function getConfigName(): string + { + return $this->configName; + } + + public function reference(string $serviceName, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): Reference + { + return new Reference($this->format($serviceName), $invalidBehavior); + } + + public function referenceDefault(string $serviceName, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): Reference + { + return new Reference($this->formatDefault($serviceName), $invalidBehavior); + } + + public function parameter(string $serviceName): string + { + $fullName = $this->format($serviceName); + + return "%$fullName%"; + } + + public function parameterDefault(string $serviceName): string + { + $fullName = $this->formatDefault($serviceName); + + return "%$fullName%"; + } + + public function format(string $serviceName): string + { + return $this->doFormat($this->moduleName, $this->configName, $serviceName); + } + + public function formatDefault(string $serviceName): string + { + return $this->doFormat($this->moduleName, self::DEFAULT_CONFIG, $serviceName); + } + + private function doFormat(string $moduleName, string $configName, string $serviceName): string + { + return sprintf('enqueue.%s.%s.%s', $moduleName, $configName, $serviceName); + } +} diff --git a/Symfony/DriverFactoryInterface.php b/Symfony/DriverFactoryInterface.php deleted file mode 100644 index 0b1ab02..0000000 --- a/Symfony/DriverFactoryInterface.php +++ /dev/null @@ -1,21 +0,0 @@ -info($message) + ->beforeNormalization() + ->always(function () { + return []; + }) + ->end() + ->validate() + ->always(function () use ($message) { + throw new \InvalidArgumentException($message); + }) + ->end() + ; + + return $node; + } +} diff --git a/Symfony/MissingTransportFactory.php b/Symfony/MissingTransportFactory.php deleted file mode 100644 index 25e6791..0000000 --- a/Symfony/MissingTransportFactory.php +++ /dev/null @@ -1,94 +0,0 @@ -name = $name; - $this->packages = $packages; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - if (1 == count($this->packages)) { - $message = sprintf( - 'In order to use the transport "%s" install a package "%s"', - $this->getName(), - implode('", "', $this->packages) - ); - } else { - $message = sprintf( - 'In order to use the transport "%s" install one of the packages "%s"', - $this->getName(), - implode('", "', $this->packages) - ); - } - - $builder - ->info($message) - ->beforeNormalization() - ->always(function () { - return []; - }) - ->end() - ->validate() - ->always(function () use ($message) { - throw new \InvalidArgumentException($message); - }) - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - throw new \LogicException('Should not be called'); - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - throw new \LogicException('Should not be called'); - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - throw new \LogicException('Should not be called'); - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } -} diff --git a/Symfony/RabbitMqAmqpTransportFactory.php b/Symfony/RabbitMqAmqpTransportFactory.php deleted file mode 100644 index 94ee923..0000000 --- a/Symfony/RabbitMqAmqpTransportFactory.php +++ /dev/null @@ -1,69 +0,0 @@ -children() - ->scalarNode('delay_strategy') - ->defaultValue('dlx') - ->info('The delay strategy to be used. Possible values are "dlx", "delayed_message_plugin" or service id') - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - $factoryId = parent::createConnectionFactory($container, $config); - - $this->registerDelayStrategy($container, $config, $factoryId, $this->getName()); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(RabbitMqDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } -} diff --git a/Symfony/TransportFactoryInterface.php b/Symfony/TransportFactoryInterface.php deleted file mode 100644 index d2be801..0000000 --- a/Symfony/TransportFactoryInterface.php +++ /dev/null @@ -1,35 +0,0 @@ -assertClassImplements(ProcessorRegistryInterface::class, ArrayProcessorRegistry::class); } - public function testCouldBeConstructedWithoutAnyArgument() - { - new ArrayProcessorRegistry(); - } - public function testShouldThrowExceptionIfProcessorIsNotSet() { $registry = new ArrayProcessorRegistry(); @@ -51,10 +47,10 @@ public function testShouldAllowGetProcessorAddedViaAddMethod() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor + * @return MockObject|Processor */ protected function createProcessorMock() { - return $this->createMock(PsrProcessor::class); + return $this->createMock(Processor::class); } } diff --git a/Tests/Client/Amqp/AmqpDriverTest.php b/Tests/Client/Amqp/AmqpDriverTest.php deleted file mode 100644 index a82fda0..0000000 --- a/Tests/Client/Amqp/AmqpDriverTest.php +++ /dev/null @@ -1,417 +0,0 @@ -assertClassImplements(DriverInterface::class, AmqpDriver::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new AmqpDriver( - $this->createAmqpContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - } - - public function testShouldReturnConfigObject() - { - $config = $this->createDummyConfig(); - - $driver = new AmqpDriver($this->createAmqpContextMock(), $config, $this->createDummyQueueMetaRegistry()); - - $this->assertSame($config, $driver->getConfig()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new AmqpQueue('aName'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new AmqpDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aFooQueue'); - - $this->assertSame($expectedQueue, $queue); - $this->assertSame([], $queue->getArguments()); - $this->assertSame(2, $queue->getFlags()); - $this->assertNull($queue->getConsumerTag()); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new AmqpQueue('aName'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new AmqpDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aBarQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $transportMessage = new AmqpMessage(); - $transportMessage->setBody('body'); - $transportMessage->setHeaders(['hkey' => 'hval']); - $transportMessage->setProperties(['key' => 'val']); - $transportMessage->setHeader('content_type', 'ContentType'); - $transportMessage->setHeader('expiration', '12345000'); - $transportMessage->setMessageId('MessageId'); - $transportMessage->setTimestamp(1000); - $transportMessage->setReplyTo('theReplyTo'); - $transportMessage->setCorrelationId('theCorrelationId'); - - $driver = new AmqpDriver( - $this->createAmqpContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $clientMessage = $driver->createClientMessage($transportMessage); - - $this->assertInstanceOf(Message::class, $clientMessage); - $this->assertSame('body', $clientMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'expiration' => '12345000', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $clientMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $clientMessage->getProperties()); - $this->assertSame('MessageId', $clientMessage->getMessageId()); - $this->assertSame(12345, $clientMessage->getExpire()); - $this->assertSame('ContentType', $clientMessage->getContentType()); - $this->assertSame(1000, $clientMessage->getTimestamp()); - $this->assertSame('theReplyTo', $clientMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId()); - } - - public function testShouldThrowExceptionIfExpirationIsNotNumeric() - { - $transportMessage = new AmqpMessage(); - $transportMessage->setHeader('expiration', 'is-not-numeric'); - - $driver = new AmqpDriver( - $this->createAmqpContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('expiration header is not numeric. "is-not-numeric"'); - - $driver->createClientMessage($transportMessage); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setBody('body'); - $clientMessage->setHeaders(['hkey' => 'hval']); - $clientMessage->setProperties(['key' => 'val']); - $clientMessage->setContentType('ContentType'); - $clientMessage->setExpire(123); - $clientMessage->setMessageId('MessageId'); - $clientMessage->setTimestamp(1000); - $clientMessage->setReplyTo('theReplyTo'); - $clientMessage->setCorrelationId('theCorrelationId'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new AmqpMessage()) - ; - - $driver = new AmqpDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - $this->assertInstanceOf(AmqpMessage::class, $transportMessage); - $this->assertSame('body', $transportMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - 'content_type' => 'ContentType', - 'delivery_mode' => 2, - 'expiration' => '123000', - ], $transportMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $transportMessage->getProperties()); - $this->assertSame('MessageId', $transportMessage->getMessageId()); - $this->assertSame(1000, $transportMessage->getTimestamp()); - $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); - } - - public function testShouldSendMessageToRouter() - { - $topic = new AmqpTopic(''); - $transportMessage = new AmqpMessage(); - - $producer = $this->createAmqpProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createTopic') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new AmqpDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic'); - - $driver->sendToRouter($message); - } - - public function testShouldThrowExceptionIfTopicParameterIsNotSet() - { - $driver = new AmqpDriver( - $this->createAmqpContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name parameter is required but is not set'); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $queue = new AmqpQueue(''); - $transportMessage = new AmqpMessage(); - - $producer = $this->createAmqpProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new AmqpDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - - $driver->sendToProcessor($message); - } - - public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet() - { - $driver = new AmqpDriver( - $this->createAmqpContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Processor name parameter is required but is not set'); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet() - { - $driver = new AmqpDriver( - $this->createAmqpContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Queue name parameter is required but is not set'); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - - $driver->sendToProcessor($message); - } - - public function testShouldSetupBroker() - { - $routerTopic = new AmqpTopic(''); - $routerQueue = new AmqpQueue(''); - - $processorQueue = new AmqpQueue(''); - - $context = $this->createAmqpContextMock(); - // setup router - $context - ->expects($this->at(0)) - ->method('createTopic') - ->willReturn($routerTopic) - ; - $context - ->expects($this->at(1)) - ->method('createQueue') - ->willReturn($routerQueue) - ; - $context - ->expects($this->at(2)) - ->method('declareTopic') - ->with($this->identicalTo($routerTopic)) - ; - $context - ->expects($this->at(3)) - ->method('declareQueue') - ->with($this->identicalTo($routerQueue)) - ; - $context - ->expects($this->at(4)) - ->method('bind') - ->with($this->isInstanceOf(AmqpBind::class)) - ; - // setup processor queue - $context - ->expects($this->at(5)) - ->method('createQueue') - ->willReturn($processorQueue) - ; - $context - ->expects($this->at(6)) - ->method('declareQueue') - ->with($this->identicalTo($processorQueue)) - ; - - $meta = new QueueMetaRegistry($this->createDummyConfig(), [ - 'default' => [], - ]); - - $driver = new AmqpDriver( - $context, - $this->createDummyConfig(), - $meta - ); - - $driver->setupBroker(); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext - */ - private function createAmqpContextMock() - { - return $this->createMock(AmqpContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpProducer - */ - private function createAmqpProducerMock() - { - return $this->createMock(AmqpProducer::class); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry($this->createDummyConfig(), []); - $registry->add('default'); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } - - /** - * @return Config - */ - private function createDummyConfig() - { - return Config::create('aPrefix'); - } -} diff --git a/Tests/Client/Amqp/RabbitMqDriverTest.php b/Tests/Client/Amqp/RabbitMqDriverTest.php deleted file mode 100644 index 3b67ef5..0000000 --- a/Tests/Client/Amqp/RabbitMqDriverTest.php +++ /dev/null @@ -1,599 +0,0 @@ -assertClassImplements(DriverInterface::class, RabbitMqDriver::class); - } - - public function testShouldExtendsAmqpDriverClass() - { - $this->assertClassExtends(AmqpDriver::class, RabbitMqDriver::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - } - - public function testShouldReturnConfigObject() - { - $config = Config::create(); - - $driver = new RabbitMqDriver($this->createAmqpContextMock(), $config, $this->createDummyQueueMetaRegistry()); - - $this->assertSame($config, $driver->getConfig()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new AmqpQueue('aName'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new AmqpDriver($context, Config::create(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aFooQueue'); - - $this->assertSame($expectedQueue, $queue); - $this->assertSame([], $queue->getArguments()); - $this->assertSame(2, $queue->getFlags()); - $this->assertNull($queue->getConsumerTag()); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new AmqpQueue('aName'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new AmqpDriver($context, Config::create(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aBarQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $transportMessage = new AmqpMessage(); - $transportMessage->setBody('body'); - $transportMessage->setHeaders(['hkey' => 'hval']); - $transportMessage->setProperties(['key' => 'val']); - $transportMessage->setProperty('enqueue-delay', '5678000'); - $transportMessage->setHeader('content_type', 'ContentType'); - $transportMessage->setHeader('expiration', '12345000'); - $transportMessage->setHeader('priority', 3); - $transportMessage->setMessageId('MessageId'); - $transportMessage->setTimestamp(1000); - $transportMessage->setReplyTo('theReplyTo'); - $transportMessage->setCorrelationId('theCorrelationId'); - - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - new Config('', '', '', '', '', '', ['delay_strategy' => 'dlx']), - $this->createDummyQueueMetaRegistry() - ); - - $clientMessage = $driver->createClientMessage($transportMessage); - - $this->assertInstanceOf(Message::class, $clientMessage); - $this->assertSame('body', $clientMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'expiration' => '12345000', - 'priority' => 3, - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $clientMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - 'enqueue-delay' => '5678000', - ], $clientMessage->getProperties()); - $this->assertSame('MessageId', $clientMessage->getMessageId()); - $this->assertSame(12345, $clientMessage->getExpire()); - $this->assertSame(5678, $clientMessage->getDelay()); - $this->assertSame('ContentType', $clientMessage->getContentType()); - $this->assertSame(1000, $clientMessage->getTimestamp()); - $this->assertSame(MessagePriority::HIGH, $clientMessage->getPriority()); - $this->assertSame('theReplyTo', $clientMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId()); - } - - public function testShouldThrowExceptionIfXDelayIsNotNumeric() - { - $transportMessage = new AmqpMessage(); - $transportMessage->setProperty('enqueue-delay', 'is-not-numeric'); - - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('"enqueue-delay" header is not numeric. "is-not-numeric"'); - - $driver->createClientMessage($transportMessage); - } - - public function testShouldThrowExceptionIfExpirationIsNotNumeric() - { - $transportMessage = new AmqpMessage(); - $transportMessage->setHeader('expiration', 'is-not-numeric'); - - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('expiration header is not numeric. "is-not-numeric"'); - - $driver->createClientMessage($transportMessage); - } - - public function testShouldThrowExceptionIfCantConvertTransportPriorityToClientPriority() - { - $transportMessage = new AmqpMessage(); - $transportMessage->setHeader('priority', 'unknown'); - - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Cant convert transport priority to client: "unknown"'); - - $driver->createClientMessage($transportMessage); - } - - public function testShouldThrowExceptionIfCantConvertClientPriorityToTransportPriority() - { - $clientMessage = new Message(); - $clientMessage->setPriority('unknown'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new AmqpMessage()) - ; - - $driver = new RabbitMqDriver( - $context, - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Given priority could not be converted to client\'s one. Got: unknown'); - - $driver->createTransportMessage($clientMessage); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setBody('body'); - $clientMessage->setHeaders(['hkey' => 'hval']); - $clientMessage->setProperties(['key' => 'val']); - $clientMessage->setContentType('ContentType'); - $clientMessage->setExpire(123); - $clientMessage->setPriority(MessagePriority::VERY_HIGH); - $clientMessage->setDelay(432); - $clientMessage->setMessageId('MessageId'); - $clientMessage->setTimestamp(1000); - $clientMessage->setReplyTo('theReplyTo'); - $clientMessage->setCorrelationId('theCorrelationId'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new AmqpMessage()) - ; - - $driver = new RabbitMqDriver( - $context, - new Config('', '', '', '', '', '', ['delay_strategy' => 'dlx']), - $this->createDummyQueueMetaRegistry() - ); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - $this->assertInstanceOf(AmqpMessage::class, $transportMessage); - $this->assertSame('body', $transportMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - 'content_type' => 'ContentType', - 'delivery_mode' => 2, - 'expiration' => '123000', - 'priority' => 4, - ], $transportMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - 'enqueue-delay' => 432000, - ], $transportMessage->getProperties()); - $this->assertSame('MessageId', $transportMessage->getMessageId()); - $this->assertSame(1000, $transportMessage->getTimestamp()); - $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); - } - - public function testThrowIfDelayNotSupportedOnConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setDelay(432); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new AmqpMessage()) - ; - - $driver = new RabbitMqDriver( - $context, - new Config('', '', '', '', '', '', ['delay_strategy' => null]), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The message delaying is not supported. In order to use delay feature install RabbitMQ delay strategy.'); - $driver->createTransportMessage($clientMessage); - } - - public function testShouldSendMessageToRouter() - { - $topic = new AmqpTopic(''); - $transportMessage = new AmqpMessage(); - - $producer = $this->createAmqpProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createTopic') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RabbitMqDriver( - $context, - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic'); - - $driver->sendToRouter($message); - } - - public function testShouldThrowExceptionIfTopicParameterIsNotSet() - { - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name parameter is required but is not set'); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $queue = new AmqpQueue(''); - $transportMessage = new AmqpMessage(); - - $producer = $this->createAmqpProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RabbitMqDriver( - $context, - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - - $driver->sendToProcessor($message); - } - - public function testShouldSendMessageToProcessorWithDeliveryDelay() - { - $queue = new AmqpQueue(''); - $transportMessage = new AmqpMessage(); - - $producer = $this->createAmqpProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $producer - ->expects($this->once()) - ->method('setDeliveryDelay') - ->with($this->identicalTo(10000)) - ; - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RabbitMqDriver( - $context, - new Config('', '', '', '', '', '', ['delay_strategy' => 'dlx']), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - $message->setDelay(10); - - $driver->sendToProcessor($message); - } - - public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet() - { - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Processor name parameter is required but is not set'); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet() - { - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Queue name parameter is required but is not set'); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - - $driver->sendToProcessor($message); - } - - public function testShouldSetupBrokerWhenDelayPluginNotInstalled() - { - $routerTopic = new AmqpTopic(''); - $routerQueue = new AmqpQueue(''); - - $processorQueue = new AmqpQueue(''); - - $context = $this->createAmqpContextMock(); - // setup router - $context - ->expects($this->at(0)) - ->method('createTopic') - ->willReturn($routerTopic) - ; - $context - ->expects($this->at(1)) - ->method('createQueue') - ->willReturn($routerQueue) - ; - $context - ->expects($this->at(2)) - ->method('declareTopic') - ->with($this->identicalTo($routerTopic)) - ; - $context - ->expects($this->at(3)) - ->method('declareQueue') - ->with($this->identicalTo($routerQueue)) - ; - $context - ->expects($this->at(4)) - ->method('bind') - ->with($this->isInstanceOf(AmqpBind::class)) - ; - // setup processor queue - $context - ->expects($this->at(5)) - ->method('createQueue') - ->willReturn($processorQueue) - ; - - $config = Config::create('', '', '', '', '', '', ['delay_strategy' => null]); - - $meta = new QueueMetaRegistry($config, ['default' => []]); - - $driver = new RabbitMqDriver($context, $config, $meta); - - $driver->setupBroker(); - } - - public function testShouldSetupBroker() - { - $routerTopic = new AmqpTopic(''); - $routerQueue = new AmqpQueue(''); - - $processorQueue = new AmqpQueue(''); - - $context = $this->createAmqpContextMock(); - // setup router - $context - ->expects($this->at(0)) - ->method('createTopic') - ->willReturn($routerTopic) - ; - $context - ->expects($this->at(1)) - ->method('createQueue') - ->willReturn($routerQueue) - ; - $context - ->expects($this->at(2)) - ->method('declareTopic') - ->with($this->identicalTo($routerTopic)) - ; - $context - ->expects($this->at(3)) - ->method('declareQueue') - ->with($this->identicalTo($routerQueue)) - ; - $context - ->expects($this->at(4)) - ->method('bind') - ->with($this->isInstanceOf(AmqpBind::class)) - ; - // setup processor queue - $context - ->expects($this->at(5)) - ->method('createQueue') - ->willReturn($processorQueue) - ; - $context - ->expects($this->at(6)) - ->method('declareQueue') - ->with($this->identicalTo($processorQueue)) - ; - - $config = Config::create('', '', '', '', '', '', ['delay_strategy' => 'dlx']); - - $meta = new QueueMetaRegistry($config, ['default' => []]); - - $driver = new RabbitMqDriver($context, $config, $meta); - - $driver->setupBroker(); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext - */ - private function createAmqpContextMock() - { - return $this->createMock(AmqpContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpProducer - */ - private function createAmqpProducerMock() - { - return $this->createMock(AmqpProducer::class); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry(Config::create('aPrefix'), []); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } -} diff --git a/Tests/Client/ChainExtensionTest.php b/Tests/Client/ChainExtensionTest.php index 3b1d82f..0f42bcf 100644 --- a/Tests/Client/ChainExtensionTest.php +++ b/Tests/Client/ChainExtensionTest.php @@ -3,9 +3,16 @@ namespace Enqueue\Tests\Client; use Enqueue\Client\ChainExtension; +use Enqueue\Client\DriverInterface; +use Enqueue\Client\DriverPreSend; use Enqueue\Client\ExtensionInterface; use Enqueue\Client\Message; +use Enqueue\Client\PostSend; +use Enqueue\Client\PreSend; +use Enqueue\Client\ProducerInterface; use Enqueue\Test\ClassExtensionTrait; +use Interop\Queue\Destination; +use Interop\Queue\Message as TransportMessage; use PHPUnit\Framework\TestCase; class ChainExtensionTest extends TestCase @@ -17,57 +24,129 @@ public function testShouldImplementExtensionInterface() $this->assertClassImplements(ExtensionInterface::class, ChainExtension::class); } - public function testCouldBeConstructedWithExtensionsArray() + public function testShouldBeFinal() { - new ChainExtension([$this->createExtension(), $this->createExtension()]); + $this->assertClassFinal(ChainExtension::class); } - public function testShouldProxyOnPreSendToAllInternalExtensions() + public function testThrowIfArrayContainsNotExtension() { - $message = new Message(); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Invalid extension given'); + + new ChainExtension([$this->createExtension(), new \stdClass()]); + } + + public function testShouldProxyOnPreSendEventToAllInternalExtensions() + { + $preSend = new PreSend( + 'aCommandOrTopic', + new Message(), + $this->createMock(ProducerInterface::class), + $this->createMock(DriverInterface::class) + ); + + $fooExtension = $this->createExtension(); + $fooExtension + ->expects($this->once()) + ->method('onPreSendEvent') + ->with($this->identicalTo($preSend)) + ; + $barExtension = $this->createExtension(); + $barExtension + ->expects($this->once()) + ->method('onPreSendEvent') + ->with($this->identicalTo($preSend)) + ; + + $extensions = new ChainExtension([$fooExtension, $barExtension]); + + $extensions->onPreSendEvent($preSend); + } + + public function testShouldProxyOnPreSendCommandToAllInternalExtensions() + { + $preSend = new PreSend( + 'aCommandOrTopic', + new Message(), + $this->createMock(ProducerInterface::class), + $this->createMock(DriverInterface::class) + ); + + $fooExtension = $this->createExtension(); + $fooExtension + ->expects($this->once()) + ->method('onPreSendCommand') + ->with($this->identicalTo($preSend)) + ; + $barExtension = $this->createExtension(); + $barExtension + ->expects($this->once()) + ->method('onPreSendCommand') + ->with($this->identicalTo($preSend)) + ; + + $extensions = new ChainExtension([$fooExtension, $barExtension]); + + $extensions->onPreSendCommand($preSend); + } + + public function testShouldProxyOnDriverPreSendToAllInternalExtensions() + { + $driverPreSend = new DriverPreSend( + new Message(), + $this->createMock(ProducerInterface::class), + $this->createMock(DriverInterface::class) + ); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) - ->method('onPreSend') - ->with('topic', $this->identicalTo($message)) + ->method('onDriverPreSend') + ->with($this->identicalTo($driverPreSend)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) - ->method('onPreSend') - ->with('topic', $this->identicalTo($message)) + ->method('onDriverPreSend') + ->with($this->identicalTo($driverPreSend)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onPreSend('topic', $message); + $extensions->onDriverPreSend($driverPreSend); } - public function testShouldProxyOnPostSendToAllInternalExtensions() + public function testShouldProxyOnPostSentToAllInternalExtensions() { - $message = new Message(); + $postSend = new PostSend( + new Message(), + $this->createMock(ProducerInterface::class), + $this->createMock(DriverInterface::class), + $this->createMock(Destination::class), + $this->createMock(TransportMessage::class) + ); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) ->method('onPostSend') - ->with('topic', $this->identicalTo($message)) + ->with($this->identicalTo($postSend)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) ->method('onPostSend') - ->with('topic', $this->identicalTo($message)) + ->with($this->identicalTo($postSend)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onPostSend('topic', $message); + $extensions->onPostSend($postSend); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ExtensionInterface + * @return \PHPUnit\Framework\MockObject\MockObject|ExtensionInterface */ protected function createExtension() { diff --git a/Tests/Client/ConfigTest.php b/Tests/Client/ConfigTest.php index 1ecb0f9..09b80e2 100644 --- a/Tests/Client/ConfigTest.php +++ b/Tests/Client/ConfigTest.php @@ -7,139 +7,246 @@ class ConfigTest extends TestCase { - public function testShouldReturnRouterProcessorNameSetInConstructor() + public function testShouldReturnPrefixSetInConstructor() { $config = new Config( - 'aPrefix', + 'thePrefix', + 'theSeparator', 'aApp', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aRouterProcessorName', $config->getRouterProcessorName()); + $this->assertEquals('thePrefix', $config->getPrefix()); } - public function testShouldReturnRouterTopicNameSetInConstructor() + /** + * @dataProvider provideEmptyStrings + */ + public function testShouldTrimReturnPrefixSetInConstructor(string $empty) { $config = new Config( - 'aPrefix', + $empty, 'aApp', + 'theSeparator', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aRouterTopicName', $config->getRouterTopicName()); + $this->assertSame('', $config->getPrefix()); } - public function testShouldReturnRouterQueueNameSetInConstructor() + public function testShouldReturnAppNameSetInConstructor() { $config = new Config( 'aPrefix', - 'aApp', + 'theSeparator', + 'theApp', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aRouterQueueName', $config->getRouterQueueName()); + $this->assertEquals('theApp', $config->getApp()); } - public function testShouldReturnDefaultQueueNameSetInConstructor() + /** + * @dataProvider provideEmptyStrings + */ + public function testShouldTrimReturnAppNameSetInConstructor(string $empty) { $config = new Config( 'aPrefix', - 'aApp', + 'theSeparator', + $empty, 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aDefaultQueueName', $config->getDefaultProcessorQueueName()); + $this->assertSame('', $config->getApp()); } - public function testShouldCreateRouterTopicName() + public function testShouldReturnRouterProcessorNameSetInConstructor() { $config = new Config( 'aPrefix', + 'theSeparator', 'aApp', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aprefix.aname', $config->createTransportRouterTopicName('aName')); + $this->assertEquals('aRouterProcessorName', $config->getRouterProcessor()); } - public function testShouldCreateProcessorQueueName() + public function testShouldReturnRouterTopicNameSetInConstructor() { $config = new Config( 'aPrefix', + 'theSeparator', 'aApp', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aprefix.aapp.aname', $config->createTransportQueueName('aName')); + $this->assertEquals('aRouterTopicName', $config->getRouterTopic()); } - public function testShouldCreateProcessorQueueNameWithoutAppName() + public function testShouldReturnRouterQueueNameSetInConstructor() { $config = new Config( 'aPrefix', - '', + 'theSeparator', + 'aApp', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aprefix.aname', $config->createTransportQueueName('aName')); + $this->assertEquals('aRouterQueueName', $config->getRouterQueue()); } - public function testShouldCreateProcessorQueueNameWithoutPrefix() + public function testShouldReturnDefaultQueueNameSetInConstructor() { $config = new Config( - '', + 'aPrefix', + 'theSeparator', 'aApp', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aapp.aname', $config->createTransportQueueName('aName')); + $this->assertEquals('aDefaultQueueName', $config->getDefaultQueue()); } - public function testShouldCreateProcessorQueueNameWithoutPrefixAndAppName() + public function testShouldCreateDefaultConfig() { - $config = new Config( + $config = Config::create(); + + $this->assertSame('default', $config->getDefaultQueue()); + $this->assertSame('router', $config->getRouterProcessor()); + $this->assertSame('default', $config->getRouterQueue()); + $this->assertSame('router', $config->getRouterTopic()); + } + + /** + * @dataProvider provideEmptyStrings + */ + public function testThrowIfRouterTopicNameIsEmpty(string $empty) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Router topic is empty.'); + new Config( '', '', - 'aRouterTopicName', + '', + $empty, 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] + ); + } + + /** + * @dataProvider provideEmptyStrings + */ + public function testThrowIfRouterQueueNameIsEmpty(string $empty) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Router queue is empty.'); + new Config( + '', + '', + '', + 'aRouterTopicName', + $empty, + 'aDefaultQueueName', + 'aRouterProcessorName', + [], + [] + ); + } + + /** + * @dataProvider provideEmptyStrings + */ + public function testThrowIfDefaultQueueNameIsEmpty(string $empty) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Default processor queue name is empty.'); + new Config( + '', + '', + '', + 'aRouterTopicName', + 'aRouterQueueName', + $empty, + 'aRouterProcessorName', + [], + [] ); + } - $this->assertEquals('aname', $config->createTransportQueueName('aName')); + /** + * @dataProvider provideEmptyStrings + */ + public function testThrowIfRouterProcessorNameIsEmpty(string $empty) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Router processor name is empty.'); + new Config( + '', + '', + '', + 'aRouterTopicName', + 'aRouterQueueName', + 'aDefaultQueueName', + $empty, + [], + [] + ); } - public function testShouldCreateDefaultConfig() + public function provideEmptyStrings() { - $config = Config::create(); + yield ['']; + + yield [' ']; + + yield [' ']; - $this->assertSame('default', $config->getDefaultProcessorQueueName()); - $this->assertSame('router', $config->getRouterProcessorName()); - $this->assertSame('default', $config->getRouterQueueName()); - $this->assertSame('router', $config->getRouterTopicName()); + yield ["\t"]; } } diff --git a/Tests/Client/ConsumptionExtension/DelayRedeliveredMessageExtensionTest.php b/Tests/Client/ConsumptionExtension/DelayRedeliveredMessageExtensionTest.php index b61a341..a660126 100644 --- a/Tests/Client/ConsumptionExtension/DelayRedeliveredMessageExtensionTest.php +++ b/Tests/Client/ConsumptionExtension/DelayRedeliveredMessageExtensionTest.php @@ -4,22 +4,25 @@ use Enqueue\Client\ConsumptionExtension\DelayRedeliveredMessageExtension; use Enqueue\Client\DriverInterface; +use Enqueue\Client\DriverSendResult; use Enqueue\Client\Message; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\MessageReceived; use Enqueue\Consumption\Result; use Enqueue\Null\NullMessage; use Enqueue\Null\NullQueue; -use Interop\Queue\PsrContext; +use Enqueue\Test\TestLogger; +use Interop\Queue\Consumer; +use Interop\Queue\Context as InteropContext; +use Interop\Queue\Destination; +use Interop\Queue\Message as TransportMessage; +use Interop\Queue\Processor; +use Interop\Queue\Queue; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class DelayRedeliveredMessageExtensionTest extends TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new DelayRedeliveredMessageExtension($this->createDriverMock(), 12345); - } - public function testShouldSendDelayedMessageAndRejectOriginalMessage() { $queue = new NullQueue('queue'); @@ -38,6 +41,7 @@ public function testShouldSendDelayedMessageAndRejectOriginalMessage() ->expects(self::once()) ->method('sendToProcessor') ->with(self::isInstanceOf(Message::class)) + ->willReturn($this->createDriverSendResult()) ; $driver ->expects(self::once()) @@ -46,32 +50,23 @@ public function testShouldSendDelayedMessageAndRejectOriginalMessage() ->willReturn($delayedMessage) ; - $logger = $this->createLoggerMock(); - $logger - ->expects(self::at(0)) - ->method('debug') - ->with('[DelayRedeliveredMessageExtension] Send delayed message') - ; - $logger - ->expects(self::at(1)) - ->method('debug') - ->with( - '[DelayRedeliveredMessageExtension] '. - 'Reject redelivered original message by setting reject status to context.' - ) - ; + $logger = new TestLogger(); - $context = new Context($this->createPsrContextMock()); - $context->setPsrQueue($queue); - $context->setPsrMessage($originMessage); - $context->setLogger($logger); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub($queue), + $originMessage, + $this->createProcessorMock(), + 1, + $logger + ); - $this->assertNull($context->getResult()); + $this->assertNull($messageReceived->getResult()); $extension = new DelayRedeliveredMessageExtension($driver, 12345); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - $result = $context->getResult(); + $result = $messageReceived->getResult(); $this->assertInstanceOf(Result::class, $result); $this->assertSame(Result::REJECT, $result->getStatus()); $this->assertSame('A new copy of the message was sent with a delay. The original message is rejected', $result->getReason()); @@ -80,6 +75,16 @@ public function testShouldSendDelayedMessageAndRejectOriginalMessage() $this->assertEquals([ 'enqueue.redelivery_count' => 1, ], $delayedMessage->getProperties()); + + self::assertTrue( + $logger->hasDebugThatContains('[DelayRedeliveredMessageExtension] Send delayed message') + ); + self::assertTrue( + $logger->hasDebugThatContains( + '[DelayRedeliveredMessageExtension] '. + 'Reject redelivered original message by setting reject status to context.' + ) + ); } public function testShouldDoNothingIfMessageIsNotRedelivered() @@ -92,13 +97,19 @@ public function testShouldDoNothingIfMessageIsNotRedelivered() ->method('sendToProcessor') ; - $context = new Context($this->createPsrContextMock()); - $context->setPsrMessage($message); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); $extension = new DelayRedeliveredMessageExtension($driver, 12345); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - $this->assertNull($context->getResult()); + $this->assertNull($messageReceived->getResult()); } public function testShouldDoNothingIfMessageIsRedeliveredButResultWasAlreadySetOnContext() @@ -112,35 +123,64 @@ public function testShouldDoNothingIfMessageIsRedeliveredButResultWasAlreadySetO ->method('sendToProcessor') ; - $context = new Context($this->createPsrContextMock()); - $context->setPsrMessage($message); - $context->setResult('aStatus'); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); + $messageReceived->setResult(Result::ack()); $extension = new DelayRedeliveredMessageExtension($driver, 12345); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface + * @return MockObject */ - private function createDriverMock() + private function createDriverMock(): DriverInterface { return $this->createMock(DriverInterface::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject + */ + private function createContextMock(): InteropContext + { + return $this->createMock(InteropContext::class); + } + + /** + * @return MockObject */ - private function createPsrContextMock() + private function createProcessorMock(): Processor { - return $this->createMock(PsrContext::class); + return $this->createMock(Processor::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|LoggerInterface + * @return MockObject|Consumer */ - private function createLoggerMock() + private function createConsumerStub(?Queue $queue): Consumer + { + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue ?? new NullQueue('queue')) + ; + + return $consumerMock; + } + + private function createDriverSendResult(): DriverSendResult { - return $this->createMock(LoggerInterface::class); + return new DriverSendResult( + $this->createMock(Destination::class), + $this->createMock(TransportMessage::class) + ); } } diff --git a/Tests/Client/ConsumptionExtension/ExclusiveCommandExtensionTest.php b/Tests/Client/ConsumptionExtension/ExclusiveCommandExtensionTest.php index acbcb41..b1e47c8 100644 --- a/Tests/Client/ConsumptionExtension/ExclusiveCommandExtensionTest.php +++ b/Tests/Client/ConsumptionExtension/ExclusiveCommandExtensionTest.php @@ -4,14 +4,19 @@ use Enqueue\Client\Config; use Enqueue\Client\ConsumptionExtension\ExclusiveCommandExtension; -use Enqueue\Client\ExtensionInterface as ClientExtensionInterface; -use Enqueue\Client\Message; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\ExtensionInterface as ConsumptionExtensionInterface; -use Enqueue\Null\NullContext; +use Enqueue\Client\DriverInterface; +use Enqueue\Client\Route; +use Enqueue\Client\RouteCollection; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\MessageReceivedExtensionInterface; use Enqueue\Null\NullMessage; use Enqueue\Null\NullQueue; use Enqueue\Test\ClassExtensionTrait; +use Interop\Queue\Consumer; +use Interop\Queue\Context as InteropContext; +use Interop\Queue\Processor; +use Interop\Queue\Queue; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; @@ -19,176 +24,263 @@ class ExclusiveCommandExtensionTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementConsumptionExtensionInterface() + public function testShouldImplementMessageReceivedExtensionInterface() { - $this->assertClassImplements(ConsumptionExtensionInterface::class, ExclusiveCommandExtension::class); + $this->assertClassImplements(MessageReceivedExtensionInterface::class, ExclusiveCommandExtension::class); } - public function testShouldImplementClientExtensionInterface() + public function testShouldBeFinal() { - $this->assertClassImplements(ClientExtensionInterface::class, ExclusiveCommandExtension::class); - } - - public function testCouldBeConstructedWithQueueNameToProcessorNameMap() - { - new ExclusiveCommandExtension([]); - - new ExclusiveCommandExtension(['fooQueueName' => 'fooProcessorName']); + $this->assertClassFinal(ExclusiveCommandExtension::class); } public function testShouldDoNothingIfMessageHasTopicPropertySetOnPreReceive() { $message = new NullMessage(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'aTopic'); + $message->setProperty(Config::TOPIC, 'aTopic'); - $context = new Context(new NullContext()); - $context->setPsrMessage($message); + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('createQueue') + ; - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', - ]); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); + + $extension = new ExclusiveCommandExtension($driver); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - self::assertNull($context->getResult()); + self::assertNull($messageReceived->getResult()); $this->assertEquals([ - 'enqueue.topic_name' => 'aTopic', + Config::TOPIC => 'aTopic', ], $message->getProperties()); } - public function testShouldDoNothingIfMessageHasProcessorNamePropertySetOnPreReceive() + public function testShouldDoNothingIfMessageHasCommandPropertySetOnPreReceive() { $message = new NullMessage(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'aProcessor'); + $message->setProperty(Config::COMMAND, 'aCommand'); - $context = new Context(new NullContext()); - $context->setPsrMessage($message); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', - ]); + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('createQueue') + ; + + $extension = new ExclusiveCommandExtension($driver); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - self::assertNull($context->getResult()); + self::assertNull($messageReceived->getResult()); $this->assertEquals([ - 'enqueue.processor_name' => 'aProcessor', + Config::COMMAND => 'aCommand', ], $message->getProperties()); } - public function testShouldDoNothingIfMessageHasProcessorQueueNamePropertySetOnPreReceive() + public function testShouldDoNothingIfMessageHasProcessorPropertySetOnPreReceive() { $message = new NullMessage(); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aProcessorQueueName'); + $message->setProperty(Config::PROCESSOR, 'aProcessor'); - $context = new Context(new NullContext()); - $context->setPsrMessage($message); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', - ]); + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('createQueue') + ; + + $extension = new ExclusiveCommandExtension($driver); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - self::assertNull($context->getResult()); + self::assertNull($messageReceived->getResult()); $this->assertEquals([ - 'enqueue.processor_queue_name' => 'aProcessorQueueName', + Config::PROCESSOR => 'aProcessor', ], $message->getProperties()); } - public function testShouldDoNothingIfCurrentQueueIsNotInTheMap() + public function testShouldDoNothingIfCurrentQueueHasNoExclusiveProcessor() { $message = new NullMessage(); $queue = new NullQueue('aBarQueueName'); - $context = new Context(new NullContext()); - $context->setPsrMessage($message); - $context->setPsrQueue($queue); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub($queue), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', - ]); + $extension = new ExclusiveCommandExtension($this->createDriverStub(new RouteCollection([]))); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - self::assertNull($context->getResult()); + self::assertNull($messageReceived->getResult()); $this->assertEquals([], $message->getProperties()); } - public function testShouldSetCommandPropertiesIfCurrentQueueInTheMap() + public function testShouldSetCommandPropertiesIfCurrentQueueHasExclusiveCommandProcessor() { $message = new NullMessage(); - $queue = new NullQueue('aFooQueueName'); - - $context = new Context(new NullContext()); - $context->setPsrMessage($message); - $context->setPsrQueue($queue); - $context->setLogger(new NullLogger()); - - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', + $queue = new NullQueue('fooQueue'); + + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub($queue), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); + + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'theFooProcessor', [ + 'exclusive' => true, + 'queue' => 'fooQueue', + ]), + new Route('barCommand', Route::COMMAND, 'theFooProcessor', [ + 'exclusive' => true, + 'queue' => 'barQueue', + ]), ]); - $extension->onPreReceived($context); + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->any()) + ->method('createRouteQueue') + ->with($this->isInstanceOf(Route::class)) + ->willReturnCallback(function (Route $route) { + return new NullQueue($route->getQueue()); + }) + ; + + $extension = new ExclusiveCommandExtension($driver); + $extension->onMessageReceived($messageReceived); - self::assertNull($context->getResult()); + self::assertNull($messageReceived->getResult()); $this->assertEquals([ - 'enqueue.topic_name' => '__command__', - 'enqueue.processor_queue_name' => 'aFooQueueName', - 'enqueue.processor_name' => 'aFooProcessorName', - 'enqueue.command_name' => 'aFooProcessorName', + Config::PROCESSOR => 'theFooProcessor', + Config::COMMAND => 'fooCommand', ], $message->getProperties()); } - public function testShouldDoNothingOnPreSendIfTopicNotCommandOne() + public function testShouldDoNothingIfAnotherQueue() { - $message = new Message(); - - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', + $message = new NullMessage(); + $queue = new NullQueue('barQueue'); + + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub($queue), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); + + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'theFooProcessor', [ + 'exclusive' => true, + 'queue' => 'fooQueue', + ]), + new Route('barCommand', Route::COMMAND, 'theFooProcessor', [ + 'exclusive' => false, + 'queue' => 'barQueue', + ]), ]); - $extension->onPreSend('aTopic', $message); + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $queueName) { + return new NullQueue($queueName); + }) + ; + + $extension = new ExclusiveCommandExtension($driver); + $extension->onMessageReceived($messageReceived); + + self::assertNull($messageReceived->getResult()); $this->assertEquals([], $message->getProperties()); } - public function testShouldDoNothingIfCommandNotExclusive() + /** + * @return MockObject|DriverInterface + */ + private function createDriverStub(?RouteCollection $routeCollection = null): DriverInterface { - $message = new Message(); - $message->setProperty(Config::PARAMETER_COMMAND_NAME, 'theBarProcessorName'); - - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', - ]); - - $extension->onPreSend(Config::COMMAND_TOPIC, $message); - - $this->assertEquals([ - 'enqueue.command_name' => 'theBarProcessorName', - ], $message->getProperties()); + $driver = $this->createMock(DriverInterface::class); + $driver + ->expects($this->any()) + ->method('getRouteCollection') + ->willReturn($routeCollection ?? new RouteCollection([])) + ; + + return $driver; } - public function testShouldForceExclusiveCommandQueue() + /** + * @return MockObject + */ + private function createContextMock(): InteropContext { - $message = new Message(); - $message->setProperty(Config::PARAMETER_COMMAND_NAME, 'aFooProcessorName'); - - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', - ]); + return $this->createMock(InteropContext::class); + } - $extension->onPreSend(Config::COMMAND_TOPIC, $message); + /** + * @return MockObject + */ + private function createProcessorMock(): Processor + { + return $this->createMock(Processor::class); + } - $this->assertEquals([ - 'enqueue.command_name' => 'aFooProcessorName', - 'enqueue.processor_name' => 'aFooProcessorName', - 'enqueue.processor_queue_name' => 'aFooQueueName', - ], $message->getProperties()); + /** + * @return MockObject|Consumer + */ + private function createConsumerStub(?Queue $queue): Consumer + { + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue ?? new NullQueue('queue')) + ; + + return $consumerMock; } } diff --git a/Tests/Client/ConsumptionExtension/FlushSpoolProducerExtensionTest.php b/Tests/Client/ConsumptionExtension/FlushSpoolProducerExtensionTest.php index 1469c99..6a782c5 100644 --- a/Tests/Client/ConsumptionExtension/FlushSpoolProducerExtensionTest.php +++ b/Tests/Client/ConsumptionExtension/FlushSpoolProducerExtensionTest.php @@ -4,86 +4,33 @@ use Enqueue\Client\ConsumptionExtension\FlushSpoolProducerExtension; use Enqueue\Client\SpoolProducer; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\End; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\EndExtensionInterface; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; use Enqueue\Test\ClassExtensionTrait; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Message; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; class FlushSpoolProducerExtensionTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementExtensionInterface() + public function testShouldImplementPostMessageReceivedExtensionInterface() { - $this->assertClassImplements(ExtensionInterface::class, FlushSpoolProducerExtension::class); + $this->assertClassImplements(PostMessageReceivedExtensionInterface::class, FlushSpoolProducerExtension::class); } - public function testCouldBeConstructedWithSpoolProducerAsFirstArgument() + public function testShouldImplementEndExtensionInterface() { - new FlushSpoolProducerExtension($this->createSpoolProducerMock()); + $this->assertClassImplements(EndExtensionInterface::class, FlushSpoolProducerExtension::class); } - public function testShouldDoNothingOnStart() - { - $producer = $this->createSpoolProducerMock(); - $producer - ->expects(self::never()) - ->method('flush') - ; - - $extension = new FlushSpoolProducerExtension($producer); - $extension->onStart($this->createContextMock()); - } - - public function testShouldDoNothingOnBeforeReceive() - { - $producer = $this->createSpoolProducerMock(); - $producer - ->expects(self::never()) - ->method('flush') - ; - - $extension = new FlushSpoolProducerExtension($producer); - $extension->onBeforeReceive($this->createContextMock()); - } - - public function testShouldDoNothingOnPreReceived() - { - $producer = $this->createSpoolProducerMock(); - $producer - ->expects(self::never()) - ->method('flush') - ; - - $extension = new FlushSpoolProducerExtension($producer); - $extension->onPreReceived($this->createContextMock()); - } - - public function testShouldDoNothingOnResult() - { - $producer = $this->createSpoolProducerMock(); - $producer - ->expects(self::never()) - ->method('flush') - ; - - $extension = new FlushSpoolProducerExtension($producer); - $extension->onResult($this->createContextMock()); - } - - public function testShouldDoNothingOnIdle() - { - $producer = $this->createSpoolProducerMock(); - $producer - ->expects(self::never()) - ->method('flush') - ; - - $extension = new FlushSpoolProducerExtension($producer); - $extension->onIdle($this->createContextMock()); - } - - public function testShouldFlushSpoolProducerOnInterrupted() + public function testShouldFlushSpoolProducerOnEnd() { $producer = $this->createSpoolProducerMock(); $producer @@ -91,8 +38,10 @@ public function testShouldFlushSpoolProducerOnInterrupted() ->method('flush') ; + $end = new End($this->createInteropContextMock(), 1, 2, new NullLogger()); + $extension = new FlushSpoolProducerExtension($producer); - $extension->onInterrupted($this->createContextMock()); + $extension->onEnd($end); } public function testShouldFlushSpoolProducerOnPostReceived() @@ -103,20 +52,29 @@ public function testShouldFlushSpoolProducerOnPostReceived() ->method('flush') ; + $context = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() + ); + $extension = new FlushSpoolProducerExtension($producer); - $extension->onPostReceived($this->createContextMock()); + $extension->onPostMessageReceived($context); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Context + * @return MockObject */ - private function createContextMock() + private function createInteropContextMock(): Context { return $this->createMock(Context::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|SpoolProducer + * @return MockObject|SpoolProducer */ private function createSpoolProducerMock() { diff --git a/Tests/Client/ConsumptionExtension/LogExtensionTest.php b/Tests/Client/ConsumptionExtension/LogExtensionTest.php new file mode 100644 index 0000000..db75767 --- /dev/null +++ b/Tests/Client/ConsumptionExtension/LogExtensionTest.php @@ -0,0 +1,536 @@ +assertClassImplements(StartExtensionInterface::class, LogExtension::class); + } + + public function testShouldImplementEndExtensionInterface() + { + $this->assertClassImplements(EndExtensionInterface::class, LogExtension::class); + } + + public function testShouldImplementMessageReceivedExtensionInterface() + { + $this->assertClassImplements(MessageReceivedExtensionInterface::class, LogExtension::class); + } + + public function testShouldImplementPostMessageReceivedExtensionInterface() + { + $this->assertClassImplements(PostMessageReceivedExtensionInterface::class, LogExtension::class); + } + + public function testShouldSubClassOfLogExtension() + { + $this->assertClassExtends(\Enqueue\Consumption\Extension\LogExtension::class, LogExtension::class); + } + + public function testShouldLogStartOnStart() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('Consumption has started') + ; + + $context = new Start($this->createContextMock(), $logger, [], 1, 1); + + $extension = new LogExtension(); + $extension->onStart($context); + } + + public function testShouldLogEndOnEnd() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('Consumption has ended') + ; + + $context = new End($this->createContextMock(), 1, 2, $logger); + + $extension = new LogExtension(); + $extension->onEnd($context); + } + + public function testShouldLogMessageReceived() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('Received from {queueName} {body}', [ + 'queueName' => 'aQueue', + 'redelivered' => false, + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + ]) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new MessageReceived($this->createContextMock(), $consumerMock, $message, $this->createProcessorMock(), 1, $logger); + + $extension = new LogExtension(); + $extension->onMessageReceived($context); + } + + public function testShouldLogMessageProcessedWithStringResult() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + 'Processed from {queueName} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'aResult', + 'reason' => '', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, 'aResult', 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogRejectedMessageAsError() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::ERROR, + 'Processed from {queueName} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'reject', + 'reason' => '', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Processor::REJECT, 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogMessageProcessedWithResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + 'Processed from {queueName} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => '', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack(), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogMessageProcessedWithReasonResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + 'Processed from {queueName} {body} {result} {reason}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => 'aReason', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack('aReason'), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogProcessedCommandMessageWithStringResult() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + '[client] Processed {command} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::COMMAND => 'aCommand']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'aResult', + 'reason' => '', + 'command' => 'aCommand', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty(Config::COMMAND, 'aCommand'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, 'aResult', 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogRejectedCommandMessageAsError() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::ERROR, + '[client] Processed {command} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::COMMAND => 'aCommand']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'reject', + 'reason' => '', + 'command' => 'aCommand', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setProperty(Config::COMMAND, 'aCommand'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Processor::REJECT, 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogProcessedCommandMessageWithResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + '[client] Processed {command} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::COMMAND => 'aCommand']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => '', + 'command' => 'aCommand', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setProperty(Config::COMMAND, 'aCommand'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack(), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogProcessedCommandMessageWithReasonResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + '[client] Processed {command} {body} {result} {reason}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::COMMAND => 'aCommand']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => 'aReason', + 'command' => 'aCommand', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setProperty(Config::COMMAND, 'aCommand'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack('aReason'), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogProcessedTopicProcessorMessageWithStringResult() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + '[client] Processed {topic} -> {processor} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::TOPIC => 'aTopic', Config::PROCESSOR => 'aProcessor']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'aResult', + 'reason' => '', + 'topic' => 'aTopic', + 'processor' => 'aProcessor', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty(Config::TOPIC, 'aTopic'); + $message->setProperty(Config::PROCESSOR, 'aProcessor'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, 'aResult', 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogRejectedTopicProcessorMessageAsError() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::ERROR, + '[client] Processed {topic} -> {processor} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::TOPIC => 'aTopic', Config::PROCESSOR => 'aProcessor']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'reject', + 'reason' => '', + 'topic' => 'aTopic', + 'processor' => 'aProcessor', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty(Config::TOPIC, 'aTopic'); + $message->setProperty(Config::PROCESSOR, 'aProcessor'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Processor::REJECT, 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogProcessedTopicProcessorMessageWithResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + '[client] Processed {topic} -> {processor} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::TOPIC => 'aTopic', Config::PROCESSOR => 'aProcessor']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => '', + 'topic' => 'aTopic', + 'processor' => 'aProcessor', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty(Config::TOPIC, 'aTopic'); + $message->setProperty(Config::PROCESSOR, 'aProcessor'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack(), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogProcessedTopicProcessorMessageWithReasonResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + '[client] Processed {topic} -> {processor} {body} {result} {reason}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::TOPIC => 'aTopic', Config::PROCESSOR => 'aProcessor']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => 'aReason', + 'topic' => 'aTopic', + 'processor' => 'aProcessor', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty(Config::TOPIC, 'aTopic'); + $message->setProperty(Config::PROCESSOR, 'aProcessor'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack('aReason'), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + /** + * @return MockObject + */ + private function createConsumerStub(Queue $queue): Consumer + { + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue) + ; + + return $consumerMock; + } + + /** + * @return MockObject + */ + private function createContextMock(): Context + { + return $this->createMock(Context::class); + } + + /** + * @return MockObject + */ + private function createProcessorMock(): Processor + { + return $this->createMock(Processor::class); + } + + /** + * @return MockObject|LoggerInterface + */ + private function createLogger() + { + return $this->createMock(LoggerInterface::class); + } +} diff --git a/Tests/Client/ConsumptionExtension/SetRouterPropertiesExtensionTest.php b/Tests/Client/ConsumptionExtension/SetRouterPropertiesExtensionTest.php index 6dc723c..d521aef 100644 --- a/Tests/Client/ConsumptionExtension/SetRouterPropertiesExtensionTest.php +++ b/Tests/Client/ConsumptionExtension/SetRouterPropertiesExtensionTest.php @@ -5,31 +5,31 @@ use Enqueue\Client\Config; use Enqueue\Client\ConsumptionExtension\SetRouterPropertiesExtension; use Enqueue\Client\DriverInterface; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\MessageReceivedExtensionInterface; use Enqueue\Null\NullMessage; use Enqueue\Null\NullQueue; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrContext; +use Interop\Queue\Consumer; +use Interop\Queue\Context as InteropContext; +use Interop\Queue\Processor; +use Interop\Queue\Queue; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; class SetRouterPropertiesExtensionTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementExtensionInterface() + public function testShouldImplementMessageReceivedExtensionInterface() { - $this->assertClassImplements(ExtensionInterface::class, SetRouterPropertiesExtension::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new SetRouterPropertiesExtension($this->createDriverMock()); + $this->assertClassImplements(MessageReceivedExtensionInterface::class, SetRouterPropertiesExtension::class); } public function testShouldSetRouterProcessorPropertyIfNotSetAndOnRouterQueue() { - $config = Config::create('test', '', '', 'router-queue', '', 'router-processor-name'); + $config = Config::create('test', '.', '', '', 'router-queue', '', 'router-processor-name'); $queue = new NullQueue('test.router-queue'); $driver = $this->createDriverMock(); @@ -46,17 +46,23 @@ public function testShouldSetRouterProcessorPropertyIfNotSetAndOnRouterQueue() ; $message = new NullMessage(); + $message->setProperty(Config::TOPIC, 'aTopic'); - $context = new Context($this->createPsrContextMock()); - $context->setPsrMessage($message); - $context->setPsrQueue(new NullQueue('test.router-queue')); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(new NullQueue('test.router-queue')), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); $extension = new SetRouterPropertiesExtension($driver); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); $this->assertEquals([ - 'enqueue.processor_name' => 'router-processor-name', - 'enqueue.processor_queue_name' => 'router-queue', + Config::PROCESSOR => 'router-processor-name', + Config::TOPIC => 'aTopic', ], $message->getProperties()); } @@ -79,15 +85,23 @@ public function testShouldNotSetRouterProcessorPropertyIfNotSetAndNotOnRouterQue ; $message = new NullMessage(); + $message->setProperty(Config::TOPIC, 'aTopic'); - $context = new Context($this->createPsrContextMock()); - $context->setPsrMessage($message); - $context->setPsrQueue(new NullQueue('test.another-queue')); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(new NullQueue('test.another-queue')), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); $extension = new SetRouterPropertiesExtension($driver); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - $this->assertEquals([], $message->getProperties()); + $this->assertEquals([ + Config::TOPIC => 'aTopic', + ], $message->getProperties()); } public function testShouldNotSetAnyPropertyIfProcessorNamePropertyAlreadySet() @@ -99,32 +113,86 @@ public function testShouldNotSetAnyPropertyIfProcessorNamePropertyAlreadySet() ; $message = new NullMessage(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'non-router-processor'); + $message->setProperty(Config::PROCESSOR, 'non-router-processor'); - $context = new Context($this->createPsrContextMock()); - $context->setPsrMessage($message); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); $extension = new SetRouterPropertiesExtension($driver); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); $this->assertEquals([ - 'enqueue.processor_name' => 'non-router-processor', + 'enqueue.processor' => 'non-router-processor', ], $message->getProperties()); } + public function testShouldSkipMessagesWithoutTopicPropertySet() + { + $driver = $this->createDriverMock(); + $driver + ->expects($this->never()) + ->method('getConfig') + ; + + $message = new NullMessage(); + + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); + + $extension = new SetRouterPropertiesExtension($driver); + $extension->onMessageReceived($messageReceived); + + $this->assertEquals([], $message->getProperties()); + } + /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject|InteropContext */ - protected function createPsrContextMock() + protected function createContextMock(): InteropContext { - return $this->createMock(PsrContext::class); + return $this->createMock(InteropContext::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface + * @return MockObject|DriverInterface */ - protected function createDriverMock() + protected function createDriverMock(): DriverInterface { return $this->createMock(DriverInterface::class); } + + /** + * @return MockObject + */ + private function createProcessorMock(): Processor + { + return $this->createMock(Processor::class); + } + + /** + * @return MockObject|Consumer + */ + private function createConsumerStub(?Queue $queue): Consumer + { + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue ?? new NullQueue('queue')) + ; + + return $consumerMock; + } } diff --git a/Tests/Client/ConsumptionExtension/SetupBrokerExtensionTest.php b/Tests/Client/ConsumptionExtension/SetupBrokerExtensionTest.php index 21b011f..fbd3679 100644 --- a/Tests/Client/ConsumptionExtension/SetupBrokerExtensionTest.php +++ b/Tests/Client/ConsumptionExtension/SetupBrokerExtensionTest.php @@ -4,10 +4,11 @@ use Enqueue\Client\ConsumptionExtension\SetupBrokerExtension; use Enqueue\Client\DriverInterface; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\Start; +use Enqueue\Consumption\StartExtensionInterface; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrContext; +use Interop\Queue\Context as InteropContext; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; @@ -15,14 +16,9 @@ class SetupBrokerExtensionTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementExtensionInterface() + public function testShouldImplementStartExtensionInterface() { - $this->assertClassImplements(ExtensionInterface::class, SetupBrokerExtension::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new SetupBrokerExtension($this->createDriverMock()); + $this->assertClassImplements(StartExtensionInterface::class, SetupBrokerExtension::class); } public function testShouldSetupBroker() @@ -36,8 +32,7 @@ public function testShouldSetupBroker() ->with($this->identicalTo($logger)) ; - $context = new Context($this->createMock(PsrContext::class)); - $context->setLogger($logger); + $context = new Start($this->createMock(InteropContext::class), $logger, [], 0, 0); $extension = new SetupBrokerExtension($driver); $extension->onStart($context); @@ -54,8 +49,7 @@ public function testShouldSetupBrokerOnlyOnce() ->with($this->identicalTo($logger)) ; - $context = new Context($this->createMock(PsrContext::class)); - $context->setLogger($logger); + $context = new Start($this->createMock(InteropContext::class), $logger, [], 0, 0); $extension = new SetupBrokerExtension($driver); $extension->onStart($context); @@ -63,7 +57,7 @@ public function testShouldSetupBrokerOnlyOnce() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface + * @return MockObject|DriverInterface */ private function createDriverMock() { diff --git a/Tests/Client/DelegateProcessorTest.php b/Tests/Client/DelegateProcessorTest.php index f107dde..9743cf4 100644 --- a/Tests/Client/DelegateProcessorTest.php +++ b/Tests/Client/DelegateProcessorTest.php @@ -4,36 +4,30 @@ use Enqueue\Client\Config; use Enqueue\Client\DelegateProcessor; -use Enqueue\Client\ProcessorRegistryInterface; use Enqueue\Null\NullMessage; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; +use Enqueue\ProcessorRegistryInterface; +use Interop\Queue\Context; +use Interop\Queue\Processor; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class DelegateProcessorTest extends TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new DelegateProcessor($this->createProcessorRegistryMock()); - } - public function testShouldThrowExceptionIfProcessorNameIsNotSet() { - $this->setExpectedException( - \LogicException::class, - 'Got message without required parameter: "enqueue.processor_name"' - ); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Got message without required parameter: "enqueue.processor"'); $processor = new DelegateProcessor($this->createProcessorRegistryMock()); - $processor->process(new NullMessage(), $this->createPsrContextMock()); + $processor->process(new NullMessage(), $this->createContextMock()); } public function testShouldProcessMessage() { - $session = $this->createPsrContextMock(); + $session = $this->createContextMock(); $message = new NullMessage(); $message->setProperties([ - Config::PARAMETER_PROCESSOR_NAME => 'processor-name', + Config::PROCESSOR => 'processor-name', ]); $processor = $this->createProcessorMock(); @@ -41,7 +35,7 @@ public function testShouldProcessMessage() ->expects($this->once()) ->method('process') ->with($this->identicalTo($message), $this->identicalTo($session)) - ->will($this->returnValue('return-value')) + ->willReturn('return-value') ; $processorRegistry = $this->createProcessorRegistryMock(); @@ -49,7 +43,7 @@ public function testShouldProcessMessage() ->expects($this->once()) ->method('get') ->with('processor-name') - ->will($this->returnValue($processor)) + ->willReturn($processor) ; $processor = new DelegateProcessor($processorRegistry); @@ -59,7 +53,7 @@ public function testShouldProcessMessage() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ProcessorRegistryInterface + * @return MockObject|ProcessorRegistryInterface */ protected function createProcessorRegistryMock() { @@ -67,18 +61,18 @@ protected function createProcessorRegistryMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject|Context */ - protected function createPsrContextMock() + protected function createContextMock() { - return $this->createMock(PsrContext::class); + return $this->createMock(Context::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor + * @return MockObject|Processor */ protected function createProcessorMock() { - return $this->createMock(PsrProcessor::class); + return $this->createMock(Processor::class); } } diff --git a/Tests/Client/Driver/AmqpDriverTest.php b/Tests/Client/Driver/AmqpDriverTest.php new file mode 100644 index 0000000..2cfb170 --- /dev/null +++ b/Tests/Client/Driver/AmqpDriverTest.php @@ -0,0 +1,360 @@ +assertClassImplements(DriverInterface::class, AmqpDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, AmqpDriver::class); + } + + public function testThrowIfPriorityIsNotSupportedOnCreateTransportMessage() + { + $clientMessage = new Message(); + $clientMessage->setPriority('invalidPriority'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($this->createMessage()) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Cant convert client priority "invalidPriority" to transport one. Could be one of "enqueue.message_queue.client.very_low_message_priority", "enqueue.message_queue.client.low_message_priority", "enqueue.message_queue.client.normal_message_priority'); + $driver->createTransportMessage($clientMessage); + } + + public function testShouldSetExpirationHeaderFromClientMessageExpireInMillisecondsOnCreateTransportMessage() + { + $clientMessage = new Message(); + $clientMessage->setExpire(333); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($this->createMessage()) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + /** @var AmqpMessage $transportMessage */ + $transportMessage = $driver->createTransportMessage($clientMessage); + + $this->assertSame(333000, $transportMessage->getExpiration()); + $this->assertSame('333000', $transportMessage->getHeader('expiration')); + } + + public function testShouldSetPersistedDeliveryModeOnCreateTransportMessage() + { + $clientMessage = new Message(); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($this->createMessage()) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + /** @var AmqpMessage $transportMessage */ + $transportMessage = $driver->createTransportMessage($clientMessage); + + $this->assertSame(AmqpMessage::DELIVERY_MODE_PERSISTENT, $transportMessage->getDeliveryMode()); + } + + public function testShouldCreateDurableQueue() + { + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($this->createQueue('aName')) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + /** @var AmqpQueue $queue */ + $queue = $driver->createQueue('aName'); + + $this->assertSame(AmqpQueue::FLAG_DURABLE, $queue->getFlags()); + } + + public function testShouldResetPriorityAndExpirationAndNeverCallProducerDeliveryDelayOnSendMessageToRouter() + { + $topic = $this->createTopic(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) + ; + $producer + ->expects($this->never()) + ->method('setDeliveryDelay') + ; + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createTopic') + ->willReturn($topic) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setExpire(123); + $message->setPriority(MessagePriority::HIGH); + + $driver->sendToRouter($message); + + $this->assertNull($transportMessage->getExpiration()); + $this->assertNull($transportMessage->getPriority()); + } + + public function testShouldSetupBroker() + { + $routerTopic = $this->createTopic(''); + $routerQueue = $this->createQueue(''); + $processorWithDefaultQueue = $this->createQueue('default'); + $processorWithCustomQueue = $this->createQueue('custom'); + $context = $this->createContextMock(); + // setup router + $context + ->expects($this->at(0)) + ->method('createTopic') + ->willReturn($routerTopic) + ; + $context + ->expects($this->at(1)) + ->method('declareTopic') + ->with($this->identicalTo($routerTopic)) + ; + + $context + ->expects($this->at(2)) + ->method('createQueue') + ->willReturn($routerQueue) + ; + $context + ->expects($this->at(3)) + ->method('declareQueue') + ->with($this->identicalTo($routerQueue)) + ; + + $context + ->expects($this->at(4)) + ->method('bind') + ->with($this->isInstanceOf(AmqpBind::class)) + ; + + // setup processor with default queue + $context + ->expects($this->at(5)) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($processorWithDefaultQueue) + ; + $context + ->expects($this->at(6)) + ->method('declareQueue') + ->with($this->identicalTo($processorWithDefaultQueue)) + ; + + $context + ->expects($this->at(7)) + ->method('createQueue') + ->with($this->getCustomQueueTransportName()) + ->willReturn($processorWithCustomQueue) + ; + $context + ->expects($this->at(8)) + ->method('declareQueue') + ->with($this->identicalTo($processorWithCustomQueue)) + ; + + $driver = new AmqpDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('aTopic', Route::TOPIC, 'aProcessor'), + new Route('aCommand', Route::COMMAND, 'aProcessor', ['queue' => 'custom']), + ]) + ); + $driver->setupBroker(); + } + + public function testShouldNotDeclareSameQueues() + { + $context = $this->createContextMock(); + + // setup processor with default queue + $context + ->expects($this->any()) + ->method('createTopic') + ->willReturn($this->createTopic('')) + ; + $context + ->expects($this->any()) + ->method('createQueue') + ->willReturn($this->createQueue('custom')) + ; + $context + ->expects($this->exactly(2)) + ->method('declareQueue') + ; + + $driver = new AmqpDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('aTopic', Route::TOPIC, 'aProcessor', ['queue' => 'custom']), + new Route('aCommand', Route::COMMAND, 'aProcessor', ['queue' => 'custom']), + ]) + ); + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new AmqpDriver(...$args); + } + + /** + * @return AmqpContext + */ + protected function createContextMock(): Context + { + return $this->createMock(AmqpContext::class); + } + + /** + * @return AmqpProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(AmqpProducer::class); + } + + /** + * @return AmqpQueue + */ + protected function createQueue(string $name): InteropQueue + { + return new AmqpQueue($name); + } + + protected function createTopic(string $name): AmqpTopic + { + return new AmqpTopic($name); + } + + /** + * @return AmqpMessage + */ + protected function createMessage(): InteropMessage + { + return new AmqpMessage(); + } + + protected function getRouterTransportName(): string + { + return 'aprefix.router'; + } + + protected function assertTransportMessage(InteropMessage $transportMessage): void + { + $this->assertSame('body', $transportMessage->getBody()); + Assert::assertArraySubset([ + 'hkey' => 'hval', + 'delivery_mode' => AmqpMessage::DELIVERY_MODE_PERSISTENT, + 'content_type' => 'ContentType', + 'expiration' => '123000', + 'priority' => 3, + 'message_id' => 'theMessageId', + 'timestamp' => 1000, + 'reply_to' => 'theReplyTo', + 'correlation_id' => 'theCorrelationId', + ], $transportMessage->getHeaders()); + $this->assertEquals([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'ContentType', + Config::PRIORITY => MessagePriority::HIGH, + Config::EXPIRE => 123, + Config::DELAY => 345, + ], $transportMessage->getProperties()); + $this->assertSame('theMessageId', $transportMessage->getMessageId()); + $this->assertSame(1000, $transportMessage->getTimestamp()); + $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); + } +} diff --git a/Tests/Client/Driver/DbalDriverTest.php b/Tests/Client/Driver/DbalDriverTest.php new file mode 100644 index 0000000..554a399 --- /dev/null +++ b/Tests/Client/Driver/DbalDriverTest.php @@ -0,0 +1,101 @@ +assertClassImplements(DriverInterface::class, DbalDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, DbalDriver::class); + } + + public function testShouldSetupBroker() + { + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getTableName') + ; + $context + ->expects($this->once()) + ->method('createDataBaseTable') + ; + + $driver = new DbalDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new DbalDriver(...$args); + } + + /** + * @return DbalContext + */ + protected function createContextMock(): Context + { + return $this->createMock(DbalContext::class); + } + + /** + * @return DbalProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(DbalProducer::class); + } + + /** + * @return DbalDestination + */ + protected function createQueue(string $name): InteropQueue + { + return new DbalDestination($name); + } + + /** + * @return DbalDestination + */ + protected function createTopic(string $name): InteropTopic + { + return new DbalDestination($name); + } + + /** + * @return DbalMessage + */ + protected function createMessage(): InteropMessage + { + return new DbalMessage(); + } +} diff --git a/Tests/Client/Driver/FsDriverTest.php b/Tests/Client/Driver/FsDriverTest.php new file mode 100644 index 0000000..f1cd02f --- /dev/null +++ b/Tests/Client/Driver/FsDriverTest.php @@ -0,0 +1,125 @@ +assertClassImplements(DriverInterface::class, FsDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, FsDriver::class); + } + + public function testShouldSetupBroker() + { + $routerQueue = new FsDestination(TempFile::generate()); + + $processorQueue = new FsDestination(TempFile::generate()); + + $context = $this->createContextMock(); + // setup router + $context + ->expects($this->at(0)) + ->method('createQueue') + ->willReturn($routerQueue) + ; + $context + ->expects($this->at(1)) + ->method('declareDestination') + ->with($this->identicalTo($routerQueue)) + ; + // setup processor queue + $context + ->expects($this->at(2)) + ->method('createQueue') + ->willReturn($processorQueue) + ; + $context + ->expects($this->at(3)) + ->method('declareDestination') + ->with($this->identicalTo($processorQueue)) + ; + + $routeCollection = new RouteCollection([ + new Route('aTopic', Route::TOPIC, 'aProcessor'), + ]); + + $driver = new FsDriver( + $context, + $this->createDummyConfig(), + $routeCollection + ); + + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new FsDriver(...$args); + } + + /** + * @return FsContext + */ + protected function createContextMock(): Context + { + return $this->createMock(FsContext::class); + } + + /** + * @return FsProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(FsProducer::class); + } + + /** + * @return FsDestination + */ + protected function createQueue(string $name): InteropQueue + { + return new FsDestination(new \SplFileInfo($name)); + } + + /** + * @return FsDestination + */ + protected function createTopic(string $name): InteropTopic + { + return new FsDestination(new \SplFileInfo($name)); + } + + /** + * @return FsMessage + */ + protected function createMessage(): InteropMessage + { + return new FsMessage(); + } +} diff --git a/Tests/Client/Driver/GenericDriverTest.php b/Tests/Client/Driver/GenericDriverTest.php new file mode 100644 index 0000000..78f7f6e --- /dev/null +++ b/Tests/Client/Driver/GenericDriverTest.php @@ -0,0 +1,83 @@ +assertClassImplements(DriverInterface::class, GenericDriver::class); + } + + protected function createDriver(...$args): DriverInterface + { + return new GenericDriver(...$args); + } + + protected function createContextMock(): Context + { + return $this->createMock(Context::class); + } + + protected function createProducerMock(): InteropProducer + { + return $this->createMock(InteropProducer::class); + } + + protected function createQueue(string $name): InteropQueue + { + return new NullQueue($name); + } + + protected function createTopic(string $name): InteropTopic + { + return new NullTopic($name); + } + + protected function createMessage(): InteropMessage + { + return new NullMessage(); + } + + protected function assertTransportMessage(InteropMessage $transportMessage): void + { + $this->assertSame('body', $transportMessage->getBody()); + Assert::assertArraySubset([ + 'hkey' => 'hval', + 'message_id' => 'theMessageId', + 'timestamp' => 1000, + 'reply_to' => 'theReplyTo', + 'correlation_id' => 'theCorrelationId', + ], $transportMessage->getHeaders()); + $this->assertEquals([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'ContentType', + Config::PRIORITY => MessagePriority::HIGH, + Config::EXPIRE => 123, + Config::DELAY => 345, + ], $transportMessage->getProperties()); + $this->assertSame('theMessageId', $transportMessage->getMessageId()); + $this->assertSame(1000, $transportMessage->getTimestamp()); + $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); + } +} diff --git a/Tests/Client/Driver/GenericDriverTestsTrait.php b/Tests/Client/Driver/GenericDriverTestsTrait.php new file mode 100644 index 0000000..d5ad498 --- /dev/null +++ b/Tests/Client/Driver/GenericDriverTestsTrait.php @@ -0,0 +1,1249 @@ +createDriver( + $this->createContextMock(), + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $this->assertInstanceOf(DriverInterface::class, $driver); + } + + public function testShouldReturnContextSetInConstructor() + { + $context = $this->createContextMock(); + + $driver = $this->createDriver($context, $this->createDummyConfig(), new RouteCollection([])); + + $this->assertSame($context, $driver->getContext()); + } + + public function testShouldReturnConfigObjectSetInConstructor() + { + $config = $this->createDummyConfig(); + + $driver = $this->createDriver($this->createContextMock(), $config, new RouteCollection([])); + + $this->assertSame($config, $driver->getConfig()); + } + + public function testShouldReturnRouteCollectionSetInConstructor() + { + $routeCollection = new RouteCollection([]); + + /** @var DriverInterface $driver */ + $driver = $this->createDriver($this->createContextMock(), $this->createDummyConfig(), $routeCollection); + + $this->assertSame($routeCollection, $driver->getRouteCollection()); + } + + public function testShouldCreateAndReturnQueueInstanceWithPrefixAndAppName() + { + $expectedQueue = $this->createQueue('aName'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getPrefixAppFooQueueTransportName()) + ->willReturn($expectedQueue) + ; + + $config = new Config( + 'aPrefix', + '.', + 'anAppName', + 'aRouterTopicName', + 'aRouterQueueName', + 'aDefaultQueue', + 'aRouterProcessor', + [], + [] + ); + + $driver = $this->createDriver($context, $config, new RouteCollection([])); + + $queue = $driver->createQueue('aFooQueue'); + + $this->assertSame($expectedQueue, $queue); + } + + public function testShouldCreateAndReturnQueueInstanceWithPrefixWithoutAppName() + { + $expectedQueue = $this->createQueue('aName'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getPrefixFooQueueTransportName()) + ->willReturn($expectedQueue) + ; + + $config = new Config( + 'aPrefix', + '.', + '', + 'aRouterTopicName', + 'aRouterQueueName', + 'aDefaultQueue', + 'aRouterProcessor', + [], + [] + ); + + $driver = $this->createDriver($context, $config, new RouteCollection([])); + + $queue = $driver->createQueue('aFooQueue'); + + $this->assertSame($expectedQueue, $queue); + } + + public function testShouldCreateAndReturnQueueInstanceWithAppNameAndWithoutPrefix() + { + $expectedQueue = $this->createQueue('aName'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getAppFooQueueTransportName()) + ->willReturn($expectedQueue) + ; + + $config = new Config( + '', + '.', + 'anAppName', + 'aRouterTopicName', + 'aRouterQueueName', + 'aDefaultQueue', + 'aRouterProcessor', + [], + [] + ); + + $driver = $this->createDriver($context, $config, new RouteCollection([])); + + $queue = $driver->createQueue('aFooQueue'); + + $this->assertSame($expectedQueue, $queue); + } + + public function testShouldCreateAndReturnQueueInstanceWithoutPrefixAndAppName() + { + $expectedQueue = $this->createQueue('aName'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with('afooqueue') + ->willReturn($expectedQueue) + ; + + $config = new Config( + '', + '.', + '', + 'aRouterTopicName', + 'aRouterQueueName', + 'aDefaultQueue', + 'aRouterProcessor', + [], + [] + ); + + $driver = $this->createDriver($context, $config, new RouteCollection([])); + + $queue = $driver->createQueue('aFooQueue'); + + $this->assertSame($expectedQueue, $queue); + } + + public function testShouldCreateAndReturnQueueInstance() + { + $expectedQueue = $this->createQueue('aName'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getPrefixFooQueueTransportName()) + ->willReturn($expectedQueue) + ; + + $driver = $this->createDriver($context, $this->createDummyConfig(), new RouteCollection([])); + + $queue = $driver->createQueue('aFooQueue'); + + $this->assertSame($expectedQueue, $queue); + } + + public function testShouldCreateClientMessageFromTransportOne() + { + $transportMessage = $this->createMessage(); + $transportMessage->setBody('body'); + $transportMessage->setHeaders(['hkey' => 'hval']); + $transportMessage->setProperty('pkey', 'pval'); + $transportMessage->setProperty(Config::CONTENT_TYPE, 'theContentType'); + $transportMessage->setProperty(Config::EXPIRE, '22'); + $transportMessage->setProperty(Config::PRIORITY, MessagePriority::HIGH); + $transportMessage->setProperty('enqueue.delay', '44'); + $transportMessage->setMessageId('theMessageId'); + $transportMessage->setTimestamp(1000); + $transportMessage->setReplyTo('theReplyTo'); + $transportMessage->setCorrelationId('theCorrelationId'); + + $driver = $this->createDriver( + $this->createContextMock(), + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $clientMessage = $driver->createClientMessage($transportMessage); + + $this->assertClientMessage($clientMessage); + } + + public function testShouldCreateTransportMessageFromClientOne() + { + $clientMessage = new Message(); + $clientMessage->setBody('body'); + $clientMessage->setHeaders(['hkey' => 'hval']); + $clientMessage->setProperties(['pkey' => 'pval']); + $clientMessage->setContentType('ContentType'); + $clientMessage->setExpire(123); + $clientMessage->setDelay(345); + $clientMessage->setPriority(MessagePriority::HIGH); + $clientMessage->setMessageId('theMessageId'); + $clientMessage->setTimestamp(1000); + $clientMessage->setReplyTo('theReplyTo'); + $clientMessage->setCorrelationId('theCorrelationId'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($this->createMessage()) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $transportMessage = $driver->createTransportMessage($clientMessage); + + $this->assertTransportMessage($transportMessage); + } + + public function testShouldSendMessageToRouter() + { + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->willReturnCallback(function (Destination $topic, InteropMessage $message) use ($transportMessage) { + $this->assertSame( + $this->getRouterTransportName(), + $topic instanceof InteropTopic ? $topic->getTopicName() : $topic->getQueueName()); + $this->assertSame($transportMessage, $message); + }) + ; + $context = $this->createContextStub(); + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + + $driver->sendToRouter($message); + } + + public function testShouldNotInitDeliveryDelayOnSendMessageToRouter() + { + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ; + $producer + ->expects($this->never()) + ->method('setDeliveryDelay') + ; + + $context = $this->createContextStub(); + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setDelay(456); + $message->setProperty(Config::TOPIC, 'topic'); + + $driver->sendToRouter($message); + } + + public function testShouldNotInitTimeToLiveOnSendMessageToRouter() + { + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ; + $producer + ->expects($this->never()) + ->method('setTimeToLive') + ; + + $context = $this->createContextStub(); + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setExpire(456); + $message->setProperty(Config::TOPIC, 'topic'); + + $driver->sendToRouter($message); + } + + public function testShouldNotInitPriorityOnSendMessageToRouter() + { + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ; + $producer + ->expects($this->never()) + ->method('setPriority') + ; + + $context = $this->createContextStub(); + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setPriority(MessagePriority::HIGH); + $message->setProperty(Config::TOPIC, 'topic'); + + $driver->sendToRouter($message); + } + + public function testThrowIfTopicIsNotSetOnSendToRouter() + { + $driver = $this->createDriver( + $this->createContextMock(), + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Topic name parameter is required but is not set'); + + $driver->sendToRouter(new Message()); + } + + public function testThrowIfCommandSetOnSendToRouter() + { + $driver = $this->createDriver( + $this->createContextMock(), + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'aCommand'); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Command must not be send to router but go directly to its processor.'); + + $driver->sendToRouter($message); + } + + public function testShouldSendMessageToRouterProcessor() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $config = $this->createDummyConfig(); + + $driver = $this->createDriver( + $context, + $config, + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor', [ + 'queue' => 'custom', + ]), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, $config->getRouterProcessor()); + + $driver->sendToProcessor($message); + } + + public function testShouldSendTopicMessageToProcessorToDefaultQueue() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testShouldSendTopicMessageToProcessorToCustomQueue() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getCustomQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor', ['queue' => 'custom']), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testShouldInitDeliveryDelayIfDelayPropertyOnSendToProcessor() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('setDeliveryDelay') + ->with(456000) + ; + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]) + ); + + $message = new Message(); + $message->setDelay(456); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testShouldSetInitTimeToLiveIfExpirePropertyOnSendToProcessor() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('setTimeToLive') + ->with(678000) + ; + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]) + ); + + $message = new Message(); + $message->setExpire(678); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testShouldSetInitPriorityIfPriorityPropertyOnSendToProcessor() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('setPriority') + ->with(3) + ; + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]) + ); + + $message = new Message(); + $message->setPriority(MessagePriority::HIGH); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testThrowIfNoRouteFoundForTopicMessageOnSendToProcessor() + { + $context = $this->createContextMock(); + $context + ->expects($this->never()) + ->method('createProducer') + ; + $context + ->expects($this->never()) + ->method('createMessage') + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('There is no route for topic "topic" and processor "processor"'); + $driver->sendToProcessor($message); + } + + public function testShouldSetRouterProcessorIfProcessorPropertyEmptyOnSendToProcessor() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'expectedProcessor'), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + + $driver->sendToProcessor($message); + + $this->assertSame('router', $message->getProperty(Config::PROCESSOR)); + } + + public function testShouldSendCommandMessageToProcessorToDefaultQueue() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('command', Route::COMMAND, 'processor'), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testShouldSendCommandMessageToProcessorToCustomQueue() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getCustomQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('command', Route::COMMAND, 'processor', ['queue' => 'custom']), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testThrowIfNoRouteFoundForCommandMessageOnSendToProcessor() + { + $context = $this->createContextMock(); + $context + ->expects($this->never()) + ->method('createProducer') + ; + $context + ->expects($this->never()) + ->method('createMessage') + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('There is no route for command "command".'); + $driver->sendToProcessor($message); + } + + public function testShouldOverwriteProcessorPropertySetByOneFromCommandRouteOnSendToProcessor() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getCustomQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('command', Route::COMMAND, 'expectedProcessor', ['queue' => 'custom']), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setProperty(Config::PROCESSOR, 'processorShouldBeOverwritten'); + + $driver->sendToProcessor($message); + + $this->assertSame('expectedProcessor', $message->getProperty(Config::PROCESSOR)); + } + + public function testShouldNotInitDeliveryDelayOnSendMessageToProcessorIfPropertyNull() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->never()) + ->method('setDeliveryDelay') + ; + $producer + ->expects($this->once()) + ->method('send') + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('command', Route::COMMAND, 'expectedProcessor', ['queue' => 'custom']), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setDelay(null); + + $driver->sendToProcessor($message); + } + + public function testShouldNotInitPriorityOnSendMessageToProcessorIfPropertyNull() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->never()) + ->method('setPriority') + ; + $producer + ->expects($this->once()) + ->method('send') + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('command', Route::COMMAND, 'expectedProcessor', ['queue' => 'custom']), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setPriority(null); + + $driver->sendToProcessor($message); + } + + public function testShouldNotInitTimeToLiveOnSendMessageToProcessorIfPropertyNull() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->never()) + ->method('setTimeToLive') + ; + $producer + ->expects($this->once()) + ->method('send') + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('command', Route::COMMAND, 'expectedProcessor', ['queue' => 'custom']), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setExpire(null); + + $driver->sendToProcessor($message); + } + + public function testThrowIfNeitherTopicNorCommandAreSentOnSendToProcessor() + { + $driver = $this->createDriver( + $this->createContextMock(), + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Queue name parameter is required but is not set'); + + $message = new Message(); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Either topic or command parameter must be set.'); + $driver->sendToProcessor($message); + } + + abstract protected function createDriver(...$args): DriverInterface; + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + abstract protected function createContextMock(): Context; + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + abstract protected function createProducerMock(): InteropProducer; + + abstract protected function createQueue(string $name): InteropQueue; + + abstract protected function createTopic(string $name): InteropTopic; + + abstract protected function createMessage(): InteropMessage; + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + protected function createContextStub(): Context + { + $context = $this->createContextMock(); + + $context + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $name) { + return $this->createQueue($name); + }) + ; + + $context + ->expects($this->any()) + ->method('createTopic') + ->willReturnCallback(function (string $name) { + return $this->createTopic($name); + }) + ; + + return $context; + } + + protected function assertTransportMessage(InteropMessage $transportMessage): void + { + $this->assertSame('body', $transportMessage->getBody()); + $this->assertEquals([ + 'hkey' => 'hval', + 'message_id' => 'theMessageId', + 'timestamp' => 1000, + 'reply_to' => 'theReplyTo', + 'correlation_id' => 'theCorrelationId', + ], $transportMessage->getHeaders()); + $this->assertEquals([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'ContentType', + Config::PRIORITY => MessagePriority::HIGH, + Config::EXPIRE => 123, + Config::DELAY => 345, + ], $transportMessage->getProperties()); + $this->assertSame('theMessageId', $transportMessage->getMessageId()); + $this->assertSame(1000, $transportMessage->getTimestamp()); + $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); + } + + protected function assertClientMessage(Message $clientMessage): void + { + $this->assertSame('body', $clientMessage->getBody()); + Assert::assertArraySubset([ + 'hkey' => 'hval', + ], $clientMessage->getHeaders()); + Assert::assertArraySubset([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'theContentType', + Config::EXPIRE => '22', + Config::PRIORITY => MessagePriority::HIGH, + Config::DELAY => '44', + ], $clientMessage->getProperties()); + $this->assertSame('theMessageId', $clientMessage->getMessageId()); + $this->assertSame(22, $clientMessage->getExpire()); + $this->assertSame(44, $clientMessage->getDelay()); + $this->assertSame(MessagePriority::HIGH, $clientMessage->getPriority()); + $this->assertSame('theContentType', $clientMessage->getContentType()); + $this->assertSame(1000, $clientMessage->getTimestamp()); + $this->assertSame('theReplyTo', $clientMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId()); + } + + protected function createDummyConfig(): Config + { + return Config::create('aPrefix'); + } + + protected function getDefaultQueueTransportName(): string + { + return 'aprefix.default'; + } + + protected function getCustomQueueTransportName(): string + { + return 'aprefix.custom'; + } + + protected function getRouterTransportName(): string + { + return 'aprefix.default'; + } + + protected function getPrefixAppFooQueueTransportName(): string + { + return 'aprefix.anappname.afooqueue'; + } + + protected function getPrefixFooQueueTransportName(): string + { + return 'aprefix.afooqueue'; + } + + protected function getAppFooQueueTransportName(): string + { + return 'anappname.afooqueue'; + } +} diff --git a/Tests/Client/Driver/GpsDriverTest.php b/Tests/Client/Driver/GpsDriverTest.php new file mode 100644 index 0000000..c0cac04 --- /dev/null +++ b/Tests/Client/Driver/GpsDriverTest.php @@ -0,0 +1,142 @@ +assertClassImplements(DriverInterface::class, GpsDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, GpsDriver::class); + } + + public function testShouldSetupBroker() + { + $routerTopic = new GpsTopic(''); + $routerQueue = new GpsQueue(''); + + $processorTopic = new GpsTopic($this->getDefaultQueueTransportName()); + $processorQueue = new GpsQueue($this->getDefaultQueueTransportName()); + + $context = $this->createContextMock(); + // setup router + $context + ->expects($this->at(0)) + ->method('createTopic') + ->willReturn($routerTopic) + ; + $context + ->expects($this->at(1)) + ->method('createQueue') + ->willReturn($routerQueue) + ; + $context + ->expects($this->at(2)) + ->method('subscribe') + ->with($this->identicalTo($routerTopic), $this->identicalTo($routerQueue)) + ; + $context + ->expects($this->at(3)) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($processorQueue) + ; + // setup processor queue + $context + ->expects($this->at(4)) + ->method('createTopic') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($processorTopic) + ; + $context + ->expects($this->at(5)) + ->method('subscribe') + ->with($this->identicalTo($processorTopic), $this->identicalTo($processorQueue)) + ; + + $driver = new GpsDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('aTopic', Route::TOPIC, 'aProcessor'), + ]) + ); + + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new GpsDriver(...$args); + } + + /** + * @return GpsContext + */ + protected function createContextMock(): Context + { + return $this->createMock(GpsContext::class); + } + + /** + * @return GpsProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(GpsProducer::class); + } + + /** + * @return GpsQueue + */ + protected function createQueue(string $name): InteropQueue + { + return new GpsQueue($name); + } + + /** + * @return GpsTopic + */ + protected function createTopic(string $name): InteropTopic + { + return new GpsTopic($name); + } + + /** + * @return GpsMessage + */ + protected function createMessage(): InteropMessage + { + return new GpsMessage(); + } + + protected function getRouterTransportName(): string + { + return 'aprefix.router'; + } +} diff --git a/Tests/Client/Driver/MongodbDriverTest.php b/Tests/Client/Driver/MongodbDriverTest.php new file mode 100644 index 0000000..697c757 --- /dev/null +++ b/Tests/Client/Driver/MongodbDriverTest.php @@ -0,0 +1,105 @@ +assertClassImplements(DriverInterface::class, MongodbDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, MongodbDriver::class); + } + + public function testShouldSetupBroker() + { + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createCollection') + ; + $context + ->expects($this->once()) + ->method('getConfig') + ->willReturn([ + 'dbname' => 'aDb', + 'collection_name' => 'aCol', + ]) + ; + + $driver = new MongodbDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new MongodbDriver(...$args); + } + + /** + * @return MongodbContext + */ + protected function createContextMock(): Context + { + return $this->createMock(MongodbContext::class); + } + + /** + * @return MongodbProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(MongodbProducer::class); + } + + /** + * @return MongodbDestination + */ + protected function createQueue(string $name): InteropQueue + { + return new MongodbDestination($name); + } + + /** + * @return MongodbDestination + */ + protected function createTopic(string $name): InteropTopic + { + return new MongodbDestination($name); + } + + /** + * @return MongodbMessage + */ + protected function createMessage(): InteropMessage + { + return new MongodbMessage(); + } +} diff --git a/Tests/Client/Driver/RabbitMqDriverTest.php b/Tests/Client/Driver/RabbitMqDriverTest.php new file mode 100644 index 0000000..b209d85 --- /dev/null +++ b/Tests/Client/Driver/RabbitMqDriverTest.php @@ -0,0 +1,139 @@ +assertClassImplements(DriverInterface::class, RabbitMqDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, RabbitMqDriver::class); + } + + public function testShouldBeSubClassOfAmqpDriver() + { + $this->assertClassExtends(AmqpDriver::class, RabbitMqDriver::class); + } + + public function testShouldCreateQueueWithMaxPriorityArgument() + { + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($this->createQueue('aName')) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + /** @var AmqpQueue $queue */ + $queue = $driver->createQueue('aName'); + + $this->assertSame(['x-max-priority' => 4], $queue->getArguments()); + } + + protected function createDriver(...$args): DriverInterface + { + return new RabbitMqDriver(...$args); + } + + /** + * @return AmqpContext + */ + protected function createContextMock(): Context + { + return $this->createMock(AmqpContext::class); + } + + /** + * @return AmqpProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(AmqpProducer::class); + } + + /** + * @return AmqpQueue + */ + protected function createQueue(string $name): InteropQueue + { + return new AmqpQueue($name); + } + + protected function createTopic(string $name): AmqpTopic + { + return new AmqpTopic($name); + } + + /** + * @return AmqpMessage + */ + protected function createMessage(): InteropMessage + { + return new AmqpMessage(); + } + + protected function getRouterTransportName(): string + { + return 'aprefix.router'; + } + + protected function assertTransportMessage(InteropMessage $transportMessage): void + { + $this->assertSame('body', $transportMessage->getBody()); + Assert::assertArraySubset([ + 'hkey' => 'hval', + 'delivery_mode' => AmqpMessage::DELIVERY_MODE_PERSISTENT, + 'content_type' => 'ContentType', + 'expiration' => '123000', + 'priority' => 3, + 'message_id' => 'theMessageId', + 'timestamp' => 1000, + 'reply_to' => 'theReplyTo', + 'correlation_id' => 'theCorrelationId', + ], $transportMessage->getHeaders()); + $this->assertEquals([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'ContentType', + Config::PRIORITY => MessagePriority::HIGH, + Config::EXPIRE => 123, + Config::DELAY => 345, + ], $transportMessage->getProperties()); + $this->assertSame('theMessageId', $transportMessage->getMessageId()); + $this->assertSame(1000, $transportMessage->getTimestamp()); + $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); + } +} diff --git a/Tests/Client/Driver/RabbitMqStompDriverTest.php b/Tests/Client/Driver/RabbitMqStompDriverTest.php new file mode 100644 index 0000000..9fc72be --- /dev/null +++ b/Tests/Client/Driver/RabbitMqStompDriverTest.php @@ -0,0 +1,590 @@ +assertClassImplements(DriverInterface::class, RabbitMqStompDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, RabbitMqStompDriver::class); + } + + public function testShouldBeSubClassOfStompDriver() + { + $this->assertClassExtends(StompDriver::class, RabbitMqStompDriver::class); + } + + public function testShouldCreateAndReturnStompQueueInstance() + { + $expectedQueue = new StompDestination(ExtensionType::RABBITMQ); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with('aprefix.afooqueue') + ->willReturn($expectedQueue) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]), + $this->createManagementClientMock() + ); + + $queue = $driver->createQueue('aFooQueue'); + + $expectedHeaders = [ + 'durable' => true, + 'auto-delete' => false, + 'exclusive' => false, + 'x-max-priority' => 4, + ]; + + $this->assertSame($expectedQueue, $queue); + $this->assertTrue($queue->isDurable()); + $this->assertFalse($queue->isAutoDelete()); + $this->assertFalse($queue->isExclusive()); + $this->assertSame($expectedHeaders, $queue->getHeaders()); + } + + public function testThrowIfClientPriorityInvalidOnCreateTransportMessage() + { + $clientMessage = new Message(); + $clientMessage->setPriority('unknown'); + + $transportMessage = new StompMessage(); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]), + $this->createManagementClientMock() + ); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Cant convert client priority to transport: "unknown"'); + + $driver->createTransportMessage($clientMessage); + } + + public function testThrowIfDelayIsSetButDelayPluginInstalledOptionIsFalse() + { + $clientMessage = new Message(); + $clientMessage->setDelay(123); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn(new StompMessage()) + ; + + $config = Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['delay_plugin_installed' => false] + ); + + $driver = $this->createDriver( + $context, + $config, + new RouteCollection([]), + $this->createManagementClientMock() + ); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The message delaying is not supported. In order to use delay feature install RabbitMQ delay plugin.'); + + $driver->createTransportMessage($clientMessage); + } + + public function testShouldSetXDelayHeaderIfDelayPluginInstalledOptionIsTrue() + { + $clientMessage = new Message(); + $clientMessage->setDelay(123); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn(new StompMessage()) + ; + + $config = Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['delay_plugin_installed' => true] + ); + + $driver = $this->createDriver( + $context, + $config, + new RouteCollection([]), + $this->createManagementClientMock() + ); + + $transportMessage = $driver->createTransportMessage($clientMessage); + + $this->assertSame('123000', $transportMessage->getHeader('x-delay')); + } + + public function testShouldInitDeliveryDelayIfDelayPropertyOnSendToProcessor() + { + $this->shouldSendMessageToDelayExchangeIfDelaySet(); + } + + public function shouldSendMessageToDelayExchangeIfDelaySet() + { + $queue = new StompDestination(ExtensionType::RABBITMQ); + $queue->setStompName('queueName'); + + $delayTopic = new StompDestination(ExtensionType::RABBITMQ); + $delayTopic->setStompName('delayTopic'); + + $transportMessage = new StompMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->at(0)) + ->method('setDeliveryDelay') + ->with(10000) + ; + $producer + ->expects($this->at(1)) + ->method('setDeliveryDelay') + ->with(null) + ; + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($delayTopic), $this->identicalTo($transportMessage)) + ; + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createTopic') + ->willReturn($delayTopic) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $config = Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['delay_plugin_installed' => true] + ); + + $driver = $this->createDriver( + $context, + $config, + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]), + $this->createManagementClientMock() + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + $message->setDelay(10); + + $driver->sendToProcessor($message); + } + + public function testShouldNotSetupBrokerIfManagementPluginInstalledOptionIsNotEnabled() + { + $config = Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['management_plugin_installed' => false] + ); + + $driver = $this->createDriver( + $this->createContextMock(), + $config, + new RouteCollection([]), + $this->createManagementClientMock() + ); + + $logger = new TestLogger(); + + $driver->setupBroker($logger); + + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Could not setup broker. The option `management_plugin_installed` is not enabled. Please enable that option and install rabbit management plugin' + ) + ); + } + + public function testShouldSetupBroker() + { + $routeCollection = new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]); + + $managementClient = $this->createManagementClientMock(); + $managementClient + ->expects($this->at(0)) + ->method('declareExchange') + ->with('aprefix.router', [ + 'type' => 'fanout', + 'durable' => true, + 'auto_delete' => false, + ]) + ; + $managementClient + ->expects($this->at(1)) + ->method('declareQueue') + ->with('aprefix.default', [ + 'durable' => true, + 'auto_delete' => false, + 'arguments' => [ + 'x-max-priority' => 4, + ], + ]) + ; + $managementClient + ->expects($this->at(2)) + ->method('bind') + ->with('aprefix.router', 'aprefix.default', 'aprefix.default') + ; + $managementClient + ->expects($this->at(3)) + ->method('declareQueue') + ->with('aprefix.default', [ + 'durable' => true, + 'auto_delete' => false, + 'arguments' => [ + 'x-max-priority' => 4, + ], + ]) + ; + + $contextMock = $this->createContextMock(); + $contextMock + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $name) { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_QUEUE); + $destination->setStompName($name); + + return $destination; + }) + ; + + $config = Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['delay_plugin_installed' => false, 'management_plugin_installed' => true] + ); + + $driver = $this->createDriver( + $contextMock, + $config, + $routeCollection, + $managementClient + ); + + $logger = new TestLogger(); + + $driver->setupBroker($logger); + + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Declare router exchange: aprefix.router' + ) + ); + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Declare router queue: aprefix.default' + ) + ); + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Bind router queue to exchange: aprefix.default -> aprefix.router' + ) + ); + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Declare processor queue: aprefix.default' + ) + ); + } + + public function testSetupBrokerShouldCreateDelayExchangeIfEnabled() + { + $routeCollection = new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]); + + $managementClient = $this->createManagementClientMock(); + $managementClient + ->expects($this->at(4)) + ->method('declareExchange') + ->with('aprefix.default.delayed', [ + 'type' => 'x-delayed-message', + 'durable' => true, + 'auto_delete' => false, + 'arguments' => [ + 'x-delayed-type' => 'direct', + ], + ]) + ; + $managementClient + ->expects($this->at(5)) + ->method('bind') + ->with('aprefix.default.delayed', 'aprefix.default', 'aprefix.default') + ; + + $config = Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['delay_plugin_installed' => true, 'management_plugin_installed' => true] + ); + + $contextMock = $this->createContextMock(); + $contextMock + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $name) { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_QUEUE); + $destination->setStompName($name); + + return $destination; + }) + ; + $contextMock + ->expects($this->any()) + ->method('createTopic') + ->willReturnCallback(function (string $name) { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_TOPIC); + $destination->setStompName($name); + + return $destination; + }) + ; + + $driver = $this->createDriver( + $contextMock, + $config, + $routeCollection, + $managementClient + ); + + $logger = new TestLogger(); + + $driver->setupBroker($logger); + + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Declare delay exchange: aprefix.default.delayed' + ) + ); + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Bind processor queue to delay exchange: aprefix.default -> aprefix.default.delayed' + ) + ); + } + + protected function createDriver(...$args): DriverInterface + { + return new RabbitMqStompDriver( + $args[0], + $args[1], + $args[2], + isset($args[3]) ? $args[3] : $this->createManagementClientMock() + ); + } + + /** + * @return StompContext + */ + protected function createContextMock(): Context + { + return $this->createMock(StompContext::class); + } + + /** + * @return StompProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(StompProducer::class); + } + + /** + * @return StompDestination + */ + protected function createQueue(string $name): InteropQueue + { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_QUEUE); + $destination->setStompName($name); + + return $destination; + } + + /** + * @return StompDestination + */ + protected function createTopic(string $name): InteropTopic + { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_TOPIC); + $destination->setStompName($name); + + return $destination; + } + + /** + * @return StompMessage + */ + protected function createMessage(): InteropMessage + { + return new StompMessage(); + } + + protected function assertTransportMessage(InteropMessage $transportMessage): void + { + $this->assertSame('body', $transportMessage->getBody()); + $this->assertEquals([ + 'hkey' => 'hval', + 'message_id' => 'theMessageId', + 'timestamp' => 1000, + 'reply-to' => 'theReplyTo', + 'persistent' => true, + 'correlation_id' => 'theCorrelationId', + 'expiration' => '123000', + 'priority' => 3, + 'x-delay' => '345000', + ], $transportMessage->getHeaders()); + $this->assertEquals([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'ContentType', + Config::PRIORITY => MessagePriority::HIGH, + Config::EXPIRE => 123, + Config::DELAY => 345, + ], $transportMessage->getProperties()); + $this->assertSame('theMessageId', $transportMessage->getMessageId()); + $this->assertSame(1000, $transportMessage->getTimestamp()); + $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); + } + + protected function createDummyConfig(): Config + { + return Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['delay_plugin_installed' => true, 'management_plugin_installed' => true] + ); + } + + protected function getRouterTransportName(): string + { + return '/topic/aprefix.router'; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createManagementClientMock(): StompManagementClient + { + return $this->createMock(StompManagementClient::class); + } +} diff --git a/Tests/Client/Driver/RdKafkaDriverTest.php b/Tests/Client/Driver/RdKafkaDriverTest.php new file mode 100644 index 0000000..c5e40e7 --- /dev/null +++ b/Tests/Client/Driver/RdKafkaDriverTest.php @@ -0,0 +1,122 @@ +assertClassImplements(DriverInterface::class, RdKafkaDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, RdKafkaDriver::class); + } + + public function testShouldSetupBroker() + { + $routerTopic = new RdKafkaTopic(''); + $routerQueue = new RdKafkaTopic(''); + + $processorTopic = new RdKafkaTopic(''); + + $context = $this->createContextMock(); + + $context + ->expects($this->at(0)) + ->method('createQueue') + ->willReturn($routerTopic) + ; + $context + ->expects($this->at(1)) + ->method('createQueue') + ->willReturn($routerQueue) + ; + $context + ->expects($this->at(2)) + ->method('createQueue') + ->willReturn($processorTopic) + ; + + $driver = new RdKafkaDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]) + ); + + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new RdKafkaDriver(...$args); + } + + /** + * @return RdKafkaContext + */ + protected function createContextMock(): Context + { + return $this->createMock(RdKafkaContext::class); + } + + /** + * @return RdKafkaProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(RdKafkaProducer::class); + } + + /** + * @return RdKafkaTopic + */ + protected function createQueue(string $name): InteropQueue + { + return new RdKafkaTopic($name); + } + + protected function createTopic(string $name): RdKafkaTopic + { + return new RdKafkaTopic($name); + } + + /** + * @return RdKafkaMessage + */ + protected function createMessage(): InteropMessage + { + return new RdKafkaMessage(); + } + + /** + * @return Config + */ + private function createDummyConfig() + { + return Config::create('aPrefix'); + } +} diff --git a/Tests/Client/Driver/SqsDriverTest.php b/Tests/Client/Driver/SqsDriverTest.php new file mode 100644 index 0000000..2e3005e --- /dev/null +++ b/Tests/Client/Driver/SqsDriverTest.php @@ -0,0 +1,153 @@ +assertClassImplements(DriverInterface::class, SqsDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, SqsDriver::class); + } + + public function testShouldSetupBroker() + { + $routerQueue = new SqsDestination(''); + $processorQueue = new SqsDestination(''); + + $context = $this->createContextMock(); + // setup router + $context + ->expects($this->at(0)) + ->method('createQueue') + ->with('aprefix_dot_default') + ->willReturn($routerQueue) + ; + $context + ->expects($this->at(1)) + ->method('declareQueue') + ->with($this->identicalTo($routerQueue)) + ; + // setup processor queue + $context + ->expects($this->at(2)) + ->method('createQueue') + ->with('aprefix_dot_default') + ->willReturn($processorQueue) + ; + $context + ->expects($this->at(3)) + ->method('declareQueue') + ->with($this->identicalTo($processorQueue)) + ; + + $driver = new SqsDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]) + ); + + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new SqsDriver(...$args); + } + + /** + * @return SqsContext + */ + protected function createContextMock(): Context + { + return $this->createMock(SqsContext::class); + } + + /** + * @return SqsProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(SqsProducer::class); + } + + /** + * @return SqsDestination + */ + protected function createQueue(string $name): InteropQueue + { + return new SqsDestination($name); + } + + /** + * @return SqsDestination + */ + protected function createTopic(string $name): InteropTopic + { + return new SqsDestination($name); + } + + /** + * @return SqsMessage + */ + protected function createMessage(): InteropMessage + { + return new SqsMessage(); + } + + protected function getPrefixAppFooQueueTransportName(): string + { + return 'aprefix_dot_anappname_dot_afooqueue'; + } + + protected function getPrefixFooQueueTransportName(): string + { + return 'aprefix_dot_afooqueue'; + } + + protected function getAppFooQueueTransportName(): string + { + return 'anappname_dot_afooqueue'; + } + + protected function getDefaultQueueTransportName(): string + { + return 'aprefix_dot_default'; + } + + protected function getCustomQueueTransportName(): string + { + return 'aprefix_dot_custom'; + } + + protected function getRouterTransportName(): string + { + return 'aprefix_dot_default'; + } +} diff --git a/Tests/Client/Driver/StompDriverTest.php b/Tests/Client/Driver/StompDriverTest.php new file mode 100644 index 0000000..8f777fd --- /dev/null +++ b/Tests/Client/Driver/StompDriverTest.php @@ -0,0 +1,191 @@ +assertClassImplements(DriverInterface::class, StompDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, StompDriver::class); + } + + public function testSetupBrokerShouldOnlyLogMessageThatStompDoesNotSupportBrokerSetup() + { + $driver = new StompDriver( + $this->createContextMock(), + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $logger = $this->createLoggerMock(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('[StompDriver] Stomp protocol does not support broker configuration') + ; + + $driver->setupBroker($logger); + } + + public function testShouldCreateDurableQueue() + { + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($this->createQueue('aName')) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + /** @var StompDestination $queue */ + $queue = $driver->createQueue('aName'); + + $this->assertTrue($queue->isDurable()); + $this->assertFalse($queue->isAutoDelete()); + $this->assertFalse($queue->isExclusive()); + } + + public function testShouldSetPersistedTrueOnCreateTransportMessage() + { + $clientMessage = new Message(); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($this->createMessage()) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + /** @var StompMessage $transportMessage */ + $transportMessage = $driver->createTransportMessage($clientMessage); + + $this->assertTrue($transportMessage->isPersistent()); + } + + protected function createDriver(...$args): DriverInterface + { + return new StompDriver(...$args); + } + + /** + * @return StompContext + */ + protected function createContextMock(): Context + { + return $this->createMock(StompContext::class); + } + + /** + * @return StompProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(StompProducer::class); + } + + /** + * @return StompDestination + */ + protected function createQueue(string $name): InteropQueue + { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_QUEUE); + $destination->setStompName($name); + + return $destination; + } + + /** + * @return StompDestination + */ + protected function createTopic(string $name): InteropTopic + { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_TOPIC); + $destination->setStompName($name); + + return $destination; + } + + /** + * @return StompMessage + */ + protected function createMessage(): InteropMessage + { + return new StompMessage(); + } + + protected function assertTransportMessage(InteropMessage $transportMessage): void + { + $this->assertSame('body', $transportMessage->getBody()); + $this->assertEquals([ + 'hkey' => 'hval', + 'message_id' => 'theMessageId', + 'timestamp' => 1000, + 'reply-to' => 'theReplyTo', + 'persistent' => true, + 'correlation_id' => 'theCorrelationId', + ], $transportMessage->getHeaders()); + $this->assertEquals([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'ContentType', + Config::PRIORITY => MessagePriority::HIGH, + Config::EXPIRE => 123, + Config::DELAY => 345, + ], $transportMessage->getProperties()); + $this->assertSame('theMessageId', $transportMessage->getMessageId()); + $this->assertSame(1000, $transportMessage->getTimestamp()); + $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); + } + + protected function createLoggerMock(): LoggerInterface + { + return $this->createMock(LoggerInterface::class); + } + + protected function getRouterTransportName(): string + { + return '/topic/aprefix.router'; + } +} diff --git a/Tests/Client/Driver/StompManagementClientTest.php b/Tests/Client/Driver/StompManagementClientTest.php new file mode 100644 index 0000000..081a62c --- /dev/null +++ b/Tests/Client/Driver/StompManagementClientTest.php @@ -0,0 +1,114 @@ +createExchangeMock(); + $exchange + ->expects($this->once()) + ->method('create') + ->with('vhost', 'name', ['options']) + ->willReturn([]) + ; + + $client = $this->createClientMock(); + $client + ->expects($this->once()) + ->method('exchanges') + ->willReturn($exchange) + ; + + $management = new StompManagementClient($client, 'vhost'); + $management->declareExchange('name', ['options']); + } + + public function testCouldDeclareQueue() + { + $queue = $this->createQueueMock(); + $queue + ->expects($this->once()) + ->method('create') + ->with('vhost', 'name', ['options']) + ->willReturn([]) + ; + + $client = $this->createClientMock(); + $client + ->expects($this->once()) + ->method('queues') + ->willReturn($queue) + ; + + $management = new StompManagementClient($client, 'vhost'); + $management->declareQueue('name', ['options']); + } + + public function testCouldBind() + { + $binding = $this->createBindingMock(); + $binding + ->expects($this->once()) + ->method('create') + ->with('vhost', 'exchange', 'queue', 'routing-key', ['arguments']) + ->willReturn([]) + ; + + $client = $this->createClientMock(); + $client + ->expects($this->once()) + ->method('bindings') + ->willReturn($binding) + ; + + $management = new StompManagementClient($client, 'vhost'); + $management->bind('exchange', 'queue', 'routing-key', ['arguments']); + } + + public function testCouldCreateNewInstanceUsingFactory() + { + $instance = StompManagementClient::create('', ''); + + $this->assertInstanceOf(StompManagementClient::class, $instance); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|Client + */ + private function createClientMock() + { + return $this->createMock(Client::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|Exchange + */ + private function createExchangeMock() + { + return $this->createMock(Exchange::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|Queue + */ + private function createQueueMock() + { + return $this->createMock(Queue::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|Binding + */ + private function createBindingMock() + { + return $this->createMock(Binding::class); + } +} diff --git a/Tests/Client/DriverFactoryTest.php b/Tests/Client/DriverFactoryTest.php new file mode 100644 index 0000000..3d9d7b9 --- /dev/null +++ b/Tests/Client/DriverFactoryTest.php @@ -0,0 +1,186 @@ +assertTrue($rc->implementsInterface(DriverFactoryInterface::class)); + } + + public function testShouldBeFinal() + { + $rc = new \ReflectionClass(DriverFactory::class); + + $this->assertTrue($rc->isFinal()); + } + + public function testThrowIfPackageThatSupportSchemeNotInstalled() + { + $scheme = 'scheme5b7aa7d7cd213'; + $class = 'ConnectionClass5b7aa7d7cd213'; + + Resources::addDriver($class, [$scheme], [], ['thePackage', 'theOtherPackage']); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('To use given scheme "scheme5b7aa7d7cd213" a package has to be installed. Run "composer req thePackage theOtherPackage" to add it.'); + $factory = new DriverFactory(); + + $factory->create($this->createConnectionFactoryMock(), $this->createDummyConfig($scheme.'://foo'), new RouteCollection([])); + } + + public function testThrowIfSchemeIsNotKnown() + { + $scheme = 'scheme5b7aa862e70a5'; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('A given scheme "scheme5b7aa862e70a5" is not supported. Maybe it is a custom driver, make sure you registered it with "Enqueue\Client\Resources::addDriver".'); + + $factory = new DriverFactory(); + + $factory->create($this->createConnectionFactoryMock(), $this->createDummyConfig($scheme.'://foo'), new RouteCollection([])); + } + + public function testThrowIfDsnInvalid() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The DSN is invalid. It does not have scheme separator ":".'); + + $factory = new DriverFactory(); + + $factory->create($this->createConnectionFactoryMock(), $this->createDummyConfig('invalidDsn'), new RouteCollection([])); + } + + /** + * @dataProvider provideDSN + */ + public function testReturnsExpectedFactories( + string $dsn, + string $connectionFactoryClass, + string $contextClass, + array $conifg, + string $expectedDriverClass, + ) { + $connectionFactoryMock = $this->createMock($connectionFactoryClass); + $connectionFactoryMock + ->expects($this->once()) + ->method('createContext') + ->willReturn($this->createMock($contextClass)) + ; + + $driverFactory = new DriverFactory(); + + $driver = $driverFactory->create($connectionFactoryMock, $this->createDummyConfig($dsn), new RouteCollection([])); + + $this->assertInstanceOf($expectedDriverClass, $driver); + } + + public static function provideDSN() + { + yield ['null:', NullConnectionFactory::class, NullContext::class, [], GenericDriver::class]; + + yield ['amqp:', AmqpConnectionFactory::class, AmqpContext::class, [], AmqpDriver::class]; + + yield ['amqp+rabbitmq:', AmqpConnectionFactory::class, AmqpContext::class, [], RabbitMqDriver::class]; + + yield ['mysql:', DbalConnectionFactory::class, DbalContext::class, [], DbalDriver::class]; + + yield ['file:', FsConnectionFactory::class, FsContext::class, [], FsDriver::class]; + + // https://github.com/php-enqueue/enqueue-dev/issues/511 + // yield ['gearman:', GearmanConnectionFactory::class, NullContext::class, [], NullDriver::class]; + + yield ['gps:', GpsConnectionFactory::class, GpsContext::class, [], GpsDriver::class]; + + yield ['mongodb:', MongodbConnectionFactory::class, MongodbContext::class, [], MongodbDriver::class]; + + yield ['kafka:', RdKafkaConnectionFactory::class, RdKafkaContext::class, [], RdKafkaDriver::class]; + + yield ['redis:', RedisConnectionFactory::class, RedisContext::class, [], GenericDriver::class]; + + yield ['redis+predis:', RedisConnectionFactory::class, RedisContext::class, [], GenericDriver::class]; + + yield ['sqs:', SqsConnectionFactory::class, SqsContext::class, [], SqsDriver::class]; + + yield ['stomp:', StompConnectionFactory::class, StompContext::class, [], StompDriver::class]; + + yield ['stomp+rabbitmq:', StompConnectionFactory::class, StompContext::class, [], RabbitMqStompDriver::class]; + + yield ['stomp+foo+bar:', StompConnectionFactory::class, StompContext::class, [], StompDriver::class]; + + yield ['gearman:', GearmanConnectionFactory::class, GearmanContext::class, [], GenericDriver::class]; + + yield ['beanstalk:', PheanstalkConnectionFactory::class, PheanstalkContext::class, [], GenericDriver::class]; + } + + private function createDummyConfig(string $dsn): Config + { + return Config::create( + null, + null, + null, + null, + null, + null, + null, + ['dsn' => $dsn], + [] + ); + } + + private function createConnectionFactoryMock(): ConnectionFactory + { + return $this->createMock(ConnectionFactory::class); + } + + private function createConfigMock(): Config + { + return $this->createMock(Config::class); + } +} diff --git a/Tests/Client/DriverPreSendTest.php b/Tests/Client/DriverPreSendTest.php new file mode 100644 index 0000000..32af2a8 --- /dev/null +++ b/Tests/Client/DriverPreSendTest.php @@ -0,0 +1,84 @@ +createProducerMock(); + $expectedDriver = $this->createDriverMock(); + + $context = new DriverPreSend( + $expectedMessage, + $expectedProducer, + $expectedDriver + ); + + $this->assertSame($expectedMessage, $context->getMessage()); + $this->assertSame($expectedProducer, $context->getProducer()); + $this->assertSame($expectedDriver, $context->getDriver()); + } + + public function testShouldAllowGetCommand() + { + $message = new Message(); + $message->setProperty(Config::COMMAND, 'theCommand'); + + $context = new DriverPreSend( + $message, + $this->createProducerMock(), + $this->createDriverMock() + ); + + $this->assertFalse($context->isEvent()); + $this->assertSame('theCommand', $context->getCommand()); + } + + public function testShouldAllowGetTopic() + { + $message = new Message(); + $message->setProperty(Config::TOPIC, 'theTopic'); + + $context = new DriverPreSend( + $message, + $this->createProducerMock(), + $this->createDriverMock() + ); + + $this->assertTrue($context->isEvent()); + $this->assertSame('theTopic', $context->getTopic()); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverMock(): DriverInterface + { + return $this->createMock(DriverInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createProducerMock(): ProducerInterface + { + return $this->createMock(ProducerInterface::class); + } +} diff --git a/Tests/Client/Extension/PrepareBodyExtensionTest.php b/Tests/Client/Extension/PrepareBodyExtensionTest.php new file mode 100644 index 0000000..c3032cc --- /dev/null +++ b/Tests/Client/Extension/PrepareBodyExtensionTest.php @@ -0,0 +1,131 @@ +assertTrue($rc->implementsInterface(PreSendEventExtensionInterface::class)); + $this->assertTrue($rc->implementsInterface(PreSendCommandExtensionInterface::class)); + } + + /** + * @dataProvider provideMessages + * + * @param mixed|null $contentType + */ + public function testShouldSendStringUnchangedAndAddPlainTextContentTypeIfEmpty( + $body, + $contentType, + string $expectedBody, + string $expectedContentType, + ) { + $message = new Message($body); + $message->setContentType($contentType); + + $context = $this->createDummyPreSendContext('aTopic', $message); + + $extension = new PrepareBodyExtension(); + + $extension->onPreSendEvent($context); + + $this->assertSame($expectedBody, $message->getBody()); + $this->assertSame($expectedContentType, $message->getContentType()); + } + + public function testThrowIfBodyIsObject() + { + $message = new Message(new \stdClass()); + + $context = $this->createDummyPreSendContext('aTopic', $message); + + $extension = new PrepareBodyExtension(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The message\'s body must be either null, scalar, array or object (implements \JsonSerializable). Got: stdClass'); + + $extension->onPreSendEvent($context); + } + + public function testThrowIfBodyIsArrayWithObjectsInsideOnSend() + { + $message = new Message(['foo' => new \stdClass()]); + + $context = $this->createDummyPreSendContext('aTopic', $message); + + $extension = new PrepareBodyExtension(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The message\'s body must be an array of scalars. Found not scalar in the array: stdClass'); + + $extension->onPreSendEvent($context); + } + + public function testShouldThrowExceptionIfBodyIsArrayWithObjectsInSubArraysInsideOnSend() + { + $message = new Message(['foo' => ['bar' => new \stdClass()]]); + + $context = $this->createDummyPreSendContext('aTopic', $message); + + $extension = new PrepareBodyExtension(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The message\'s body must be an array of scalars. Found not scalar in the array: stdClass'); + + $extension->onPreSendEvent($context); + } + + public static function provideMessages() + { + yield ['theBody', null, 'theBody', 'text/plain']; + + yield ['theBody', 'foo/bar', 'theBody', 'foo/bar']; + + yield [12345, null, '12345', 'text/plain']; + + yield [12345, 'foo/bar', '12345', 'foo/bar']; + + yield [12.345, null, '12.345', 'text/plain']; + + yield [12.345, 'foo/bar', '12.345', 'foo/bar']; + + yield [true, null, '1', 'text/plain']; + + yield [true, 'foo/bar', '1', 'foo/bar']; + + yield [null, null, '', 'text/plain']; + + yield [null, 'foo/bar', '', 'foo/bar']; + + yield [['foo' => 'fooVal'], null, '{"foo":"fooVal"}', 'application/json']; + + yield [['foo' => 'fooVal'], 'foo/bar', '{"foo":"fooVal"}', 'foo/bar']; + + yield [new JsonSerializableObject(), null, '{"foo":"fooVal"}', 'application/json']; + + yield [new JsonSerializableObject(), 'foo/bar', '{"foo":"fooVal"}', 'foo/bar']; + } + + private function createDummyPreSendContext($commandOrTopic, $message): PreSend + { + return new PreSend( + $commandOrTopic, + $message, + $this->createMock(ProducerInterface::class), + $this->createMock(DriverInterface::class) + ); + } +} diff --git a/Tests/Client/Meta/QueueMetaRegistryTest.php b/Tests/Client/Meta/QueueMetaRegistryTest.php deleted file mode 100644 index 787290f..0000000 --- a/Tests/Client/Meta/QueueMetaRegistryTest.php +++ /dev/null @@ -1,146 +0,0 @@ - [], - 'anotherQueueName' => [], - ]; - - $registry = new QueueMetaRegistry($this->createConfig(), $meta); - - $this->assertAttributeEquals($meta, 'meta', $registry); - } - - public function testShouldAllowAddQueueMetaUsingAddMethod() - { - $registry = new QueueMetaRegistry($this->createConfig(), []); - - $registry->add('theFooQueueName', 'theTransportQueueName'); - $registry->add('theBarQueueName'); - - $this->assertAttributeSame([ - 'theFooQueueName' => [ - 'transportName' => 'theTransportQueueName', - 'processors' => [], - ], - 'theBarQueueName' => [ - 'transportName' => null, - 'processors' => [], - ], - ], 'meta', $registry); - } - - public function testShouldAllowAddSubscriber() - { - $registry = new QueueMetaRegistry($this->createConfig(), []); - - $registry->addProcessor('theFooQueueName', 'theFooProcessorName'); - $registry->addProcessor('theFooQueueName', 'theBarProcessorName'); - $registry->addProcessor('theBarQueueName', 'theBazProcessorName'); - - $this->assertAttributeSame([ - 'theFooQueueName' => [ - 'transportName' => null, - 'processors' => ['theFooProcessorName', 'theBarProcessorName'], - ], - 'theBarQueueName' => [ - 'transportName' => null, - 'processors' => ['theBazProcessorName'], - ], - ], 'meta', $registry); - } - - public function testThrowIfThereIsNotMetaForRequestedClientQueueName() - { - $registry = new QueueMetaRegistry($this->createConfig(), []); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The queue meta not found. Requested name `aName`'); - $registry->getQueueMeta('aName'); - } - - public function testShouldAllowGetQueueByNameWithDefaultInfo() - { - $queues = [ - 'theQueueName' => [], - ]; - - $registry = new QueueMetaRegistry($this->createConfig(), $queues); - - $queue = $registry->getQueueMeta('theQueueName'); - - $this->assertInstanceOf(QueueMeta::class, $queue); - $this->assertSame('theQueueName', $queue->getClientName()); - $this->assertSame('aprefix.anappname.thequeuename', $queue->getTransportName()); - $this->assertSame([], $queue->getProcessors()); - } - - public function testShouldAllowGetQueueByNameWithCustomInfo() - { - $queues = [ - 'theClientQueueName' => ['transportName' => 'theTransportName', 'processors' => ['theSubscriber']], - ]; - - $registry = new QueueMetaRegistry($this->createConfig(), $queues); - - $queue = $registry->getQueueMeta('theClientQueueName'); - $this->assertInstanceOf(QueueMeta::class, $queue); - $this->assertSame('theClientQueueName', $queue->getClientName()); - $this->assertSame('theTransportName', $queue->getTransportName()); - $this->assertSame(['theSubscriber'], $queue->getProcessors()); - } - - public function testShouldNotAllowToOverwriteDefaultTransportNameByEmptyValue() - { - $registry = new QueueMetaRegistry($this->createConfig(), [ - 'theClientQueueName' => ['transportName' => null, 'processors' => []], - ]); - - $queue = $registry->getQueueMeta('theClientQueueName'); - $this->assertInstanceOf(QueueMeta::class, $queue); - $this->assertSame('aprefix.anappname.theclientqueuename', $queue->getTransportName()); - } - - public function testShouldAllowGetAllQueues() - { - $queues = [ - 'fooQueueName' => [], - 'barQueueName' => [], - ]; - - $registry = new QueueMetaRegistry($this->createConfig(), $queues); - - $queues = $registry->getQueuesMeta(); - $this->assertInstanceOf(\Generator::class, $queues); - - $queues = iterator_to_array($queues); - /* @var QueueMeta[] $queues */ - - $this->assertContainsOnly(QueueMeta::class, $queues); - $this->assertCount(2, $queues); - - $this->assertSame('fooQueueName', $queues[0]->getClientName()); - $this->assertSame('aprefix.anappname.fooqueuename', $queues[0]->getTransportName()); - - $this->assertSame('barQueueName', $queues[1]->getClientName()); - $this->assertSame('aprefix.anappname.barqueuename', $queues[1]->getTransportName()); - } - - /** - * @return Config - */ - private function createConfig() - { - return new Config('aPrefix', 'anAppName', 'aRouterTopic', 'aRouterQueueName', 'aDefaultQueueName', 'aRouterProcessorName'); - } -} diff --git a/Tests/Client/Meta/QueueMetaTest.php b/Tests/Client/Meta/QueueMetaTest.php deleted file mode 100644 index fde6dc5..0000000 --- a/Tests/Client/Meta/QueueMetaTest.php +++ /dev/null @@ -1,39 +0,0 @@ -assertAttributeEquals('aClientName', 'clientName', $meta); - $this->assertAttributeEquals('aTransportName', 'transportName', $meta); - $this->assertAttributeEquals([], 'processors', $meta); - } - - public function testShouldAllowGetClientNameSetInConstructor() - { - $meta = new QueueMeta('theClientName', 'aTransportName'); - - $this->assertSame('theClientName', $meta->getClientName()); - } - - public function testShouldAllowGetTransportNameSetInConstructor() - { - $meta = new QueueMeta('aClientName', 'theTransportName'); - - $this->assertSame('theTransportName', $meta->getTransportName()); - } - - public function testShouldAllowGetSubscribersSetInConstructor() - { - $meta = new QueueMeta('aClientName', 'aTransportName', ['aSubscriber']); - - $this->assertSame(['aSubscriber'], $meta->getProcessors()); - } -} diff --git a/Tests/Client/Meta/TopicMetaRegistryTest.php b/Tests/Client/Meta/TopicMetaRegistryTest.php deleted file mode 100644 index ce074b6..0000000 --- a/Tests/Client/Meta/TopicMetaRegistryTest.php +++ /dev/null @@ -1,124 +0,0 @@ - [], - 'anotherTopicName' => [], - ]; - - $registry = new TopicMetaRegistry($topics); - - $this->assertAttributeEquals($topics, 'meta', $registry); - } - - public function testShouldAllowAddTopicMetaUsingAddMethod() - { - $registry = new TopicMetaRegistry([]); - - $registry->add('theFooTopicName', 'aDescription'); - $registry->add('theBarTopicName'); - - $this->assertAttributeSame([ - 'theFooTopicName' => [ - 'description' => 'aDescription', - 'processors' => [], - ], - 'theBarTopicName' => [ - 'description' => null, - 'processors' => [], - ], - ], 'meta', $registry); - } - - public function testShouldAllowAddSubscriber() - { - $registry = new TopicMetaRegistry([]); - - $registry->addProcessor('theFooTopicName', 'theFooProcessorName'); - $registry->addProcessor('theFooTopicName', 'theBarProcessorName'); - $registry->addProcessor('theBarTopicName', 'theBazProcessorName'); - - $this->assertAttributeSame([ - 'theFooTopicName' => [ - 'description' => null, - 'processors' => ['theFooProcessorName', 'theBarProcessorName'], - ], - 'theBarTopicName' => [ - 'description' => null, - 'processors' => ['theBazProcessorName'], - ], - ], 'meta', $registry); - } - - public function testThrowIfThereIsNotMetaForRequestedTopicName() - { - $registry = new TopicMetaRegistry([]); - - $this->setExpectedException( - \InvalidArgumentException::class, - 'The topic meta not found. Requested name `aName`' - ); - $registry->getTopicMeta('aName'); - } - - public function testShouldAllowGetTopicByNameWithDefaultInfo() - { - $topics = [ - 'theTopicName' => [], - ]; - - $registry = new TopicMetaRegistry($topics); - - $topic = $registry->getTopicMeta('theTopicName'); - $this->assertInstanceOf(TopicMeta::class, $topic); - $this->assertSame('theTopicName', $topic->getName()); - $this->assertSame('', $topic->getDescription()); - $this->assertSame([], $topic->getProcessors()); - } - - public function testShouldAllowGetTopicByNameWithCustomInfo() - { - $topics = [ - 'theTopicName' => ['description' => 'theDescription', 'processors' => ['theSubscriber']], - ]; - - $registry = new TopicMetaRegistry($topics); - - $topic = $registry->getTopicMeta('theTopicName'); - $this->assertInstanceOf(TopicMeta::class, $topic); - $this->assertSame('theTopicName', $topic->getName()); - $this->assertSame('theDescription', $topic->getDescription()); - $this->assertSame(['theSubscriber'], $topic->getProcessors()); - } - - public function testShouldAllowGetAllTopics() - { - $topics = [ - 'fooTopicName' => [], - 'barTopicName' => [], - ]; - - $registry = new TopicMetaRegistry($topics); - - $topics = $registry->getTopicsMeta(); - $this->assertInstanceOf(\Generator::class, $topics); - - $topics = iterator_to_array($topics); - /* @var TopicMeta[] $topics */ - - $this->assertContainsOnly(TopicMeta::class, $topics); - $this->assertCount(2, $topics); - - $this->assertSame('fooTopicName', $topics[0]->getName()); - $this->assertSame('barTopicName', $topics[1]->getName()); - } -} diff --git a/Tests/Client/Meta/TopicMetaTest.php b/Tests/Client/Meta/TopicMetaTest.php deleted file mode 100644 index 565a8f8..0000000 --- a/Tests/Client/Meta/TopicMetaTest.php +++ /dev/null @@ -1,57 +0,0 @@ -assertAttributeEquals('aName', 'name', $topic); - $this->assertAttributeEquals('', 'description', $topic); - $this->assertAttributeEquals([], 'processors', $topic); - } - - public function testCouldBeConstructedWithNameAndDescriptionOnly() - { - $topic = new TopicMeta('aName', 'aDescription'); - - $this->assertAttributeEquals('aName', 'name', $topic); - $this->assertAttributeEquals('aDescription', 'description', $topic); - $this->assertAttributeEquals([], 'processors', $topic); - } - - public function testCouldBeConstructedWithNameAndDescriptionAndSubscribers() - { - $topic = new TopicMeta('aName', 'aDescription', ['aSubscriber']); - - $this->assertAttributeEquals('aName', 'name', $topic); - $this->assertAttributeEquals('aDescription', 'description', $topic); - $this->assertAttributeEquals(['aSubscriber'], 'processors', $topic); - } - - public function testShouldAllowGetNameSetInConstructor() - { - $topic = new TopicMeta('theName', 'aDescription'); - - $this->assertSame('theName', $topic->getName()); - } - - public function testShouldAllowGetDescriptionSetInConstructor() - { - $topic = new TopicMeta('aName', 'theDescription'); - - $this->assertSame('theDescription', $topic->getDescription()); - } - - public function testShouldAllowGetSubscribersSetInConstructor() - { - $topic = new TopicMeta('aName', '', ['aSubscriber']); - - $this->assertSame(['aSubscriber'], $topic->getProcessors()); - } -} diff --git a/Tests/Client/PostSendTest.php b/Tests/Client/PostSendTest.php new file mode 100644 index 0000000..ba51710 --- /dev/null +++ b/Tests/Client/PostSendTest.php @@ -0,0 +1,112 @@ +createProducerMock(); + $expectedDriver = $this->createDriverMock(); + $expectedDestination = $this->createDestinationMock(); + $expectedTransportMessage = $this->createTransportMessageMock(); + + $context = new PostSend( + $expectedMessage, + $expectedProducer, + $expectedDriver, + $expectedDestination, + $expectedTransportMessage + ); + + $this->assertSame($expectedMessage, $context->getMessage()); + $this->assertSame($expectedProducer, $context->getProducer()); + $this->assertSame($expectedDriver, $context->getDriver()); + $this->assertSame($expectedDestination, $context->getTransportDestination()); + $this->assertSame($expectedTransportMessage, $context->getTransportMessage()); + } + + public function testShouldAllowGetCommand() + { + $message = new Message(); + $message->setProperty(Config::COMMAND, 'theCommand'); + + $context = new PostSend( + $message, + $this->createProducerMock(), + $this->createDriverMock(), + $this->createDestinationMock(), + $this->createTransportMessageMock() + ); + + $this->assertFalse($context->isEvent()); + $this->assertSame('theCommand', $context->getCommand()); + } + + public function testShouldAllowGetTopic() + { + $message = new Message(); + $message->setProperty(Config::TOPIC, 'theTopic'); + + $context = new PostSend( + $message, + $this->createProducerMock(), + $this->createDriverMock(), + $this->createDestinationMock(), + $this->createTransportMessageMock() + ); + + $this->assertTrue($context->isEvent()); + $this->assertSame('theTopic', $context->getTopic()); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverMock(): DriverInterface + { + return $this->createMock(DriverInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createProducerMock(): ProducerInterface + { + return $this->createMock(ProducerInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|Destination + */ + private function createDestinationMock(): Destination + { + return $this->createMock(Destination::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|TransportMessage + */ + private function createTransportMessageMock(): TransportMessage + { + return $this->createMock(TransportMessage::class); + } +} diff --git a/Tests/Client/PreSendTest.php b/Tests/Client/PreSendTest.php new file mode 100644 index 0000000..01a7e50 --- /dev/null +++ b/Tests/Client/PreSendTest.php @@ -0,0 +1,116 @@ +createProducerMock(); + $expectedDriver = $this->createDriverMock(); + + $context = new PreSend( + $expectedCommandOrTopic, + $expectedMessage, + $expectedProducer, + $expectedDriver + ); + + $this->assertSame($expectedCommandOrTopic, $context->getTopic()); + $this->assertSame($expectedCommandOrTopic, $context->getCommand()); + $this->assertSame($expectedMessage, $context->getMessage()); + $this->assertSame($expectedProducer, $context->getProducer()); + $this->assertSame($expectedDriver, $context->getDriver()); + + $this->assertEquals($expectedMessage, $context->getOriginalMessage()); + $this->assertNotSame($expectedMessage, $context->getOriginalMessage()); + } + + public function testCouldChangeTopic() + { + $context = new PreSend( + 'aCommandOrTopic', + new Message(), + $this->createProducerMock(), + $this->createDriverMock() + ); + + // guard + $this->assertSame('aCommandOrTopic', $context->getTopic()); + + $context->changeTopic('theChangedTopic'); + + $this->assertSame('theChangedTopic', $context->getTopic()); + } + + public function testCouldChangeCommand() + { + $context = new PreSend( + 'aCommandOrTopic', + new Message(), + $this->createProducerMock(), + $this->createDriverMock() + ); + + // guard + $this->assertSame('aCommandOrTopic', $context->getCommand()); + + $context->changeCommand('theChangedCommand'); + + $this->assertSame('theChangedCommand', $context->getCommand()); + } + + public function testCouldChangeBody() + { + $context = new PreSend( + 'aCommandOrTopic', + new Message('aBody'), + $this->createProducerMock(), + $this->createDriverMock() + ); + + // guard + $this->assertSame('aBody', $context->getMessage()->getBody()); + $this->assertNull($context->getMessage()->getContentType()); + + $context->changeBody('theChangedBody'); + $this->assertSame('theChangedBody', $context->getMessage()->getBody()); + $this->assertNull($context->getMessage()->getContentType()); + + $context->changeBody('theChangedBodyAgain', 'foo/bar'); + $this->assertSame('theChangedBodyAgain', $context->getMessage()->getBody()); + $this->assertSame('foo/bar', $context->getMessage()->getContentType()); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverMock(): DriverInterface + { + return $this->createMock(DriverInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createProducerMock(): ProducerInterface + { + return $this->createMock(ProducerInterface::class); + } +} diff --git a/Tests/Client/ProducerSendCommandTest.php b/Tests/Client/ProducerSendCommandTest.php new file mode 100644 index 0000000..9500e9d --- /dev/null +++ b/Tests/Client/ProducerSendCommandTest.php @@ -0,0 +1,537 @@ +createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + $driver + ->expects($this->never()) + ->method('sendToRouter') + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + $expectedProperties = [ + 'enqueue.command' => 'command', + ]; + + self::assertEquals($expectedProperties, $message->getProperties()); + } + + public function testShouldSendCommandWithReply() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + $driver + ->expects($this->never()) + ->method('sendToRouter') + ; + + $expectedPromiseMock = $this->createMock(Promise::class); + + $rpcFactoryMock = $this->createRpcFactoryMock(); + $rpcFactoryMock + ->expects($this->once()) + ->method('createReplyTo') + ->willReturn('theReplyQueue') + ; + $rpcFactoryMock + ->expects($this->once()) + ->method('createPromise') + ->with( + 'theReplyQueue', + $this->logicalNot($this->isEmpty()), + 60000 + ) + ->willReturn($expectedPromiseMock) + ; + + $producer = new Producer($driver, $rpcFactoryMock); + $actualPromise = $producer->sendCommand('command', $message, true); + + $this->assertSame($expectedPromiseMock, $actualPromise); + + self::assertEquals('theReplyQueue', $message->getReplyTo()); + self::assertNotEmpty($message->getCorrelationId()); + } + + public function testShouldSendCommandWithReplyAndCustomReplyQueueAndCorrelationId() + { + $message = new Message(); + $message->setReplyTo('theCustomReplyQueue'); + $message->setCorrelationId('theCustomCorrelationId'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + $driver + ->expects($this->never()) + ->method('sendToRouter') + ; + + $expectedPromiseMock = $this->createMock(Promise::class); + + $rpcFactoryMock = $this->createRpcFactoryMock(); + $rpcFactoryMock + ->expects($this->never()) + ->method('createReplyTo') + ; + $rpcFactoryMock + ->expects($this->once()) + ->method('createPromise') + ->with( + 'theCustomReplyQueue', + 'theCustomCorrelationId', + 60000 + ) + ->willReturn($expectedPromiseMock) + ; + + $producer = new Producer($driver, $rpcFactoryMock); + $actualPromise = $producer->sendCommand('command', $message, true); + + $this->assertSame($expectedPromiseMock, $actualPromise); + + self::assertEquals('theCustomReplyQueue', $message->getReplyTo()); + self::assertSame('theCustomCorrelationId', $message->getCorrelationId()); + } + + public function testShouldOverwriteExpectedMessageProperties() + { + $message = new Message(); + $message->setProperty(Config::COMMAND, 'commandShouldBeOverwritten'); + $message->setScope('scopeShouldBeOverwritten'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('expectedCommand', $message); + + $expectedProperties = [ + 'enqueue.command' => 'expectedCommand', + ]; + + self::assertEquals($expectedProperties, $message->getProperties()); + self::assertSame(Message::SCOPE_APP, $message->getScope()); + } + + public function testShouldSendCommandWithoutPriorityByDefault() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + self::assertNull($message->getPriority()); + } + + public function testShouldSendCommandWithCustomPriority() + { + $message = new Message(); + $message->setPriority(MessagePriority::HIGH); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + self::assertSame(MessagePriority::HIGH, $message->getPriority()); + } + + public function testShouldSendCommandWithGeneratedMessageId() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + self::assertNotEmpty($message->getMessageId()); + } + + public function testShouldSendCommandWithCustomMessageId() + { + $message = new Message(); + $message->setMessageId('theCustomMessageId'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + self::assertSame('theCustomMessageId', $message->getMessageId()); + } + + public function testShouldSendCommandWithGeneratedTimestamp() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + self::assertNotEmpty($message->getTimestamp()); + } + + public function testShouldSendCommandWithCustomTimestamp() + { + $message = new Message(); + $message->setTimestamp('theCustomTimestamp'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + self::assertSame('theCustomTimestamp', $message->getTimestamp()); + } + + public function testShouldSerializeMessageToJsonByDefault() + { + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturnCallback(function (Message $message) { + $this->assertSame('{"foo":"fooVal"}', $message->getBody()); + + return $this->createDriverSendResult(); + }) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', ['foo' => 'fooVal']); + } + + public function testShouldSerializeMessageByCustomExtension() + { + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturnCallback(function (Message $message) { + $this->assertSame('theCommandBodySerializedByCustomExtension', $message->getBody()); + + return $this->createDriverSendResult(); + }) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock(), new ChainExtension([new CustomPrepareBodyClientExtension()])); + $producer->sendCommand('command', ['foo' => 'fooVal']); + } + + public function testShouldSendCommandToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturnCallback(function (Message $message) { + self::assertSame('aBody', $message->getBody()); + self::assertNull($message->getProperty(Config::PROCESSOR)); + self::assertSame('command', $message->getProperty(Config::COMMAND)); + + return $this->createDriverSendResult(); + }) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + } + + public function testThrowWhenProcessorNamePropertySetToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + $message->setProperty(Config::PROCESSOR, 'aCustomProcessor'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('sendToProcessor') + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The enqueue.processor property must not be set.'); + $producer->sendCommand('command', $message); + } + + public function testShouldCallPreSendCommandExtensionMethodWhenSendToBus() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_MESSAGE_BUS); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPreSendCommand') + ->willReturnCallback(function (PreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('command', $context->getCommand()); + + $this->assertEquals($message, $context->getOriginalMessage()); + $this->assertNotSame($message, $context->getOriginalMessage()); + }); + + $extension + ->expects($this->never()) + ->method('onPreSendEvent') + ; + + $producer->sendCommand('command', $message); + } + + public function testShouldCallPreSendCommandExtensionMethodWhenSendToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPreSendCommand') + ->willReturnCallback(function (PreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('command', $context->getCommand()); + + $this->assertEquals($message, $context->getOriginalMessage()); + $this->assertNotSame($message, $context->getOriginalMessage()); + }); + + $extension + ->expects($this->never()) + ->method('onPreSendEvent') + ; + + $producer->sendCommand('command', $message); + } + + public function testShouldCallPreDriverSendExtensionMethod() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onDriverPreSend') + ->willReturnCallback(function (DriverPreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('command', $context->getCommand()); + + $this->assertTrue($context->isEvent()); + }); + + $producer->sendCommand('command', $message); + } + + public function testShouldCallPostSendExtensionMethod() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPostSend') + ->willReturnCallback(function (PostSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('command', $context->getCommand()); + + $this->assertFalse($context->isEvent()); + }); + + $producer->sendCommand('command', $message); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createRpcFactoryMock(): RpcFactory + { + return $this->createMock(RpcFactory::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverStub(): DriverInterface + { + $config = new Config( + 'a_prefix', + '.', + 'an_app', + 'a_router_topic', + 'a_router_queue', + 'a_default_processor_queue', + 'a_router_processor_name', + [], + [] + ); + + $driverMock = $this->createMock(DriverInterface::class); + $driverMock + ->expects($this->any()) + ->method('getConfig') + ->willReturn($config) + ; + + return $driverMock; + } + + private function createDriverSendResult(): DriverSendResult + { + return new DriverSendResult( + $this->createMock(Destination::class), + $this->createMock(TransportMessage::class) + ); + } +} diff --git a/Tests/Client/ProducerSendEventTest.php b/Tests/Client/ProducerSendEventTest.php new file mode 100644 index 0000000..c92b495 --- /dev/null +++ b/Tests/Client/ProducerSendEventTest.php @@ -0,0 +1,557 @@ +createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + $driver + ->expects($this->never()) + ->method('sendToProcessor') + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + $expectedProperties = [ + 'enqueue.topic' => 'topic', + ]; + + self::assertEquals($expectedProperties, $message->getProperties()); + } + + public function testShouldOverwriteTopicProperty() + { + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topicShouldBeOverwritten'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('expectedTopic', $message); + + $expectedProperties = [ + 'enqueue.topic' => 'expectedTopic', + ]; + + self::assertEquals($expectedProperties, $message->getProperties()); + } + + public function testShouldSendEventWithoutPriorityByDefault() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + self::assertNull($message->getPriority()); + } + + public function testShouldSendEventWithCustomPriority() + { + $message = new Message(); + $message->setPriority(MessagePriority::HIGH); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + self::assertSame(MessagePriority::HIGH, $message->getPriority()); + } + + public function testShouldSendEventWithGeneratedMessageId() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + self::assertNotEmpty($message->getMessageId()); + } + + public function testShouldSendEventWithCustomMessageId() + { + $message = new Message(); + $message->setMessageId('theCustomMessageId'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + self::assertSame('theCustomMessageId', $message->getMessageId()); + } + + public function testShouldSendEventWithGeneratedTimestamp() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + self::assertNotEmpty($message->getTimestamp()); + } + + public function testShouldSendEventWithCustomTimestamp() + { + $message = new Message(); + $message->setTimestamp('theCustomTimestamp'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + self::assertSame('theCustomTimestamp', $message->getTimestamp()); + } + + public function testShouldSerializeMessageToJsonByDefault() + { + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->willReturnCallback(function (Message $message) { + $this->assertSame('{"foo":"fooVal"}', $message->getBody()); + + return $this->createDriverSendResult(); + }) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', ['foo' => 'fooVal']); + } + + public function testShouldSerializeMessageByCustomExtension() + { + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->willReturnCallback(function (Message $message) { + $this->assertSame('theEventBodySerializedByCustomExtension', $message->getBody()); + + return $this->createDriverSendResult(); + }) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock(), new ChainExtension([new CustomPrepareBodyClientExtension()])); + $producer->sendEvent('topic', ['foo' => 'fooVal']); + } + + public function testThrowIfSendEventToMessageBusWithProcessorNamePropertySet() + { + $message = new Message(); + $message->setBody(''); + $message->setProperty(Config::PROCESSOR, 'aProcessor'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('sendToRouter') + ; + $driver + ->expects($this->never()) + ->method('sendToProcessor') + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The enqueue.processor property must not be set.'); + $producer->sendEvent('topic', $message); + } + + public function testShouldSendEventToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('sendToRouter') + ; + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturnCallback(function (Message $message) { + self::assertSame('aBody', $message->getBody()); + + // null means a driver sends a message to router processor. + self::assertNull($message->getProperty(Config::PROCESSOR)); + + return $this->createDriverSendResult(); + }) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + } + + public function testThrowWhenProcessorNamePropertySetToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + $message->setProperty(Config::PROCESSOR, 'aCustomProcessor'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('sendToProcessor') + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The enqueue.processor property must not be set.'); + $producer->sendEvent('topic', $message); + } + + public function testThrowIfUnSupportedScopeGivenOnSend() + { + $message = new Message(); + $message->setScope('iDontKnowScope'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('sendToRouter') + ; + $driver + ->expects($this->never()) + ->method('sendToProcessor') + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The message scope "iDontKnowScope" is not supported.'); + $producer->sendEvent('topic', $message); + } + + public function testShouldCallPreSendEventExtensionMethodWhenSendToBus() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_MESSAGE_BUS); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPreSendEvent') + ->willReturnCallback(function (PreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('topic', $context->getTopic()); + + $this->assertEquals($message, $context->getOriginalMessage()); + $this->assertNotSame($message, $context->getOriginalMessage()); + }); + + $extension + ->expects($this->never()) + ->method('onPreSendCommand') + ; + + $producer->sendEvent('topic', $message); + } + + public function testShouldCallPreSendEventExtensionMethodWhenSendToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPreSendEvent') + ->willReturnCallback(function (PreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('topic', $context->getTopic()); + + $this->assertEquals($message, $context->getOriginalMessage()); + $this->assertNotSame($message, $context->getOriginalMessage()); + }); + + $extension + ->expects($this->never()) + ->method('onPreSendCommand') + ; + + $producer->sendEvent('topic', $message); + } + + public function testShouldCallPreDriverSendExtensionMethodWhenSendToMessageBus() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_MESSAGE_BUS); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onDriverPreSend') + ->willReturnCallback(function (DriverPreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('topic', $context->getTopic()); + + $this->assertTrue($context->isEvent()); + }); + + $producer->sendEvent('topic', $message); + } + + public function testShouldCallPreDriverSendExtensionMethodWhenSendToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onDriverPreSend') + ->willReturnCallback(function (DriverPreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('topic', $context->getTopic()); + + $this->assertTrue($context->isEvent()); + }); + + $producer->sendEvent('topic', $message); + } + + public function testShouldCallPostSendExtensionMethodWhenSendToMessageBus() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_MESSAGE_BUS); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPostSend') + ->willReturnCallback(function (PostSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('topic', $context->getTopic()); + + $this->assertTrue($context->isEvent()); + }); + + $producer->sendEvent('topic', $message); + } + + public function testShouldCallPostSendExtensionMethodWhenSendToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPostSend') + ->willReturnCallback(function (PostSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('topic', $context->getTopic()); + + $this->assertTrue($context->isEvent()); + }); + + $producer->sendEvent('topic', $message); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createRpcFactoryMock(): RpcFactory + { + return $this->createMock(RpcFactory::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverStub(): DriverInterface + { + $config = new Config( + 'a_prefix', + '.', + 'an_app', + 'a_router_topic', + 'a_router_queue', + 'a_default_processor_queue', + 'a_router_processor_name', + [], + [] + ); + + $driverMock = $this->createMock(DriverInterface::class); + $driverMock + ->expects($this->any()) + ->method('getConfig') + ->willReturn($config) + ; + + return $driverMock; + } + + private function createDriverSendResult(): DriverSendResult + { + return new DriverSendResult( + $this->createMock(Destination::class), + $this->createMock(TransportMessage::class) + ); + } +} diff --git a/Tests/Client/ProducerTest.php b/Tests/Client/ProducerTest.php index c9a30fc..23b004a 100644 --- a/Tests/Client/ProducerTest.php +++ b/Tests/Client/ProducerTest.php @@ -2,14 +2,9 @@ namespace Enqueue\Tests\Client; -use Enqueue\Client\Config; use Enqueue\Client\DriverInterface; -use Enqueue\Client\ExtensionInterface; -use Enqueue\Client\Message; -use Enqueue\Client\MessagePriority; use Enqueue\Client\Producer; use Enqueue\Client\ProducerInterface; -use Enqueue\Null\NullQueue; use Enqueue\Rpc\RpcFactory; use Enqueue\Test\ClassExtensionTrait; use PHPUnit\Framework\TestCase; @@ -23,619 +18,24 @@ public function testShouldImplementProducerInterface() self::assertClassImplements(ProducerInterface::class, Producer::class); } - public function testCouldBeConstructedWithDriverAsFirstArgument() + public function testShouldBeFinal() { - new Producer($this->createDriverStub(), $this->createRpcFactory()); - } - - public function testShouldSendMessageToRouter() - { - $message = new Message(); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - $expectedProperties = [ - 'enqueue.topic_name' => 'topic', - ]; - - self::assertEquals($expectedProperties, $message->getProperties()); - } - - public function testShouldSendMessageWithNormalPriorityByDefault() - { - $message = new Message(); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - self::assertSame(MessagePriority::NORMAL, $message->getPriority()); - } - - public function testShouldSendMessageWithCustomPriority() - { - $message = new Message(); - $message->setPriority(MessagePriority::HIGH); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - self::assertSame(MessagePriority::HIGH, $message->getPriority()); - } - - public function testShouldSendMessageWithGeneratedMessageId() - { - $message = new Message(); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - self::assertNotEmpty($message->getMessageId()); - } - - public function testShouldSendMessageWithCustomMessageId() - { - $message = new Message(); - $message->setMessageId('theCustomMessageId'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - self::assertSame('theCustomMessageId', $message->getMessageId()); - } - - public function testShouldSendMessageWithGeneratedTimestamp() - { - $message = new Message(); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - self::assertNotEmpty($message->getTimestamp()); - } - - public function testShouldSendMessageWithCustomTimestamp() - { - $message = new Message(); - $message->setTimestamp('theCustomTimestamp'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - self::assertSame('theCustomTimestamp', $message->getTimestamp()); - } - - public function testShouldSendStringAsPlainText() - { - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertSame('theStringMessage', $message->getBody()); - self::assertSame('text/plain', $message->getContentType()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', 'theStringMessage'); - } - - public function testShouldSendArrayAsJsonString() - { - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertSame('{"foo":"fooVal"}', $message->getBody()); - self::assertSame('application/json', $message->getContentType()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', ['foo' => 'fooVal']); - } - - public function testShouldConvertMessageArrayBodyJsonString() - { - $message = new Message(); - $message->setBody(['foo' => 'fooVal']); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertSame('{"foo":"fooVal"}', $message->getBody()); - self::assertSame('application/json', $message->getContentType()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - } - - public function testSendShouldForceScalarsToStringAndSetTextContentType() - { - $queue = new NullQueue(''); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertEquals('text/plain', $message->getContentType()); - - self::assertInternalType('string', $message->getBody()); - self::assertEquals('12345', $message->getBody()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent($queue, 12345); - } - - public function testSendShouldForceMessageScalarsBodyToStringAndSetTextContentType() - { - $queue = new NullQueue(''); - - $message = new Message(); - $message->setBody(12345); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertEquals('text/plain', $message->getContentType()); - - self::assertInternalType('string', $message->getBody()); - self::assertEquals('12345', $message->getBody()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent($queue, $message); - } - - public function testSendShouldForceNullToEmptyStringAndSetTextContentType() - { - $queue = new NullQueue(''); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertEquals('text/plain', $message->getContentType()); - - self::assertInternalType('string', $message->getBody()); - self::assertEquals('', $message->getBody()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent($queue, null); - } - - public function testSendShouldForceNullBodyToEmptyStringAndSetTextContentType() - { - $queue = new NullQueue(''); - - $message = new Message(); - $message->setBody(null); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertEquals('text/plain', $message->getContentType()); - - self::assertInternalType('string', $message->getBody()); - self::assertEquals('', $message->getBody()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent($queue, $message); - } - - public function testShouldThrowExceptionIfBodyIsObjectOnSend() - { - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The message\'s body must be either null, scalar, array or object (implements \JsonSerializable). Got: stdClass'); - - $producer->sendEvent('topic', new \stdClass()); - } - - public function testShouldThrowExceptionIfBodyIsArrayWithObjectsInsideOnSend() - { - $queue = new NullQueue('queue'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The message\'s body must be an array of scalars. Found not scalar in the array: stdClass'); - - $producer->sendEvent($queue, ['foo' => new \stdClass()]); - } - - public function testShouldThrowExceptionIfBodyIsArrayWithObjectsInSubArraysInsideOnSend() - { - $queue = new NullQueue('queue'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The message\'s body must be an array of scalars. Found not scalar in the array: stdClass'); - - $producer->sendEvent($queue, ['foo' => ['bar' => new \stdClass()]]); - } - - public function testShouldSendJsonSerializableObjectAsJsonStringToMessageBus() - { - $object = new JsonSerializableObject(); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertSame('{"foo":"fooVal"}', $message->getBody()); - self::assertSame('application/json', $message->getContentType()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $object); - } - - public function testShouldSendMessageJsonSerializableBodyAsJsonStringToMessageBus() - { - $object = new JsonSerializableObject(); - - $message = new Message(); - $message->setBody($object); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertSame('{"foo":"fooVal"}', $message->getBody()); - self::assertSame('application/json', $message->getContentType()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - } - - public function testThrowIfTryToSendMessageToMessageBusWithProcessorNamePropertySet() - { - $object = new JsonSerializableObject(); - - $message = new Message(); - $message->setBody($object); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'aProcessor'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The enqueue.processor_name property must not be set for messages that are sent to message bus.'); - $producer->sendEvent('topic', $message); - } - - public function testThrowIfTryToSendMessageToMessageBusWithProcessorQueueNamePropertySet() - { - $object = new JsonSerializableObject(); - - $message = new Message(); - $message->setBody($object); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aProcessorQueue'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The enqueue.processor_queue_name property must not be set for messages that are sent to message bus.'); - $producer->sendEvent('topic', $message); - } - - public function testThrowIfNotApplicationJsonContentTypeSetWithJsonSerializableBody() - { - $object = new JsonSerializableObject(); - - $message = new Message(); - $message->setBody($object); - $message->setContentType('foo/bar'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Content type "application/json" only allowed when body is array'); - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - } - - public function testShouldSendMessageToApplicationRouter() - { - $message = new Message(); - $message->setBody('aBody'); - $message->setScope(Message::SCOPE_APP); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->once()) - ->method('sendToProcessor') - ->willReturnCallback(function (Message $message) { - self::assertSame('aBody', $message->getBody()); - self::assertSame('a_router_processor_name', $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)); - self::assertSame('a_router_queue', $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - } - - public function testShouldSendToCustomMessageToApplicationRouter() - { - $message = new Message(); - $message->setBody('aBody'); - $message->setScope(Message::SCOPE_APP); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'aCustomProcessor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aCustomProcessorQueue'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->once()) - ->method('sendToProcessor') - ->willReturnCallback(function (Message $message) { - self::assertSame('aBody', $message->getBody()); - self::assertSame('aCustomProcessor', $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)); - self::assertSame('aCustomProcessorQueue', $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - } - - public function testThrowIfUnSupportedScopeGivenOnSend() - { - $message = new Message(); - $message->setScope('iDontKnowScope'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The message scope "iDontKnowScope" is not supported.'); - $producer->sendEvent('topic', $message); - } - - public function testShouldCallPreSendPostSendExtensionMethodsWhenSendToRouter() - { - $message = new Message(); - $message->setBody('aBody'); - $message->setScope(Message::SCOPE_MESSAGE_BUS); - - $extension = $this->createMock(ExtensionInterface::class); - $extension - ->expects($this->at(0)) - ->method('onPreSend') - ->with($this->identicalTo('topic'), $this->identicalTo($message)) - ; - $extension - ->expects($this->at(1)) - ->method('onPostSend') - ->with($this->identicalTo('topic'), $this->identicalTo($message)) - ; - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ; - - $producer = new Producer($driver, $this->createRpcFactory(), $extension); - $producer->sendEvent('topic', $message); - } - - public function testShouldCallPreSendPostSendExtensionMethodsWhenSendToProcessor() - { - $message = new Message(); - $message->setBody('aBody'); - $message->setScope(Message::SCOPE_APP); - - $extension = $this->createMock(ExtensionInterface::class); - $extension - ->expects($this->at(0)) - ->method('onPreSend') - ->with($this->identicalTo('topic'), $this->identicalTo($message)) - ; - $extension - ->expects($this->at(1)) - ->method('onPostSend') - ->with($this->identicalTo('topic'), $this->identicalTo($message)) - ; - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory(), $extension); - $producer->sendEvent('topic', $message); + self::assertClassFinal(Producer::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|RpcFactory + * @return \PHPUnit\Framework\MockObject\MockObject */ - private function createRpcFactory() + private function createRpcFactoryMock(): RpcFactory { return $this->createMock(RpcFactory::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface + * @return \PHPUnit\Framework\MockObject\MockObject */ - private function createDriverStub() - { - $config = new Config( - 'a_prefix', - 'an_app', - 'a_router_topic', - 'a_router_queue', - 'a_default_processor_queue', - 'a_router_processor_name' - ); - - $driverMock = $this->createMock(DriverInterface::class); - $driverMock - ->expects($this->any()) - ->method('getConfig') - ->willReturn($config) - ; - - return $driverMock; - } -} - -class JsonSerializableObject implements \JsonSerializable -{ - public function jsonSerialize() + private function createDriverMock(): DriverInterface { - return ['foo' => 'fooVal']; + return $this->createMock(DriverInterface::class); } } diff --git a/Tests/Client/ResourcesTest.php b/Tests/Client/ResourcesTest.php new file mode 100644 index 0000000..e79fb9d --- /dev/null +++ b/Tests/Client/ResourcesTest.php @@ -0,0 +1,159 @@ +assertTrue($rc->isFinal()); + } + + public function testShouldConstructorBePrivate() + { + $rc = new \ReflectionClass(Resources::class); + + $this->assertTrue($rc->getConstructor()->isPrivate()); + } + + public function testShouldGetAvailableDriverInExpectedFormat() + { + $availableDrivers = Resources::getAvailableDrivers(); + + self::assertIsArray($availableDrivers); + $this->assertGreaterThan(0, count($availableDrivers)); + + $driverInfo = $availableDrivers[0]; + + $this->assertArrayHasKey('driverClass', $driverInfo); + $this->assertSame(AmqpDriver::class, $driverInfo['driverClass']); + + $this->assertArrayHasKey('schemes', $driverInfo); + $this->assertSame(['amqp', 'amqps'], $driverInfo['schemes']); + + $this->assertArrayHasKey('requiredSchemeExtensions', $driverInfo); + $this->assertSame([], $driverInfo['requiredSchemeExtensions']); + + $this->assertArrayHasKey('packages', $driverInfo); + $this->assertSame(['enqueue/enqueue', 'enqueue/amqp-bunny'], $driverInfo['packages']); + } + + public function testShouldGetAvailableDriverWithRequiredExtensionInExpectedFormat() + { + $availableDrivers = Resources::getAvailableDrivers(); + + self::assertIsArray($availableDrivers); + $this->assertGreaterThan(0, count($availableDrivers)); + + $driverInfo = $availableDrivers[1]; + + $this->assertArrayHasKey('driverClass', $driverInfo); + $this->assertSame(RabbitMqDriver::class, $driverInfo['driverClass']); + + $this->assertArrayHasKey('schemes', $driverInfo); + $this->assertSame(['amqp', 'amqps'], $driverInfo['schemes']); + + $this->assertArrayHasKey('requiredSchemeExtensions', $driverInfo); + $this->assertSame(['rabbitmq'], $driverInfo['requiredSchemeExtensions']); + + $this->assertArrayHasKey('packages', $driverInfo); + $this->assertSame(['enqueue/enqueue', 'enqueue/amqp-bunny'], $driverInfo['packages']); + } + + public function testShouldGetKnownDriversInExpectedFormat() + { + $knownDrivers = Resources::getAvailableDrivers(); + + self::assertIsArray($knownDrivers); + $this->assertGreaterThan(0, count($knownDrivers)); + + $driverInfo = $knownDrivers[0]; + + $this->assertArrayHasKey('driverClass', $driverInfo); + $this->assertSame(AmqpDriver::class, $driverInfo['driverClass']); + + $this->assertArrayHasKey('schemes', $driverInfo); + $this->assertSame(['amqp', 'amqps'], $driverInfo['schemes']); + + $this->assertArrayHasKey('requiredSchemeExtensions', $driverInfo); + $this->assertSame([], $driverInfo['requiredSchemeExtensions']); + + $this->assertArrayHasKey('packages', $driverInfo); + $this->assertSame(['enqueue/enqueue', 'enqueue/amqp-bunny'], $driverInfo['packages']); + } + + public function testThrowsIfDriverClassNotImplementDriverFactoryInterfaceOnAddDriver() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The driver class "stdClass" must implement "Enqueue\Client\DriverInterface" interface.'); + + Resources::addDriver(\stdClass::class, [], [], ['foo']); + } + + public function testThrowsIfNoSchemesProvidedOnAddDriver() + { + $driverClass = $this->getMockClass(DriverInterface::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Schemes could not be empty.'); + + Resources::addDriver($driverClass, [], [], ['foo']); + } + + public function testThrowsIfNoPackageProvidedOnAddDriver() + { + $driverClass = $this->getMockClass(DriverInterface::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Packages could not be empty.'); + + Resources::addDriver($driverClass, ['foo'], [], []); + } + + public function testShouldAllowRegisterDriverThatIsNotInstalled() + { + Resources::addDriver('theDriverClass', ['foo'], ['barExtension'], ['foo']); + + $availableDrivers = Resources::getKnownDrivers(); + + $driverInfo = end($availableDrivers); + + $this->assertSame('theDriverClass', $driverInfo['driverClass']); + } + + public function testShouldAllowGetPreviouslyRegisteredDriver() + { + $driverClass = $this->getMockClass(DriverInterface::class); + + Resources::addDriver( + $driverClass, + ['fooscheme', 'barscheme'], + ['fooextension', 'barextension'], + ['foo/bar'] + ); + + $availableDrivers = Resources::getAvailableDrivers(); + + $driverInfo = end($availableDrivers); + + $this->assertArrayHasKey('driverClass', $driverInfo); + $this->assertSame($driverClass, $driverInfo['driverClass']); + + $this->assertArrayHasKey('schemes', $driverInfo); + $this->assertSame(['fooscheme', 'barscheme'], $driverInfo['schemes']); + + $this->assertArrayHasKey('requiredSchemeExtensions', $driverInfo); + $this->assertSame(['fooextension', 'barextension'], $driverInfo['requiredSchemeExtensions']); + + $this->assertArrayHasKey('packages', $driverInfo); + $this->assertSame(['foo/bar'], $driverInfo['packages']); + } +} diff --git a/Tests/Client/RouterProcessorTest.php b/Tests/Client/RouterProcessorTest.php index e362c1e..7d29711 100644 --- a/Tests/Client/RouterProcessorTest.php +++ b/Tests/Client/RouterProcessorTest.php @@ -4,203 +4,218 @@ use Enqueue\Client\Config; use Enqueue\Client\DriverInterface; +use Enqueue\Client\DriverSendResult; use Enqueue\Client\Message; +use Enqueue\Client\Route; +use Enqueue\Client\RouteCollection; use Enqueue\Client\RouterProcessor; use Enqueue\Consumption\Result; use Enqueue\Null\NullContext; use Enqueue\Null\NullMessage; +use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\Destination; +use Interop\Queue\Message as TransportMessage; +use Interop\Queue\Processor; use PHPUnit\Framework\TestCase; class RouterProcessorTest extends TestCase { - public function testCouldBeConstructedWithDriverAsFirstArgument() + use ClassExtensionTrait; + use ReadAttributeTrait; + + public function testShouldImplementProcessorInterface() + { + $this->assertClassImplements(Processor::class, RouterProcessor::class); + } + + public function testShouldBeFinal() { - new RouterProcessor($this->createDriverMock()); + $this->assertClassFinal(RouterProcessor::class); } - public function testCouldBeConstructedWithSessionAndRoutes() + public function testCouldBeConstructedWithDriver() { - $routes = [ - 'aTopicName' => [['aProcessorName', 'aQueueName']], - 'anotherTopicName' => [['aProcessorName', 'aQueueName']], - ]; + $driver = $this->createDriverStub(); - $router = new RouterProcessor($this->createDriverMock(), $routes); + $processor = new RouterProcessor($driver); - $this->assertAttributeEquals($routes, 'eventRoutes', $router); + $this->assertAttributeSame($driver, 'driver', $processor); } - public function testShouldRejectIfTopicNameParameterIsNotSet() + public function testShouldRejectIfTopicNotSet() { - $router = new RouterProcessor($this->createDriverMock()); + $router = new RouterProcessor($this->createDriverStub()); $result = $router->process(new NullMessage(), new NullContext()); - $this->assertInstanceOf(Result::class, $result); $this->assertEquals(Result::REJECT, $result->getStatus()); - $this->assertEquals('Got message without required parameter: "enqueue.topic_name"', $result->getReason()); + $this->assertEquals('Topic property "enqueue.topic" is required but not set or empty.', $result->getReason()); + } + + public function testShouldRejectIfCommandSet() + { + $router = new RouterProcessor($this->createDriverStub()); + + $message = new NullMessage(); + $message->setProperty(Config::COMMAND, 'aCommand'); + + $result = $router->process($message, new NullContext()); + + $this->assertEquals(Result::REJECT, $result->getStatus()); + $this->assertEquals('Unexpected command "aCommand" got. Command must not go to the router.', $result->getReason()); } - public function testShouldRouteOriginalMessageToEventRecipient() + public function testShouldRouteOriginalMessageToAllRecipients() { $message = new NullMessage(); $message->setBody('theBody'); $message->setHeaders(['aHeader' => 'aHeaderVal']); - $message->setProperties(['aProp' => 'aPropVal', Config::PARAMETER_TOPIC_NAME => 'theTopicName']); + $message->setProperties(['aProp' => 'aPropVal', Config::TOPIC => 'theTopicName']); - $clientMessage = new Message(); + /** @var Message[] $routedMessages */ + $routedMessages = new \ArrayObject(); - $routedMessage = null; + $routeCollection = new RouteCollection([ + new Route('theTopicName', Route::TOPIC, 'aFooProcessor'), + new Route('theTopicName', Route::TOPIC, 'aBarProcessor'), + new Route('theTopicName', Route::TOPIC, 'aBazProcessor'), + ]); - $driver = $this->createDriverMock(); + $driver = $this->createDriverStub($routeCollection); $driver - ->expects($this->once()) + ->expects($this->exactly(3)) ->method('sendToProcessor') - ->with($this->identicalTo($clientMessage)) + ->willReturnCallback(function (Message $message) use ($routedMessages) { + $routedMessages->append($message); + + return $this->createDriverSendResult(); + }) ; $driver - ->expects($this->once()) + ->expects($this->exactly(3)) ->method('createClientMessage') - ->willReturnCallback(function (NullMessage $message) use (&$routedMessage, $clientMessage) { - $routedMessage = $message; - - return $clientMessage; + ->willReturnCallback(function (NullMessage $message) { + return new Message($message->getBody(), $message->getProperties(), $message->getHeaders()); }) ; - $routes = [ - 'theTopicName' => [['aFooProcessor', 'aQueueName']], - ]; + $processor = new RouterProcessor($driver); - $router = new RouterProcessor($driver, $routes); + $result = $processor->process($message, new NullContext()); - $result = $router->process($message, new NullContext()); + $this->assertEquals(Result::ACK, $result->getStatus()); + $this->assertEquals('Routed to "3" event subscribers', $result->getReason()); + + $this->assertContainsOnly(Message::class, $routedMessages); + $this->assertCount(3, $routedMessages); - $this->assertEquals(Result::ACK, $result); - $this->assertEquals([ - 'aProp' => 'aPropVal', - 'enqueue.topic_name' => 'theTopicName', - 'enqueue.processor_name' => 'aFooProcessor', - 'enqueue.processor_queue_name' => 'aQueueName', - ], $routedMessage->getProperties()); + $this->assertSame('aFooProcessor', $routedMessages[0]->getProperty(Config::PROCESSOR)); + $this->assertSame('aBarProcessor', $routedMessages[1]->getProperty(Config::PROCESSOR)); + $this->assertSame('aBazProcessor', $routedMessages[2]->getProperty(Config::PROCESSOR)); } - public function testShouldRouteOriginalMessageToCommandRecipient() + public function testShouldDoNothingIfNoRoutes() { $message = new NullMessage(); $message->setBody('theBody'); $message->setHeaders(['aHeader' => 'aHeaderVal']); - $message->setProperties([ - 'aProp' => 'aPropVal', - Config::PARAMETER_TOPIC_NAME => Config::COMMAND_TOPIC, - Config::PARAMETER_COMMAND_NAME => 'theCommandName', - ]); + $message->setProperties(['aProp' => 'aPropVal', Config::TOPIC => 'theTopicName']); - $clientMessage = new Message(); + /** @var Message[] $routedMessages */ + $routedMessages = new \ArrayObject(); - $routedMessage = null; + $routeCollection = new RouteCollection([]); - $driver = $this->createDriverMock(); + $driver = $this->createDriverStub($routeCollection); $driver - ->expects($this->once()) + ->expects($this->never()) ->method('sendToProcessor') - ->with($this->identicalTo($clientMessage)) + ->willReturnCallback(function (Message $message) use ($routedMessages) { + $routedMessages->append($message); + }) ; $driver - ->expects($this->once()) + ->expects($this->never()) ->method('createClientMessage') - ->willReturnCallback(function (NullMessage $message) use (&$routedMessage, $clientMessage) { - $routedMessage = $message; - - return $clientMessage; + ->willReturnCallback(function (NullMessage $message) { + return new Message($message->getBody(), $message->getProperties(), $message->getHeaders()); }) ; - $routes = [ - 'theCommandName' => 'aQueueName', - ]; + $processor = new RouterProcessor($driver); - $router = new RouterProcessor($driver, [], $routes); + $result = $processor->process($message, new NullContext()); - $result = $router->process($message, new NullContext()); + $this->assertEquals(Result::ACK, $result->getStatus()); + $this->assertEquals('Routed to "0" event subscribers', $result->getReason()); - $this->assertEquals(Result::ACK, $result); - $this->assertEquals([ - 'aProp' => 'aPropVal', - 'enqueue.topic_name' => Config::COMMAND_TOPIC, - 'enqueue.processor_name' => 'theCommandName', - 'enqueue.command_name' => 'theCommandName', - 'enqueue.processor_queue_name' => 'aQueueName', - ], $routedMessage->getProperties()); + $this->assertCount(0, $routedMessages); } - public function testShouldRejectCommandMessageIfCommandNamePropertyMissing() + public function testShouldDoNotModifyOriginalMessage() { $message = new NullMessage(); $message->setBody('theBody'); $message->setHeaders(['aHeader' => 'aHeaderVal']); - $message->setProperties([ - 'aProp' => 'aPropVal', - Config::PARAMETER_TOPIC_NAME => Config::COMMAND_TOPIC, + $message->setProperties(['aProp' => 'aPropVal', Config::TOPIC => 'theTopicName']); + + /** @var Message[] $routedMessages */ + $routedMessages = new \ArrayObject(); + + $routeCollection = new RouteCollection([ + new Route('theTopicName', Route::TOPIC, 'aFooProcessor'), + new Route('theTopicName', Route::TOPIC, 'aBarProcessor'), ]); - $driver = $this->createDriverMock(); + $driver = $this->createDriverStub($routeCollection); $driver - ->expects($this->never()) + ->expects($this->atLeastOnce()) ->method('sendToProcessor') - ; + ->willReturnCallback(function (Message $message) use ($routedMessages) { + $routedMessages->append($message); + + return $this->createDriverSendResult(); + }); $driver - ->expects($this->never()) + ->expects($this->atLeastOnce()) ->method('createClientMessage') - ; - - $routes = [ - 'theCommandName' => 'aQueueName', - ]; - - $router = new RouterProcessor($driver, [], $routes); - - $result = $router->process($message, new NullContext()); - - $this->assertInstanceOf(Result::class, $result); - $this->assertEquals(Result::REJECT, $result->getStatus()); - $this->assertEquals('Got message without required parameter: "enqueue.command_name"', $result->getReason()); - } - - public function testShouldAddEventRoute() - { - $router = new RouterProcessor($this->createDriverMock(), []); + ->willReturnCallback(function (NullMessage $message) { + return new Message($message->getBody(), $message->getProperties(), $message->getHeaders()); + }); - $this->assertAttributeSame([], 'eventRoutes', $router); + $processor = new RouterProcessor($driver); - $router->add('theTopicName', 'theQueueName', 'aProcessorName'); + $result = $processor->process($message, new NullContext()); - $this->assertAttributeSame([ - 'theTopicName' => [ - ['aProcessorName', 'theQueueName'], - ], - ], 'eventRoutes', $router); + // guard + $this->assertEquals(Result::ACK, $result->getStatus()); - $this->assertAttributeSame([], 'commandRoutes', $router); + $this->assertSame('theBody', $message->getBody()); + $this->assertSame(['aProp' => 'aPropVal', Config::TOPIC => 'theTopicName'], $message->getProperties()); + $this->assertSame(['aHeader' => 'aHeaderVal'], $message->getHeaders()); } - public function testShouldAddCommandRoute() + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface + */ + private function createDriverStub(?RouteCollection $routeCollection = null): DriverInterface { - $router = new RouterProcessor($this->createDriverMock(), []); - - $this->assertAttributeSame([], 'eventRoutes', $router); - - $router->add(Config::COMMAND_TOPIC, 'theQueueName', 'aProcessorName'); + $driver = $this->createMock(DriverInterface::class); + $driver + ->expects($this->any()) + ->method('getRouteCollection') + ->willReturn($routeCollection ?? new RouteCollection([])) + ; - $this->assertAttributeSame(['aProcessorName' => 'theQueueName'], 'commandRoutes', $router); - $this->assertAttributeSame([], 'eventRoutes', $router); + return $driver; } - /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface - */ - protected function createDriverMock() + private function createDriverSendResult(): DriverSendResult { - return $this->createMock(DriverInterface::class); + return new DriverSendResult( + $this->createMock(Destination::class), + $this->createMock(TransportMessage::class) + ); } } diff --git a/Tests/Client/SpoolProducerTest.php b/Tests/Client/SpoolProducerTest.php index 8c00ded..014fe49 100644 --- a/Tests/Client/SpoolProducerTest.php +++ b/Tests/Client/SpoolProducerTest.php @@ -18,11 +18,6 @@ public function testShouldImplementProducerInterface() self::assertClassImplements(ProducerInterface::class, SpoolProducer::class); } - public function testCouldBeConstructedWithRealProducer() - { - new SpoolProducer($this->createProducerMock()); - } - public function testShouldQueueEventMessageOnSend() { $message = new Message(); @@ -154,7 +149,7 @@ public function testShouldSendImmediatelyCommandMessageWithNeedReplyTrue() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ProducerInterface + * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface */ protected function createProducerMock() { diff --git a/Tests/Client/TraceableProducerTest.php b/Tests/Client/TraceableProducerTest.php index 42c9c8b..b0df066 100644 --- a/Tests/Client/TraceableProducerTest.php +++ b/Tests/Client/TraceableProducerTest.php @@ -2,7 +2,7 @@ namespace Enqueue\Tests\Client; -use Enqueue\Client\Config; +use DMS\PHPUnitExtensions\ArraySubset\Assert; use Enqueue\Client\Message; use Enqueue\Client\ProducerInterface; use Enqueue\Client\TraceableProducer; @@ -18,11 +18,6 @@ public function testShouldImplementProducerInterface() $this->assertClassImplements(ProducerInterface::class, TraceableProducer::class); } - public function testCouldBeConstructedWithInternalMessageProducer() - { - new TraceableProducer($this->createProducerMock()); - } - public function testShouldPassAllArgumentsToInternalEventMessageProducerSendMethod() { $topic = 'theTopic'; @@ -46,7 +41,7 @@ public function testShouldCollectInfoIfStringGivenAsEventMessage() $producer->sendEvent('aFooTopic', 'aFooBody'); - $this->assertSame([ + Assert::assertArraySubset([ [ 'topic' => 'aFooTopic', 'command' => null, @@ -61,6 +56,8 @@ public function testShouldCollectInfoIfStringGivenAsEventMessage() 'messageId' => null, ], ], $producer->getTraces()); + + $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]); } public function testShouldCollectInfoIfArrayGivenAsEventMessage() @@ -69,7 +66,7 @@ public function testShouldCollectInfoIfArrayGivenAsEventMessage() $producer->sendEvent('aFooTopic', ['foo' => 'fooVal', 'bar' => 'barVal']); - $this->assertSame([ + Assert::assertArraySubset([ [ 'topic' => 'aFooTopic', 'command' => null, @@ -84,6 +81,8 @@ public function testShouldCollectInfoIfArrayGivenAsEventMessage() 'messageId' => null, ], ], $producer->getTraces()); + + $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]); } public function testShouldCollectInfoIfEventMessageObjectGivenAsMessage() @@ -103,7 +102,7 @@ public function testShouldCollectInfoIfEventMessageObjectGivenAsMessage() $producer->sendEvent('aFooTopic', $message); - $this->assertSame([ + Assert::assertArraySubset([ [ 'topic' => 'aFooTopic', 'command' => null, @@ -118,6 +117,8 @@ public function testShouldCollectInfoIfEventMessageObjectGivenAsMessage() 'messageId' => 'theMessageId', ], ], $producer->getTraces()); + + $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]); } public function testShouldNotStoreAnythingIfInternalEventMessageProducerThrowsException() @@ -163,9 +164,9 @@ public function testShouldCollectInfoIfStringGivenAsCommandMessage() $producer->sendCommand('aFooCommand', 'aFooBody'); - $this->assertSame([ + Assert::assertArraySubset([ [ - 'topic' => Config::COMMAND_TOPIC, + 'topic' => null, 'command' => 'aFooCommand', 'body' => 'aFooBody', 'headers' => [], @@ -178,6 +179,8 @@ public function testShouldCollectInfoIfStringGivenAsCommandMessage() 'messageId' => null, ], ], $producer->getTraces()); + + $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]); } public function testShouldCollectInfoIfArrayGivenAsCommandMessage() @@ -186,9 +189,9 @@ public function testShouldCollectInfoIfArrayGivenAsCommandMessage() $producer->sendCommand('aFooCommand', ['foo' => 'fooVal', 'bar' => 'barVal']); - $this->assertSame([ + Assert::assertArraySubset([ [ - 'topic' => Config::COMMAND_TOPIC, + 'topic' => null, 'command' => 'aFooCommand', 'body' => ['foo' => 'fooVal', 'bar' => 'barVal'], 'headers' => [], @@ -201,6 +204,8 @@ public function testShouldCollectInfoIfArrayGivenAsCommandMessage() 'messageId' => null, ], ], $producer->getTraces()); + + $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]); } public function testShouldCollectInfoIfCommandMessageObjectGivenAsMessage() @@ -220,9 +225,9 @@ public function testShouldCollectInfoIfCommandMessageObjectGivenAsMessage() $producer->sendCommand('aFooCommand', $message); - $this->assertSame([ + Assert::assertArraySubset([ [ - 'topic' => Config::COMMAND_TOPIC, + 'topic' => null, 'command' => 'aFooCommand', 'body' => ['foo' => 'fooVal', 'bar' => 'barVal'], 'headers' => ['fooHeader' => 'fooVal'], @@ -235,6 +240,8 @@ public function testShouldCollectInfoIfCommandMessageObjectGivenAsMessage() 'messageId' => 'theMessageId', ], ], $producer->getTraces()); + + $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]); } public function testShouldNotStoreAnythingIfInternalCommandMessageProducerThrowsException() @@ -264,9 +271,9 @@ public function testShouldAllowGetInfoSentToSameTopic() $producer->sendEvent('aFooTopic', 'aFooBody'); $producer->sendEvent('aFooTopic', 'aFooBody'); - $this->assertArraySubset([ - ['topic' => 'aFooTopic', 'body' => 'aFooBody'], - ['topic' => 'aFooTopic', 'body' => 'aFooBody'], + Assert::assertArraySubset([ + ['topic' => 'aFooTopic', 'body' => 'aFooBody'], + ['topic' => 'aFooTopic', 'body' => 'aFooBody'], ], $producer->getTraces()); } @@ -277,7 +284,7 @@ public function testShouldAllowGetInfoSentToDifferentTopics() $producer->sendEvent('aFooTopic', 'aFooBody'); $producer->sendEvent('aBarTopic', 'aBarBody'); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['topic' => 'aFooTopic', 'body' => 'aFooBody'], ['topic' => 'aBarTopic', 'body' => 'aBarBody'], ], $producer->getTraces()); @@ -290,11 +297,11 @@ public function testShouldAllowGetInfoSentToSpecialTopic() $producer->sendEvent('aFooTopic', 'aFooBody'); $producer->sendEvent('aBarTopic', 'aBarBody'); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['topic' => 'aFooTopic', 'body' => 'aFooBody'], ], $producer->getTopicTraces('aFooTopic')); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['topic' => 'aBarTopic', 'body' => 'aBarBody'], ], $producer->getTopicTraces('aBarTopic')); } @@ -306,7 +313,7 @@ public function testShouldAllowGetInfoSentToSameCommand() $producer->sendCommand('aFooCommand', 'aFooBody'); $producer->sendCommand('aFooCommand', 'aFooBody'); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['command' => 'aFooCommand', 'body' => 'aFooBody'], ['command' => 'aFooCommand', 'body' => 'aFooBody'], ], $producer->getTraces()); @@ -319,7 +326,7 @@ public function testShouldAllowGetInfoSentToDifferentCommands() $producer->sendCommand('aFooCommand', 'aFooBody'); $producer->sendCommand('aBarCommand', 'aBarBody'); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['command' => 'aFooCommand', 'body' => 'aFooBody'], ['command' => 'aBarCommand', 'body' => 'aBarBody'], ], $producer->getTraces()); @@ -332,11 +339,11 @@ public function testShouldAllowGetInfoSentToSpecialCommand() $producer->sendCommand('aFooCommand', 'aFooBody'); $producer->sendCommand('aBarCommand', 'aBarBody'); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['command' => 'aFooCommand', 'body' => 'aFooBody'], ], $producer->getCommandTraces('aFooCommand')); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['command' => 'aBarCommand', 'body' => 'aBarBody'], ], $producer->getCommandTraces('aBarCommand')); } @@ -347,7 +354,7 @@ public function testShouldAllowClearStoredTraces() $producer->sendEvent('aFooTopic', 'aFooBody'); - //guard + // guard $this->assertNotEmpty($producer->getTraces()); $producer->clearTraces(); @@ -355,7 +362,7 @@ public function testShouldAllowClearStoredTraces() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ProducerInterface + * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface */ protected function createProducerMock() { diff --git a/Tests/ConnectionFactoryFactoryTest.php b/Tests/ConnectionFactoryFactoryTest.php new file mode 100644 index 0000000..b6b5b4d --- /dev/null +++ b/Tests/ConnectionFactoryFactoryTest.php @@ -0,0 +1,183 @@ +assertTrue($rc->implementsInterface(ConnectionFactoryFactoryInterface::class)); + } + + public function testShouldBeFinal() + { + $rc = new \ReflectionClass(ConnectionFactoryFactory::class); + + $this->assertTrue($rc->isFinal()); + } + + /** + * @doesNotPerformAssertions + */ + public function testShouldAcceptStringDSN() + { + $factory = new ConnectionFactoryFactory(); + + $factory->create('null:'); + } + + /** + * @doesNotPerformAssertions + */ + public function testShouldAcceptArrayWithDsnKey() + { + $factory = new ConnectionFactoryFactory(); + + $factory->create(['dsn' => 'null:']); + } + + public function testThrowIfInvalidConfigGiven() + { + $factory = new ConnectionFactoryFactory(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The config must be either array or DSN string.'); + $factory->create(new \stdClass()); + } + + public function testThrowIfArrayConfigMissDsnKeyInvalidConfigGiven() + { + $factory = new ConnectionFactoryFactory(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The config must be either array or DSN string.'); + $factory->create(new \stdClass()); + } + + public function testThrowIfPackageThatSupportSchemeNotInstalled() + { + $scheme = 'scheme5b7aa7d7cd213'; + $class = 'ConnectionClass5b7aa7d7cd213'; + + Resources::addConnection($class, [$scheme], [], 'thePackage'); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('To use given scheme "scheme5b7aa7d7cd213" a package has to be installed. Run "composer req thePackage" to add it.'); + (new ConnectionFactoryFactory())->create($scheme.'://foo'); + } + + public function testThrowIfSchemeIsNotKnown() + { + $scheme = 'scheme5b7aa862e70a5'; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('A given scheme "scheme5b7aa862e70a5" is not supported. Maybe it is a custom connection, make sure you registered it with "Enqueue\Resources::addConnection".'); + (new ConnectionFactoryFactory())->create($scheme.'://foo'); + } + + public function testThrowIfDsnInvalid() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The DSN is invalid. It does not have scheme separator ":".'); + + (new ConnectionFactoryFactory())->create('invalid-scheme'); + } + + /** + * @dataProvider provideDSN + */ + public function testReturnsExpectedFactories(string $dsn, string $expectedFactoryClass) + { + $connectionFactory = (new ConnectionFactoryFactory())->create($dsn); + + $this->assertInstanceOf($expectedFactoryClass, $connectionFactory); + } + + public static function provideDSN() + { + yield ['null:', NullConnectionFactory::class]; + + yield ['amqp:', AmqpBunnyConnectionFactory::class]; + + yield ['amqp+bunny:', AmqpBunnyConnectionFactory::class]; + + yield ['amqp+lib:', AmqpLibConnectionFactory::class]; + + yield ['amqp+ext:', AmqpExtConnectionFactory::class]; + + yield ['amqp+rabbitmq:', AmqpBunnyConnectionFactory::class]; + + yield ['amqp+rabbitmq+bunny:', AmqpBunnyConnectionFactory::class]; + + yield ['amqp+foo+bar+lib:', AmqpLibConnectionFactory::class]; + + yield ['amqp+rabbitmq+ext:', AmqpExtConnectionFactory::class]; + + yield ['amqp+rabbitmq+lib:', AmqpLibConnectionFactory::class]; + + // bunny does not support amqps, so it is skipped + yield ['amqps:', AmqpExtConnectionFactory::class]; + + // bunny does not support amqps, so it is skipped + yield ['amqps+ext:', AmqpExtConnectionFactory::class]; + + // bunny does not support amqps, so it is skipped + yield ['amqps+rabbitmq:', AmqpExtConnectionFactory::class]; + + yield ['amqps+ext+rabbitmq:', AmqpExtConnectionFactory::class]; + + yield ['amqps+lib+rabbitmq:', AmqpLibConnectionFactory::class]; + + yield ['mssql:', DbalConnectionFactory::class]; + + yield ['mysql:', DbalConnectionFactory::class]; + + yield ['pgsql:', DbalConnectionFactory::class]; + + yield ['file:', FsConnectionFactory::class]; + + // https://github.com/php-enqueue/enqueue-dev/issues/511 + // yield ['gearman:', GearmanConnectionFactory::class]; + + yield ['gps:', GpsConnectionFactory::class]; + + yield ['mongodb:', MongodbConnectionFactory::class]; + + yield ['beanstalk:', PheanstalkConnectionFactory::class]; + + yield ['kafka:', RdKafkaConnectionFactory::class]; + + yield ['redis:', RedisConnectionFactory::class]; + + yield ['redis+predis:', RedisConnectionFactory::class]; + + yield ['redis+foo+bar+phpredis:', RedisConnectionFactory::class]; + + yield ['redis+phpredis:', RedisConnectionFactory::class]; + + yield ['sqs:', SqsConnectionFactory::class]; + + yield ['stomp:', StompConnectionFactory::class]; + } +} diff --git a/Tests/Consumption/CallbackProcessorTest.php b/Tests/Consumption/CallbackProcessorTest.php index 1b034d9..86adbd3 100644 --- a/Tests/Consumption/CallbackProcessorTest.php +++ b/Tests/Consumption/CallbackProcessorTest.php @@ -6,7 +6,7 @@ use Enqueue\Null\NullContext; use Enqueue\Null\NullMessage; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Processor; use PHPUnit\Framework\TestCase; class CallbackProcessorTest extends TestCase @@ -15,13 +15,7 @@ class CallbackProcessorTest extends TestCase public function testShouldImplementProcessorInterface() { - $this->assertClassImplements(PsrProcessor::class, CallbackProcessor::class); - } - - public function testCouldBeConstructedWithCallableAsArgument() - { - new CallbackProcessor(function () { - }); + $this->assertClassImplements(Processor::class, CallbackProcessor::class); } public function testShouldCallCallbackAndProxyItsReturnedValue() diff --git a/Tests/Consumption/ChainExtensionTest.php b/Tests/Consumption/ChainExtensionTest.php index 4c412c6..198d000 100644 --- a/Tests/Consumption/ChainExtensionTest.php +++ b/Tests/Consumption/ChainExtensionTest.php @@ -3,10 +3,26 @@ namespace Enqueue\Tests\Consumption; use Enqueue\Consumption\ChainExtension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\End; +use Enqueue\Consumption\Context\InitLogger; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\Context\MessageResult; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\Context\PreSubscribe; +use Enqueue\Consumption\Context\Start; use Enqueue\Consumption\ExtensionInterface; use Enqueue\Test\ClassExtensionTrait; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; +use Interop\Queue\SubscriptionConsumer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class ChainExtensionTest extends TestCase { @@ -17,14 +33,31 @@ public function testShouldImplementExtensionInterface() $this->assertClassImplements(ExtensionInterface::class, ChainExtension::class); } - public function testCouldBeConstructedWithExtensionsArray() + public function testShouldProxyOnInitLoggerToAllInternalExtensions() { - new ChainExtension([$this->createExtension(), $this->createExtension()]); + $context = new InitLogger(new NullLogger()); + + $fooExtension = $this->createExtension(); + $fooExtension + ->expects($this->once()) + ->method('onInitLogger') + ->with($this->identicalTo($context)) + ; + $barExtension = $this->createExtension(); + $barExtension + ->expects($this->once()) + ->method('onInitLogger') + ->with($this->identicalTo($context)) + ; + + $extensions = new ChainExtension([$fooExtension, $barExtension]); + + $extensions->onInitLogger($context); } public function testShouldProxyOnStartToAllInternalExtensions() { - $context = $this->createContextMock(); + $context = new Start($this->createInteropContextMock(), $this->createLoggerMock(), [], 0, 0); $fooExtension = $this->createExtension(); $fooExtension @@ -44,53 +77,100 @@ public function testShouldProxyOnStartToAllInternalExtensions() $extensions->onStart($context); } - public function testShouldProxyOnBeforeReceiveToAllInternalExtensions() + public function testShouldProxyOnPreSubscribeToAllInternalExtensions() { - $context = $this->createContextMock(); + $context = new PreSubscribe( + $this->createInteropContextMock(), + $this->createInteropProcessorMock(), + $this->createInteropConsumerMock(), + $this->createLoggerMock() + ); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) - ->method('onBeforeReceive') + ->method('onPreSubscribe') ->with($this->identicalTo($context)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) - ->method('onBeforeReceive') + ->method('onPreSubscribe') ->with($this->identicalTo($context)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onBeforeReceive($context); + $extensions->onPreSubscribe($context); + } + + public function testShouldProxyOnPreConsumeToAllInternalExtensions() + { + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + new NullLogger(), + 1, + 2, + 3 + ); + + $fooExtension = $this->createExtension(); + $fooExtension + ->expects($this->once()) + ->method('onPreConsume') + ->with($this->identicalTo($context)) + ; + $barExtension = $this->createExtension(); + $barExtension + ->expects($this->once()) + ->method('onPreConsume') + ->with($this->identicalTo($context)) + ; + + $extensions = new ChainExtension([$fooExtension, $barExtension]); + $extensions->onPreConsume($context); } public function testShouldProxyOnPreReceiveToAllInternalExtensions() { - $context = $this->createContextMock(); + $context = new MessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + $this->createMock(Processor::class), + 1, + new NullLogger() + ); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) - ->method('onPreReceived') + ->method('onMessageReceived') ->with($this->identicalTo($context)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) - ->method('onPreReceived') + ->method('onMessageReceived') ->with($this->identicalTo($context)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onPreReceived($context); + $extensions->onMessageReceived($context); } public function testShouldProxyOnResultToAllInternalExtensions() { - $context = $this->createContextMock(); + $context = new MessageResult( + $this->createInteropContextMock(), + $this->createInteropConsumerMock(), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() + ); $fooExtension = $this->createExtension(); $fooExtension @@ -112,83 +192,129 @@ public function testShouldProxyOnResultToAllInternalExtensions() public function testShouldProxyOnPostReceiveToAllInternalExtensions() { - $context = $this->createContextMock(); + $context = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() + ); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) - ->method('onPostReceived') + ->method('onPostMessageReceived') ->with($this->identicalTo($context)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) - ->method('onPostReceived') + ->method('onPostMessageReceived') ->with($this->identicalTo($context)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onPostReceived($context); + $extensions->onPostMessageReceived($context); } - public function testShouldProxyOnIdleToAllInternalExtensions() + public function testShouldProxyOnPostConsumeToAllInternalExtensions() { - $context = $this->createContextMock(); + $postConsume = new PostConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + 1, + 1, + 1, + new NullLogger() + ); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) - ->method('onIdle') - ->with($this->identicalTo($context)) + ->method('onPostConsume') + ->with($this->identicalTo($postConsume)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) - ->method('onIdle') - ->with($this->identicalTo($context)) + ->method('onPostConsume') + ->with($this->identicalTo($postConsume)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onIdle($context); + $extensions->onPostConsume($postConsume); } - public function testShouldProxyOnInterruptedToAllInternalExtensions() + public function testShouldProxyOnEndToAllInternalExtensions() { - $context = $this->createContextMock(); + $context = new End($this->createInteropContextMock(), 1, 2, new NullLogger()); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) - ->method('onInterrupted') + ->method('onEnd') ->with($this->identicalTo($context)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) - ->method('onInterrupted') + ->method('onEnd') ->with($this->identicalTo($context)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onInterrupted($context); + $extensions->onEnd($context); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Context + * @return MockObject */ - protected function createContextMock() + protected function createLoggerMock(): LoggerInterface + { + return $this->createMock(LoggerInterface::class); + } + + /** + * @return MockObject + */ + protected function createInteropContextMock(): Context { return $this->createMock(Context::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ExtensionInterface + * @return MockObject + */ + protected function createInteropConsumerMock(): Consumer + { + return $this->createMock(Consumer::class); + } + + /** + * @return MockObject + */ + protected function createInteropProcessorMock(): Processor + { + return $this->createMock(Processor::class); + } + + /** + * @return MockObject|ExtensionInterface */ protected function createExtension() { return $this->createMock(ExtensionInterface::class); } + + /** + * @return MockObject + */ + private function createSubscriptionConsumerMock(): SubscriptionConsumer + { + return $this->createMock(SubscriptionConsumer::class); + } } diff --git a/Tests/Consumption/ContextTest.php b/Tests/Consumption/ContextTest.php deleted file mode 100644 index a0f6b26..0000000 --- a/Tests/Consumption/ContextTest.php +++ /dev/null @@ -1,258 +0,0 @@ -createPsrContext()); - } - - public function testShouldAllowGetSessionSetInConstructor() - { - $psrContext = $this->createPsrContext(); - - $context = new Context($psrContext); - - $this->assertSame($psrContext, $context->getPsrContext()); - } - - public function testShouldAllowGetMessageConsumerPreviouslySet() - { - $messageConsumer = $this->createPsrConsumer(); - - $context = new Context($this->createPsrContext()); - $context->setPsrConsumer($messageConsumer); - - $this->assertSame($messageConsumer, $context->getPsrConsumer()); - } - - public function testThrowOnTryToChangeMessageConsumerIfAlreadySet() - { - $messageConsumer = $this->createPsrConsumer(); - $anotherMessageConsumer = $this->createPsrConsumer(); - - $context = new Context($this->createPsrContext()); - - $context->setPsrConsumer($messageConsumer); - - $this->expectException(IllegalContextModificationException::class); - - $context->setPsrConsumer($anotherMessageConsumer); - } - - public function testShouldAllowGetMessageProducerPreviouslySet() - { - $processorMock = $this->createProcessorMock(); - - $context = new Context($this->createPsrContext()); - $context->setPsrProcessor($processorMock); - - $this->assertSame($processorMock, $context->getPsrProcessor()); - } - - public function testThrowOnTryToChangeProcessorIfAlreadySet() - { - $processor = $this->createProcessorMock(); - $anotherProcessor = $this->createProcessorMock(); - - $context = new Context($this->createPsrContext()); - - $context->setPsrProcessor($processor); - - $this->expectException(IllegalContextModificationException::class); - - $context->setPsrProcessor($anotherProcessor); - } - - public function testShouldAllowGetLoggerPreviouslySet() - { - $logger = new NullLogger(); - - $context = new Context($this->createPsrContext()); - $context->setLogger($logger); - - $this->assertSame($logger, $context->getLogger()); - } - - public function testShouldSetExecutionInterruptedToFalseInConstructor() - { - $context = new Context($this->createPsrContext()); - - $this->assertFalse($context->isExecutionInterrupted()); - } - - public function testShouldAllowGetPreviouslySetMessage() - { - /** @var PsrMessage $message */ - $message = $this->createMock(PsrMessage::class); - - $context = new Context($this->createPsrContext()); - - $context->setPsrMessage($message); - - $this->assertSame($message, $context->getPsrMessage()); - } - - public function testThrowOnTryToChangeMessageIfAlreadySet() - { - /** @var PsrMessage $message */ - $message = $this->createMock(PsrMessage::class); - - $context = new Context($this->createPsrContext()); - - $this->expectException(IllegalContextModificationException::class); - $this->expectExceptionMessage('The message could be set once'); - - $context->setPsrMessage($message); - $context->setPsrMessage($message); - } - - public function testShouldAllowGetPreviouslySetException() - { - $exception = new \Exception(); - - $context = new Context($this->createPsrContext()); - - $context->setException($exception); - - $this->assertSame($exception, $context->getException()); - } - - public function testShouldAllowGetPreviouslySetResult() - { - $result = 'aResult'; - - $context = new Context($this->createPsrContext()); - - $context->setResult($result); - - $this->assertSame($result, $context->getResult()); - } - - public function testThrowOnTryToChangeResultIfAlreadySet() - { - $result = 'aResult'; - - $context = new Context($this->createPsrContext()); - - $this->expectException(IllegalContextModificationException::class); - $this->expectExceptionMessage('The result modification is not allowed'); - - $context->setResult($result); - $context->setResult($result); - } - - public function testShouldAllowGetPreviouslySetExecutionInterrupted() - { - $context = new Context($this->createPsrContext()); - - // guard - $this->assertFalse($context->isExecutionInterrupted()); - - $context->setExecutionInterrupted(true); - - $this->assertTrue($context->isExecutionInterrupted()); - } - - public function testThrowOnTryToRollbackExecutionInterruptedIfAlreadySetToTrue() - { - $context = new Context($this->createPsrContext()); - - $this->expectException(IllegalContextModificationException::class); - $this->expectExceptionMessage('The execution once interrupted could not be roll backed'); - - $context->setExecutionInterrupted(true); - $context->setExecutionInterrupted(false); - } - - public function testNotThrowOnSettingExecutionInterruptedToTrueIfAlreadySetToTrue() - { - $context = new Context($this->createPsrContext()); - - $context->setExecutionInterrupted(true); - $context->setExecutionInterrupted(true); - } - - public function testShouldAllowGetPreviouslySetLogger() - { - $expectedLogger = new NullLogger(); - - $context = new Context($this->createPsrContext()); - - $context->setLogger($expectedLogger); - - $this->assertSame($expectedLogger, $context->getLogger()); - } - - public function testThrowOnSettingLoggerIfAlreadySet() - { - $context = new Context($this->createPsrContext()); - - $context->setLogger(new NullLogger()); - - $this->expectException(IllegalContextModificationException::class); - $this->expectExceptionMessage('The logger modification is not allowed'); - - $context->setLogger(new NullLogger()); - } - - public function testShouldAllowGetPreviouslySetQueue() - { - $context = new Context($this->createPsrContext()); - - $context->setPsrQueue($queue = new NullQueue('')); - - $this->assertSame($queue, $context->getPsrQueue()); - } - - public function testThrowOnSettingQueueNameIfAlreadySet() - { - $context = new Context($this->createPsrContext()); - - $context->setPsrQueue(new NullQueue('')); - - $this->expectException(IllegalContextModificationException::class); - $this->expectExceptionMessage('The queue modification is not allowed'); - - $context->setPsrQueue(new NullQueue('')); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext - */ - protected function createPsrContext() - { - return $this->createMock(PsrContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrConsumer - */ - protected function createPsrConsumer() - { - return $this->createMock(PsrConsumer::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor - */ - protected function createProcessorMock() - { - return $this->createMock(PsrProcessor::class); - } -} diff --git a/Tests/Consumption/EmptyExtensionTraitTest.php b/Tests/Consumption/EmptyExtensionTraitTest.php deleted file mode 100644 index 32ea861..0000000 --- a/Tests/Consumption/EmptyExtensionTraitTest.php +++ /dev/null @@ -1,20 +0,0 @@ -assertClassImplements(ExceptionInterface::class, ConsumptionInterruptedException::class); - } - - public function testShouldExtendLogicException() - { - $this->assertClassExtends(\LogicException::class, ConsumptionInterruptedException::class); - } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new ConsumptionInterruptedException(); - } -} diff --git a/Tests/Consumption/Exception/IllegalContextModificationExceptionTest.php b/Tests/Consumption/Exception/IllegalContextModificationExceptionTest.php index 0885b50..241f4ad 100644 --- a/Tests/Consumption/Exception/IllegalContextModificationExceptionTest.php +++ b/Tests/Consumption/Exception/IllegalContextModificationExceptionTest.php @@ -20,9 +20,4 @@ public function testShouldExtendLogicException() { $this->assertClassExtends(\LogicException::class, IllegalContextModificationException::class); } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new IllegalContextModificationException(); - } } diff --git a/Tests/Consumption/Exception/InvalidArgumentExceptionTest.php b/Tests/Consumption/Exception/InvalidArgumentExceptionTest.php index 296c762..c1c5db3 100644 --- a/Tests/Consumption/Exception/InvalidArgumentExceptionTest.php +++ b/Tests/Consumption/Exception/InvalidArgumentExceptionTest.php @@ -21,11 +21,6 @@ public function testShouldExtendLogicException() $this->assertClassExtends(\LogicException::class, InvalidArgumentException::class); } - public function testCouldBeConstructedWithoutAnyArguments() - { - new InvalidArgumentException(); - } - public function testThrowIfAssertInstanceOfNotSameAsExpected() { $this->expectException(InvalidArgumentException::class); @@ -36,6 +31,9 @@ public function testThrowIfAssertInstanceOfNotSameAsExpected() InvalidArgumentException::assertInstanceOf(new \SplStack(), \SplQueue::class); } + /** + * @doesNotPerformAssertions + */ public function testShouldDoNothingIfAssertDestinationInstanceOfSameAsExpected() { InvalidArgumentException::assertInstanceOf(new \SplQueue(), \SplQueue::class); diff --git a/Tests/Consumption/Exception/LogicExceptionTest.php b/Tests/Consumption/Exception/LogicExceptionTest.php index ddd2580..2655609 100644 --- a/Tests/Consumption/Exception/LogicExceptionTest.php +++ b/Tests/Consumption/Exception/LogicExceptionTest.php @@ -20,9 +20,4 @@ public function testShouldExtendLogicException() { $this->assertClassExtends(\LogicException::class, LogicException::class); } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new LogicException(); - } } diff --git a/Tests/Consumption/Extension/LimitConsumedMessagesExtensionTest.php b/Tests/Consumption/Extension/LimitConsumedMessagesExtensionTest.php index 250c577..137e30b 100644 --- a/Tests/Consumption/Extension/LimitConsumedMessagesExtensionTest.php +++ b/Tests/Consumption/Extension/LimitConsumedMessagesExtensionTest.php @@ -2,41 +2,84 @@ namespace Enqueue\Tests\Consumption\Extension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; use Enqueue\Consumption\Extension\LimitConsumedMessagesExtension; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\SubscriptionConsumer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class LimitConsumedMessagesExtensionTest extends TestCase { - public function testCouldBeConstructedWithRequiredArguments() + public function testOnPreConsumeShouldInterruptWhenLimitIsReached() { - new LimitConsumedMessagesExtension(12345); - } + $logger = $this->createLoggerMock(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('[LimitConsumedMessagesExtension] Message consumption is interrupted since'. + ' the message limit reached. limit: "3"') + ; - public function testShouldThrowExceptionIfMessageLimitIsNotInt() - { - $this->setExpectedException( - \InvalidArgumentException::class, - 'Expected message limit is int but got: "double"' + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + $logger, + 1, + 2, + 3 + ); + + // guard + $this->assertFalse($context->isExecutionInterrupted()); + + // test + $extension = new LimitConsumedMessagesExtension(3); + + $extension->onPreConsume($context); + $this->assertFalse($context->isExecutionInterrupted()); + + $postReceivedMessage = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() ); - new LimitConsumedMessagesExtension(0.0); + $extension->onPostMessageReceived($postReceivedMessage); + $extension->onPostMessageReceived($postReceivedMessage); + $extension->onPostMessageReceived($postReceivedMessage); + + $extension->onPreConsume($context); + $this->assertTrue($context->isExecutionInterrupted()); } - public function testOnBeforeReceiveShouldInterruptExecutionIfLimitIsZero() + public function testOnPreConsumeShouldInterruptExecutionIfLimitIsZero() { - $context = $this->createContext(); - $context->getLogger() + $logger = $this->createLoggerMock(); + $logger ->expects($this->once()) ->method('debug') ->with('[LimitConsumedMessagesExtension] Message consumption is interrupted since'. ' the message limit reached. limit: "0"') ; + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + $logger, + 1, + 2, + 3 + ); + // guard $this->assertFalse($context->isExecutionInterrupted()); @@ -44,20 +87,29 @@ public function testOnBeforeReceiveShouldInterruptExecutionIfLimitIsZero() $extension = new LimitConsumedMessagesExtension(0); // consume 1 - $extension->onBeforeReceive($context); + $extension->onPreConsume($context); $this->assertTrue($context->isExecutionInterrupted()); } - public function testOnBeforeReceiveShouldInterruptExecutionIfLimitIsLessThatZero() + public function testOnPreConsumeShouldInterruptExecutionIfLimitIsLessThatZero() { - $context = $this->createContext(); - $context->getLogger() + $logger = $this->createLoggerMock(); + $logger ->expects($this->once()) ->method('debug') ->with('[LimitConsumedMessagesExtension] Message consumption is interrupted since'. ' the message limit reached. limit: "-1"') ; + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + $logger, + 1, + 2, + 3 + ); + // guard $this->assertFalse($context->isExecutionInterrupted()); @@ -65,45 +117,65 @@ public function testOnBeforeReceiveShouldInterruptExecutionIfLimitIsLessThatZero $extension = new LimitConsumedMessagesExtension(-1); // consume 1 - $extension->onBeforeReceive($context); + $extension->onPreConsume($context); $this->assertTrue($context->isExecutionInterrupted()); } public function testOnPostReceivedShouldInterruptExecutionIfMessageLimitExceeded() { - $context = $this->createContext(); - $context->getLogger() + $logger = $this->createLoggerMock(); + $logger ->expects($this->once()) ->method('debug') ->with('[LimitConsumedMessagesExtension] Message consumption is interrupted since'. ' the message limit reached. limit: "2"') ; + $postReceivedMessage = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + $logger + ); + // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); // test $extension = new LimitConsumedMessagesExtension(2); // consume 1 - $extension->onPostReceived($context); - $this->assertFalse($context->isExecutionInterrupted()); + $extension->onPostMessageReceived($postReceivedMessage); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); // consume 2 and exit - $extension->onPostReceived($context); - $this->assertTrue($context->isExecutionInterrupted()); + $extension->onPostMessageReceived($postReceivedMessage); + $this->assertTrue($postReceivedMessage->isExecutionInterrupted()); } /** - * @return Context + * @return MockObject */ - protected function createContext() + protected function createInteropContextMock(): Context { - $context = new Context($this->createMock(PsrContext::class)); - $context->setLogger($this->createMock(LoggerInterface::class)); - $context->setPsrConsumer($this->createMock(PsrConsumer::class)); - $context->setPsrProcessor($this->createMock(PsrProcessor::class)); + return $this->createMock(Context::class); + } - return $context; + /** + * @return MockObject + */ + private function createSubscriptionConsumerMock(): SubscriptionConsumer + { + return $this->createMock(SubscriptionConsumer::class); + } + + /** + * @return MockObject + */ + private function createLoggerMock(): LoggerInterface + { + return $this->createMock(LoggerInterface::class); } } diff --git a/Tests/Consumption/Extension/LimitConsumerMemoryExtensionTest.php b/Tests/Consumption/Extension/LimitConsumerMemoryExtensionTest.php index 74514e5..25ac858 100644 --- a/Tests/Consumption/Extension/LimitConsumerMemoryExtensionTest.php +++ b/Tests/Consumption/Extension/LimitConsumerMemoryExtensionTest.php @@ -2,137 +2,197 @@ namespace Enqueue\Tests\Consumption\Extension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; use Enqueue\Consumption\Extension\LimitConsumerMemoryExtension; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\SubscriptionConsumer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class LimitConsumerMemoryExtensionTest extends TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new LimitConsumerMemoryExtension(12345); - } - public function testShouldThrowExceptionIfMemoryLimitIsNotInt() { - $this->setExpectedException(\InvalidArgumentException::class, 'Expected memory limit is int but got: "double"'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expected memory limit is int but got: "double"'); new LimitConsumerMemoryExtension(0.0); } - public function testOnIdleShouldInterruptExecutionIfMemoryLimitReached() + public function testOnPostConsumeShouldInterruptExecutionIfMemoryLimitReached() { - $context = $this->createPsrContext(); - $context->getLogger() + $logger = $this->createLoggerMock(); + $logger ->expects($this->once()) ->method('debug') ->with($this->stringContains('[LimitConsumerMemoryExtension] Interrupt execution as memory limit reached.')) ; + $postConsume = new PostConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + 1, + 1, + 1, + $logger + ); + // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postConsume->isExecutionInterrupted()); // test $extension = new LimitConsumerMemoryExtension(1); - $extension->onIdle($context); + $extension->onPostConsume($postConsume); - $this->assertTrue($context->isExecutionInterrupted()); + $this->assertTrue($postConsume->isExecutionInterrupted()); } public function testOnPostReceivedShouldInterruptExecutionIfMemoryLimitReached() { - $context = $this->createPsrContext(); - $context->getLogger() + $logger = $this->createLoggerMock(); + $logger ->expects($this->once()) ->method('debug') ->with($this->stringContains('[LimitConsumerMemoryExtension] Interrupt execution as memory limit reached.')) ; + $postReceivedMessage = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + $logger + ); + // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); // test $extension = new LimitConsumerMemoryExtension(1); - $extension->onPostReceived($context); + $extension->onPostMessageReceived($postReceivedMessage); - $this->assertTrue($context->isExecutionInterrupted()); + $this->assertTrue($postReceivedMessage->isExecutionInterrupted()); } - public function testOnBeforeReceivedShouldInterruptExecutionIfMemoryLimitReached() + public function testOnPreConsumeShouldInterruptExecutionIfMemoryLimitReached() { - $context = $this->createPsrContext(); - $context->getLogger() + $logger = $this->createLoggerMock(); + $logger ->expects($this->once()) ->method('debug') ->with($this->stringContains('[LimitConsumerMemoryExtension] Interrupt execution as memory limit reached.')) ; + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + $logger, + 1, + 2, + 3 + ); + // guard $this->assertFalse($context->isExecutionInterrupted()); // test $extension = new LimitConsumerMemoryExtension(1); - $extension->onBeforeReceive($context); + $extension->onPreConsume($context); $this->assertTrue($context->isExecutionInterrupted()); } - public function testOnBeforeReceiveShouldNotInterruptExecutionIfMemoryLimitIsNotReached() + public function testOnPreConsumeShouldNotInterruptExecutionIfMemoryLimitIsNotReached() { - $context = $this->createPsrContext(); + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + new NullLogger(), + 1, + 2, + 3 + ); // guard $this->assertFalse($context->isExecutionInterrupted()); // test - $extension = new LimitConsumerMemoryExtension(PHP_INT_MAX); - $extension->onBeforeReceive($context); + $extension = new LimitConsumerMemoryExtension(\PHP_INT_MAX); + $extension->onPreConsume($context); $this->assertFalse($context->isExecutionInterrupted()); } - public function testOnIdleShouldNotInterruptExecutionIfMemoryLimitIsNotReached() + public function testOnPostConsumeShouldNotInterruptExecutionIfMemoryLimitIsNotReached() { - $context = $this->createPsrContext(); + $postConsume = new PostConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + 1, + 1, + 1, + new NullLogger() + ); // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postConsume->isExecutionInterrupted()); // test - $extension = new LimitConsumerMemoryExtension(PHP_INT_MAX); - $extension->onIdle($context); + $extension = new LimitConsumerMemoryExtension(\PHP_INT_MAX); + $extension->onPostConsume($postConsume); - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postConsume->isExecutionInterrupted()); } - public function testOnPostReceivedShouldNotInterruptExecutionIfMemoryLimitIsNotReached() + public function testOnPostMessageReceivedShouldNotInterruptExecutionIfMemoryLimitIsNotReached() { - $context = $this->createPsrContext(); + $postReceivedMessage = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() + ); // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); // test - $extension = new LimitConsumerMemoryExtension(PHP_INT_MAX); - $extension->onPostReceived($context); + $extension = new LimitConsumerMemoryExtension(\PHP_INT_MAX); + $extension->onPostMessageReceived($postReceivedMessage); - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); } /** - * @return Context + * @return MockObject */ - protected function createPsrContext() + protected function createInteropContextMock(): Context { - $context = new Context($this->createMock(PsrContext::class)); - $context->setLogger($this->createMock(LoggerInterface::class)); - $context->setPsrConsumer($this->createMock(PsrConsumer::class)); - $context->setPsrProcessor($this->createMock(PsrProcessor::class)); + return $this->createMock(Context::class); + } - return $context; + /** + * @return MockObject + */ + private function createSubscriptionConsumerMock(): SubscriptionConsumer + { + return $this->createMock(SubscriptionConsumer::class); + } + + /** + * @return MockObject + */ + private function createLoggerMock(): LoggerInterface + { + return $this->createMock(LoggerInterface::class); } } diff --git a/Tests/Consumption/Extension/LimitConsumptionTimeExtensionTest.php b/Tests/Consumption/Extension/LimitConsumptionTimeExtensionTest.php index fffeb94..fa6cb76 100644 --- a/Tests/Consumption/Extension/LimitConsumptionTimeExtensionTest.php +++ b/Tests/Consumption/Extension/LimitConsumptionTimeExtensionTest.php @@ -2,24 +2,31 @@ namespace Enqueue\Tests\Consumption\Extension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; use Enqueue\Consumption\Extension\LimitConsumptionTimeExtension; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\SubscriptionConsumer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class LimitConsumptionTimeExtensionTest extends TestCase { - public function testCouldBeConstructedWithRequiredArguments() + public function testOnPreConsumeShouldInterruptExecutionIfConsumptionTimeExceeded() { - new LimitConsumptionTimeExtension(new \DateTime('+1 day')); - } - - public function testOnBeforeReceiveShouldInterruptExecutionIfConsumptionTimeExceeded() - { - $context = $this->createContext(); + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + new NullLogger(), + 1, + 2, + 3 + ); // guard $this->assertFalse($context->isExecutionInterrupted()); @@ -27,44 +34,65 @@ public function testOnBeforeReceiveShouldInterruptExecutionIfConsumptionTimeExce // test $extension = new LimitConsumptionTimeExtension(new \DateTime('-2 second')); - $extension->onBeforeReceive($context); + $extension->onPreConsume($context); $this->assertTrue($context->isExecutionInterrupted()); } - public function testOnIdleShouldInterruptExecutionIfConsumptionTimeExceeded() + public function testOnPostConsumeShouldInterruptExecutionIfConsumptionTimeExceeded() { - $context = $this->createContext(); + $postConsume = new PostConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + 1, + 1, + 1, + new NullLogger() + ); // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postConsume->isExecutionInterrupted()); // test $extension = new LimitConsumptionTimeExtension(new \DateTime('-2 second')); - $extension->onIdle($context); + $extension->onPostConsume($postConsume); - $this->assertTrue($context->isExecutionInterrupted()); + $this->assertTrue($postConsume->isExecutionInterrupted()); } public function testOnPostReceivedShouldInterruptExecutionIfConsumptionTimeExceeded() { - $context = $this->createContext(); + $postReceivedMessage = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() + ); // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); // test $extension = new LimitConsumptionTimeExtension(new \DateTime('-2 second')); - $extension->onPostReceived($context); + $extension->onPostMessageReceived($postReceivedMessage); - $this->assertTrue($context->isExecutionInterrupted()); + $this->assertTrue($postReceivedMessage->isExecutionInterrupted()); } - public function testOnBeforeReceiveShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded() + public function testOnPreConsumeShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded() { - $context = $this->createContext(); + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + new NullLogger(), + 1, + 2, + 3 + ); // guard $this->assertFalse($context->isExecutionInterrupted()); @@ -72,51 +100,76 @@ public function testOnBeforeReceiveShouldNotInterruptExecutionIfConsumptionTimeI // test $extension = new LimitConsumptionTimeExtension(new \DateTime('+2 second')); - $extension->onBeforeReceive($context); + $extension->onPreConsume($context); $this->assertFalse($context->isExecutionInterrupted()); } - public function testOnIdleShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded() + public function testOnPostConsumeShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded() { - $context = $this->createContext(); + $postConsume = new PostConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + 1, + 1, + 1, + new NullLogger() + ); // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postConsume->isExecutionInterrupted()); // test $extension = new LimitConsumptionTimeExtension(new \DateTime('+2 second')); - $extension->onIdle($context); + $extension->onPostConsume($postConsume); - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postConsume->isExecutionInterrupted()); } public function testOnPostReceivedShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded() { - $context = $this->createContext(); + $postReceivedMessage = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() + ); // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); // test $extension = new LimitConsumptionTimeExtension(new \DateTime('+2 second')); - $extension->onPostReceived($context); + $extension->onPostMessageReceived($postReceivedMessage); - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); + } + + /** + * @return MockObject + */ + protected function createInteropContextMock(): Context + { + return $this->createMock(Context::class); } /** - * @return Context + * @return MockObject */ - protected function createContext() + private function createSubscriptionConsumerMock(): SubscriptionConsumer { - $context = new Context($this->createMock(PsrContext::class)); - $context->setLogger($this->createMock(LoggerInterface::class)); - $context->setPsrConsumer($this->createMock(PsrConsumer::class)); - $context->setPsrProcessor($this->createMock(PsrProcessor::class)); + return $this->createMock(SubscriptionConsumer::class); + } - return $context; + /** + * @return MockObject + */ + private function createLoggerMock(): LoggerInterface + { + return $this->createMock(LoggerInterface::class); } } diff --git a/Tests/Consumption/Extension/LogExtensionTest.php b/Tests/Consumption/Extension/LogExtensionTest.php new file mode 100644 index 0000000..006a2c5 --- /dev/null +++ b/Tests/Consumption/Extension/LogExtensionTest.php @@ -0,0 +1,266 @@ +assertClassImplements(StartExtensionInterface::class, LogExtension::class); + } + + public function testShouldImplementEndExtensionInterface() + { + $this->assertClassImplements(EndExtensionInterface::class, LogExtension::class); + } + + public function testShouldImplementMessageReceivedExtensionInterface() + { + $this->assertClassImplements(MessageReceivedExtensionInterface::class, LogExtension::class); + } + + public function testShouldImplementPostMessageReceivedExtensionInterface() + { + $this->assertClassImplements(PostMessageReceivedExtensionInterface::class, LogExtension::class); + } + + public function testShouldLogStartOnStart() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('Consumption has started') + ; + + $context = new Start($this->createContextMock(), $logger, [], 1, 1); + + $extension = new LogExtension(); + $extension->onStart($context); + } + + public function testShouldLogEndOnEnd() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('Consumption has ended') + ; + + $context = new End($this->createContextMock(), 1, 2, $logger); + + $extension = new LogExtension(); + $extension->onEnd($context); + } + + public function testShouldLogMessageReceived() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('Received from {queueName} {body}', [ + 'queueName' => 'aQueue', + 'redelivered' => false, + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + ]) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new MessageReceived($this->createContextMock(), $consumerMock, $message, $this->createProcessorMock(), 1, $logger); + + $extension = new LogExtension(); + $extension->onMessageReceived($context); + } + + public function testShouldLogMessageProcessedWithStringResult() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + 'Processed from {queueName} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'aResult', + 'reason' => '', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, 'aResult', 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogRejectedMessageAsError() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::ERROR, + 'Processed from {queueName} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'reject', + 'reason' => '', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Processor::REJECT, 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogMessageProcessedWithResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + 'Processed from {queueName} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => '', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack(), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogMessageProcessedWithReasonResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + 'Processed from {queueName} {body} {result} {reason}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => 'aReason', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack('aReason'), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + /** + * @return MockObject + */ + private function createConsumerStub(Queue $queue): Consumer + { + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue) + ; + + return $consumerMock; + } + + /** + * @return MockObject + */ + private function createContextMock(): Context + { + return $this->createMock(Context::class); + } + + /** + * @return MockObject + */ + private function createProcessorMock(): Processor + { + return $this->createMock(Processor::class); + } + + /** + * @return MockObject|LoggerInterface + */ + private function createLogger() + { + return $this->createMock(LoggerInterface::class); + } +} diff --git a/Tests/Consumption/Extension/LoggerExtensionTest.php b/Tests/Consumption/Extension/LoggerExtensionTest.php index 68bc700..666892e 100644 --- a/Tests/Consumption/Extension/LoggerExtensionTest.php +++ b/Tests/Consumption/Extension/LoggerExtensionTest.php @@ -2,199 +2,78 @@ namespace Enqueue\Tests\Consumption\Extension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\InitLogger; use Enqueue\Consumption\Extension\LoggerExtension; -use Enqueue\Consumption\ExtensionInterface; -use Enqueue\Consumption\Result; -use Enqueue\Null\NullMessage; +use Enqueue\Consumption\InitLoggerExtensionInterface; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class LoggerExtensionTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementExtensionInterface() + public function testShouldImplementInitLoggerExtensionInterface() { - $this->assertClassImplements(ExtensionInterface::class, LoggerExtension::class); + $this->assertClassImplements(InitLoggerExtensionInterface::class, LoggerExtension::class); } - public function testCouldBeConstructedWithLoggerAsFirstArgument() - { - new LoggerExtension($this->createLogger()); - } - - public function testShouldSetLoggerToContextOnStart() + public function testShouldSetLoggerToContextOnInitLogger() { $logger = $this->createLogger(); $extension = new LoggerExtension($logger); - $context = new Context($this->createPsrContextMock()); + $previousLogger = new NullLogger(); + $context = new InitLogger($previousLogger); - $extension->onStart($context); + $extension->onInitLogger($context); $this->assertSame($logger, $context->getLogger()); } public function testShouldAddInfoMessageOnStart() { - $logger = $this->createLogger(); - $logger - ->expects($this->once()) - ->method('debug') - ->with($this->stringStartsWith('Set context\'s logger')) - ; - - $extension = new LoggerExtension($logger); - - $context = new Context($this->createPsrContextMock()); - - $extension->onStart($context); - } - - public function testShouldLogRejectMessageStatus() - { - $logger = $this->createLogger(); - $logger - ->expects($this->once()) - ->method('error') - ->with('reason', ['body' => 'message body', 'headers' => [], 'properties' => []]) - ; - - $extension = new LoggerExtension($logger); - - $message = new NullMessage(); - $message->setBody('message body'); - - $context = new Context($this->createPsrContextMock()); - $context->setResult(Result::reject('reason')); - $context->setPsrMessage($message); - - $extension->onPostReceived($context); - } - - public function testShouldLogRequeueMessageStatus() - { - $logger = $this->createLogger(); - $logger - ->expects($this->once()) - ->method('error') - ->with('reason', ['body' => 'message body', 'headers' => [], 'properties' => []]) - ; - - $extension = new LoggerExtension($logger); - - $message = new NullMessage(); - $message->setBody('message body'); - - $context = new Context($this->createPsrContextMock()); - $context->setResult(Result::requeue('reason')); - $context->setPsrMessage($message); - - $extension->onPostReceived($context); - } - - public function testShouldNotLogRequeueMessageStatusIfReasonIsEmpty() - { - $logger = $this->createLogger(); - $logger - ->expects($this->never()) - ->method('error') - ; - - $extension = new LoggerExtension($logger); + $previousLogger = $this->createLogger(); - $context = new Context($this->createPsrContextMock()); - $context->setResult(Result::requeue()); - - $extension->onPostReceived($context); - } - - public function testShouldLogAckMessageStatus() - { $logger = $this->createLogger(); $logger ->expects($this->once()) - ->method('info') - ->with('reason', ['body' => 'message body', 'headers' => [], 'properties' => []]) + ->method('debug') + ->with(sprintf('Change logger from "%s" to "%s"', $logger::class, $previousLogger::class)) ; $extension = new LoggerExtension($logger); - $message = new NullMessage(); - $message->setBody('message body'); + $context = new InitLogger($previousLogger); - $context = new Context($this->createPsrContextMock()); - $context->setResult(Result::ack('reason')); - $context->setPsrMessage($message); - - $extension->onPostReceived($context); + $extension->onInitLogger($context); } - public function testShouldNotLogAckMessageStatusIfReasonIsEmpty() + public function testShouldDoNothingIfSameLoggerInstanceAlreadySet() { $logger = $this->createLogger(); $logger ->expects($this->never()) - ->method('info') - ; - - $extension = new LoggerExtension($logger); - - $context = new Context($this->createPsrContextMock()); - $context->setResult(Result::ack()); - - $extension->onPostReceived($context); - } - - public function testShouldNotSetLoggerIfOneHasBeenSetOnStart() - { - $logger = $this->createLogger(); - - $alreadySetLogger = $this->createLogger(); - $alreadySetLogger - ->expects($this->once()) ->method('debug') - ->with(sprintf( - 'Skip setting context\'s logger "%s". Another one "%s" has already been set.', - get_class($logger), - get_class($alreadySetLogger) - )) ; $extension = new LoggerExtension($logger); - $context = new Context($this->createPsrContextMock()); - $context->setLogger($alreadySetLogger); + $context = new InitLogger($logger); - $extension->onStart($context); - } + $extension->onInitLogger($context); - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext - */ - protected function createPsrContextMock() - { - return $this->createMock(PsrContext::class); + $this->assertSame($logger, $context->getLogger()); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|LoggerInterface + * @return MockObject|LoggerInterface */ protected function createLogger() { return $this->createMock(LoggerInterface::class); } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrConsumer - */ - protected function createConsumerMock() - { - return $this->createMock(PsrConsumer::class); - } } diff --git a/Tests/Consumption/Extension/NicenessExtensionTest.php b/Tests/Consumption/Extension/NicenessExtensionTest.php new file mode 100644 index 0000000..734bc84 --- /dev/null +++ b/Tests/Consumption/Extension/NicenessExtensionTest.php @@ -0,0 +1,38 @@ +expectException(\InvalidArgumentException::class); + new NicenessExtension('1'); + } + + public function testShouldThrowWarningOnInvalidArgument() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('proc_nice(): Only a super user may attempt to increase the priority of a process'); + + $context = new Start($this->createContextMock(), new NullLogger(), [], 0, 0); + + $extension = new NicenessExtension(-1); + $extension->onStart($context); + } + + /** + * @return MockObject|InteropContext + */ + protected function createContextMock(): InteropContext + { + return $this->createMock(InteropContext::class); + } +} diff --git a/Tests/Consumption/Extension/ReplyExtensionTest.php b/Tests/Consumption/Extension/ReplyExtensionTest.php index 4912055..cb65816 100644 --- a/Tests/Consumption/Extension/ReplyExtensionTest.php +++ b/Tests/Consumption/Extension/ReplyExtensionTest.php @@ -2,15 +2,17 @@ namespace Enqueue\Tests\Consumption\Extension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\PostMessageReceived; use Enqueue\Consumption\Extension\ReplyExtension; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; use Enqueue\Consumption\Result; use Enqueue\Null\NullMessage; use Enqueue\Null\NullQueue; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProducer; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Producer as InteropProducer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; @@ -18,52 +20,25 @@ class ReplyExtensionTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementExtensionInterface() + public function testShouldImplementPostMessageReceivedExtensionInterface() { - $this->assertClassImplements(ExtensionInterface::class, ReplyExtension::class); - } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new ReplyExtension(); - } - - public function testShouldDoNothingOnPreReceived() - { - $extension = new ReplyExtension(); - - $extension->onPreReceived(new Context($this->createNeverUsedContextMock())); - } - - public function testShouldDoNothingOnStart() - { - $extension = new ReplyExtension(); - - $extension->onStart(new Context($this->createNeverUsedContextMock())); - } - - public function testShouldDoNothingOnBeforeReceive() - { - $extension = new ReplyExtension(); - - $extension->onBeforeReceive(new Context($this->createNeverUsedContextMock())); - } - - public function testShouldDoNothingOnInterrupted() - { - $extension = new ReplyExtension(); - - $extension->onInterrupted(new Context($this->createNeverUsedContextMock())); + $this->assertClassImplements(PostMessageReceivedExtensionInterface::class, ReplyExtension::class); } public function testShouldDoNothingIfReceivedMessageNotHaveReplyToSet() { $extension = new ReplyExtension(); - $context = new Context($this->createNeverUsedContextMock()); - $context->setPsrMessage(new NullMessage()); + $postReceivedMessage = new PostMessageReceived( + $this->createNeverUsedContextMock(), + $this->createMock(Consumer::class), + new NullMessage(), + 'aResult', + 1, + new NullLogger() + ); - $extension->onPostReceived($context); + $extension->onPostMessageReceived($postReceivedMessage); } public function testShouldDoNothingIfContextResultIsNotInstanceOfResult() @@ -73,11 +48,16 @@ public function testShouldDoNothingIfContextResultIsNotInstanceOfResult() $message = new NullMessage(); $message->setReplyTo('aReplyToQueue'); - $context = new Context($this->createNeverUsedContextMock()); - $context->setPsrMessage($message); - $context->setResult('notInstanceOfResult'); + $postReceivedMessage = new PostMessageReceived( + $this->createNeverUsedContextMock(), + $this->createMock(Consumer::class), + $message, + 'notInstanceOfResult', + 1, + new NullLogger() + ); - $extension->onPostReceived($context); + $extension->onPostMessageReceived($postReceivedMessage); } public function testShouldDoNothingIfResultInstanceOfResultButReplyMessageNotSet() @@ -87,11 +67,16 @@ public function testShouldDoNothingIfResultInstanceOfResultButReplyMessageNotSet $message = new NullMessage(); $message->setReplyTo('aReplyToQueue'); - $context = new Context($this->createNeverUsedContextMock()); - $context->setPsrMessage($message); - $context->setResult(Result::ack()); + $postReceivedMessage = new PostMessageReceived( + $this->createNeverUsedContextMock(), + $this->createMock(Consumer::class), + $message, + Result::ack(), + 1, + new NullLogger() + ); - $extension->onPostReceived($context); + $extension->onPostMessageReceived($postReceivedMessage); } public function testShouldSendReplyMessageToReplyQueueOnPostReceived() @@ -107,15 +92,15 @@ public function testShouldSendReplyMessageToReplyQueueOnPostReceived() $replyQueue = new NullQueue('aReplyName'); - $producerMock = $this->createMock(PsrProducer::class); + $producerMock = $this->createMock(InteropProducer::class); $producerMock ->expects($this->once()) ->method('send') ->with($replyQueue, $replyMessage) ; - /** @var \PHPUnit_Framework_MockObject_MockObject|PsrContext $contextMock */ - $contextMock = $this->createMock(PsrContext::class); + /** @var MockObject|Context $contextMock */ + $contextMock = $this->createMock(Context::class); $contextMock ->expects($this->once()) ->method('createQueue') @@ -127,20 +112,32 @@ public function testShouldSendReplyMessageToReplyQueueOnPostReceived() ->willReturn($producerMock) ; - $context = new Context($contextMock); - $context->setPsrMessage($message); - $context->setResult(Result::reply($replyMessage)); - $context->setLogger(new NullLogger()); + $postReceivedMessage = new PostMessageReceived( + $contextMock, + $this->createMock(Consumer::class), + $message, + Result::reply($replyMessage), + 1, + new NullLogger() + ); + + $extension->onPostMessageReceived($postReceivedMessage); + } - $extension->onPostReceived($context); + /** + * @return MockObject + */ + protected function createInteropContextMock(): Context + { + return $this->createMock(Context::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject */ - private function createNeverUsedContextMock() + private function createNeverUsedContextMock(): Context { - $contextMock = $this->createMock(PsrContext::class); + $contextMock = $this->createMock(Context::class); $contextMock ->expects($this->never()) ->method('createProducer') diff --git a/Tests/Consumption/FallbackSubscriptionConsumerTest.php b/Tests/Consumption/FallbackSubscriptionConsumerTest.php new file mode 100644 index 0000000..73fba7b --- /dev/null +++ b/Tests/Consumption/FallbackSubscriptionConsumerTest.php @@ -0,0 +1,253 @@ +assertTrue($rc->implementsInterface(SubscriptionConsumer::class)); + } + + public function testShouldInitSubscribersPropertyWithEmptyArray() + { + $subscriptionConsumer = new FallbackSubscriptionConsumer(); + + $this->assertAttributeSame([], 'subscribers', $subscriptionConsumer); + } + + public function testShouldAddConsumerAndCallbackToSubscribersPropertyOnSubscribe() + { + $subscriptionConsumer = new FallbackSubscriptionConsumer(); + + $fooCallback = function () {}; + $fooConsumer = $this->createConsumerStub('foo_queue'); + + $barCallback = function () {}; + $barConsumer = $this->createConsumerStub('bar_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + $subscriptionConsumer->subscribe($barConsumer, $barCallback); + + $this->assertAttributeSame([ + 'foo_queue' => [$fooConsumer, $fooCallback], + 'bar_queue' => [$barConsumer, $barCallback], + ], 'subscribers', $subscriptionConsumer); + } + + public function testThrowsIfTrySubscribeAnotherConsumerToAlreadySubscribedQueue() + { + $subscriptionConsumer = new FallbackSubscriptionConsumer(); + + $fooCallback = function () {}; + $fooConsumer = $this->createConsumerStub('foo_queue'); + + $barCallback = function () {}; + $barConsumer = $this->createConsumerStub('foo_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('There is a consumer subscribed to queue: "foo_queue"'); + $subscriptionConsumer->subscribe($barConsumer, $barCallback); + } + + /** + * @doesNotPerformAssertions + */ + public function testShouldAllowSubscribeSameConsumerAndCallbackSecondTime() + { + $subscriptionConsumer = new FallbackSubscriptionConsumer(); + + $fooCallback = function () {}; + $fooConsumer = $this->createConsumerStub('foo_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + } + + public function testShouldRemoveSubscribedConsumerOnUnsubscribeCall() + { + $subscriptionConsumer = new FallbackSubscriptionConsumer(); + + $fooConsumer = $this->createConsumerStub('foo_queue'); + $barConsumer = $this->createConsumerStub('bar_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, function () {}); + $subscriptionConsumer->subscribe($barConsumer, function () {}); + + // guard + $this->assertAttributeCount(2, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribe($fooConsumer); + + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + } + + public function testShouldDoNothingIfTryUnsubscribeNotSubscribedQueueName() + { + $subscriptionConsumer = new FallbackSubscriptionConsumer(); + + $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {}); + + // guard + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribe($this->createConsumerStub('bar_queue')); + + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + } + + public function testShouldDoNothingIfTryUnsubscribeNotSubscribedConsumer() + { + $subscriptionConsumer = new FallbackSubscriptionConsumer(); + + $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {}); + + // guard + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribe($this->createConsumerStub('foo_queue')); + + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + } + + public function testShouldRemoveAllSubscriberOnUnsubscribeAllCall() + { + $subscriptionConsumer = new FallbackSubscriptionConsumer(); + + $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {}); + $subscriptionConsumer->subscribe($this->createConsumerStub('bar_queue'), function () {}); + + // guard + $this->assertAttributeCount(2, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribeAll(); + + $this->assertAttributeCount(0, 'subscribers', $subscriptionConsumer); + } + + public function testShouldConsumeMessagesFromTwoQueuesInExpectedOrder() + { + $firstMessage = $this->createMessageStub('first'); + $secondMessage = $this->createMessageStub('second'); + $thirdMessage = $this->createMessageStub('third'); + $fourthMessage = $this->createMessageStub('fourth'); + $fifthMessage = $this->createMessageStub('fifth'); + + $fooConsumer = $this->createConsumerStub('foo_queue'); + $fooConsumer + ->expects($this->any()) + ->method('receiveNoWait') + ->willReturnOnConsecutiveCalls(null, $firstMessage, null, $secondMessage, $thirdMessage) + ; + + $barConsumer = $this->createConsumerStub('bar_queue'); + $barConsumer + ->expects($this->any()) + ->method('receiveNoWait') + ->willReturnOnConsecutiveCalls($fourthMessage, null, null, $fifthMessage) + ; + + $actualOrder = []; + $callback = function (InteropMessage $message, Consumer $consumer) use (&$actualOrder) { + $actualOrder[] = [$message->getBody(), $consumer->getQueue()->getQueueName()]; + }; + + $subscriptionConsumer = new FallbackSubscriptionConsumer(); + + $subscriptionConsumer->subscribe($fooConsumer, $callback); + $subscriptionConsumer->subscribe($barConsumer, $callback); + + $subscriptionConsumer->consume(100); + + $this->assertEquals([ + ['fourth', 'bar_queue'], + ['first', 'foo_queue'], + ['second', 'foo_queue'], + ['fifth', 'bar_queue'], + ['third', 'foo_queue'], + ], $actualOrder); + } + + public function testThrowsIfTryConsumeWithoutSubscribers() + { + $subscriptionConsumer = new FallbackSubscriptionConsumer(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('No subscribers'); + $subscriptionConsumer->consume(); + } + + public function testShouldConsumeTillTimeoutIsReached() + { + $fooConsumer = $this->createConsumerStub('foo_queue'); + $fooConsumer + ->expects($this->any()) + ->method('receiveNoWait') + ->willReturn(null) + ; + + $subscriptionConsumer = new FallbackSubscriptionConsumer(); + + $subscriptionConsumer->subscribe($fooConsumer, function () {}); + + $startAt = microtime(true); + $subscriptionConsumer->consume(500); + $endAt = microtime(true); + + $this->assertGreaterThan(0.49, $endAt - $startAt); + } + + /** + * @param mixed|null $body + * + * @return InteropMessage|\PHPUnit\Framework\MockObject\MockObject + */ + private function createMessageStub($body = null) + { + $messageMock = $this->createMock(InteropMessage::class); + $messageMock + ->expects($this->any()) + ->method('getBody') + ->willReturn($body) + ; + + return $messageMock; + } + + /** + * @param mixed|null $queueName + * + * @return Consumer|\PHPUnit\Framework\MockObject\MockObject + */ + private function createConsumerStub($queueName = null) + { + $queueMock = $this->createMock(InteropQueue::class); + $queueMock + ->expects($this->any()) + ->method('getQueueName') + ->willReturn($queueName); + + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queueMock) + ; + + return $consumerMock; + } +} diff --git a/Tests/Consumption/Mock/BreakCycleExtension.php b/Tests/Consumption/Mock/BreakCycleExtension.php index b61d8dd..cbc2f8b 100644 --- a/Tests/Consumption/Mock/BreakCycleExtension.php +++ b/Tests/Consumption/Mock/BreakCycleExtension.php @@ -2,14 +2,20 @@ namespace Enqueue\Tests\Consumption\Mock; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; +use Enqueue\Consumption\Context\End; +use Enqueue\Consumption\Context\InitLogger; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\Context\MessageResult; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\Context\PreSubscribe; +use Enqueue\Consumption\Context\ProcessorException; +use Enqueue\Consumption\Context\Start; use Enqueue\Consumption\ExtensionInterface; class BreakCycleExtension implements ExtensionInterface { - use EmptyExtensionTrait; - protected $cycles = 1; private $limit; @@ -19,15 +25,51 @@ public function __construct($limit) $this->limit = $limit; } - public function onPostReceived(Context $context) + public function onInitLogger(InitLogger $context): void + { + } + + public function onPostMessageReceived(PostMessageReceived $context): void + { + if ($this->cycles >= $this->limit) { + $context->interruptExecution(); + } else { + ++$this->cycles; + } + } + + public function onEnd(End $context): void + { + } + + public function onMessageReceived(MessageReceived $context): void + { + } + + public function onResult(MessageResult $context): void + { + } + + public function onPreConsume(PreConsume $context): void + { + } + + public function onPreSubscribe(PreSubscribe $context): void + { + } + + public function onProcessorException(ProcessorException $context): void + { + } + + public function onStart(Start $context): void { - $this->onIdle($context); } - public function onIdle(Context $context) + public function onPostConsume(PostConsume $context): void { if ($this->cycles >= $this->limit) { - $context->setExecutionInterrupted(true); + $context->interruptExecution(); } else { ++$this->cycles; } diff --git a/Tests/Consumption/Mock/DummySubscriptionConsumer.php b/Tests/Consumption/Mock/DummySubscriptionConsumer.php new file mode 100644 index 0000000..4035148 --- /dev/null +++ b/Tests/Consumption/Mock/DummySubscriptionConsumer.php @@ -0,0 +1,48 @@ +messages as list($message, $queueName)) { + /** @var InteropMessage $message */ + /** @var string $queueName */ + if (false == call_user_func($this->subscriptions[$queueName][1], $message, $this->subscriptions[$queueName][0])) { + return; + } + } + } + + public function subscribe(Consumer $consumer, callable $callback): void + { + $this->subscriptions[$consumer->getQueue()->getQueueName()] = [$consumer, $callback]; + } + + public function unsubscribe(Consumer $consumer): void + { + unset($this->subscriptions[$consumer->getQueue()->getQueueName()]); + } + + public function unsubscribeAll(): void + { + $this->subscriptions = []; + } + + public function addMessage(InteropMessage $message, string $queueName): void + { + $this->messages[] = [$message, $queueName]; + } +} diff --git a/Tests/Consumption/QueueConsumerTest.php b/Tests/Consumption/QueueConsumerTest.php index 196f755..2bcc253 100644 --- a/Tests/Consumption/QueueConsumerTest.php +++ b/Tests/Consumption/QueueConsumerTest.php @@ -2,61 +2,92 @@ namespace Enqueue\Tests\Consumption; +use Enqueue\Consumption\BoundProcessor; use Enqueue\Consumption\CallbackProcessor; use Enqueue\Consumption\ChainExtension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\End; +use Enqueue\Consumption\Context\InitLogger; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\Context\MessageResult; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\Context\PreSubscribe; +use Enqueue\Consumption\Context\ProcessorException; +use Enqueue\Consumption\Context\Start; use Enqueue\Consumption\Exception\InvalidArgumentException; +use Enqueue\Consumption\Extension\ExitStatusExtension; use Enqueue\Consumption\ExtensionInterface; use Enqueue\Consumption\QueueConsumer; use Enqueue\Consumption\Result; use Enqueue\Null\NullQueue; +use Enqueue\Test\ReadAttributeTrait; use Enqueue\Tests\Consumption\Mock\BreakCycleExtension; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; -use Interop\Queue\PsrQueue; +use Enqueue\Tests\Consumption\Mock\DummySubscriptionConsumer; +use Interop\Queue\Consumer; +use Interop\Queue\Context as InteropContext; +use Interop\Queue\Exception\SubscriptionConsumerNotSupportedException; +use Interop\Queue\Message; +use Interop\Queue\Processor; +use Interop\Queue\Queue; +use Interop\Queue\SubscriptionConsumer; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; class QueueConsumerTest extends TestCase { - public function testCouldBeConstructedWithConnectionAndExtensionsAsArguments() + use ReadAttributeTrait; + + public function testShouldSetEmptyArrayToBoundProcessorsPropertyInConstructor() { - new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub(), null, [], null, 0); + + $this->assertAttributeSame([], 'boundProcessors', $consumer); } - public function testCouldBeConstructedWithConnectionOnly() + public function testShouldSetProvidedBoundProcessorsToThePropertyInConstructor() { - new QueueConsumer($this->createPsrContextStub()); + $boundProcessors = [ + new BoundProcessor(new NullQueue('foo'), $this->createProcessorMock()), + new BoundProcessor(new NullQueue('bar'), $this->createProcessorMock()), + ]; + + $consumer = new QueueConsumer($this->createContextStub(), null, $boundProcessors, null, 0); + + $this->assertAttributeSame($boundProcessors, 'boundProcessors', $consumer); } - public function testCouldBeConstructedWithConnectionAndSingleExtension() + public function testShouldSetNullLoggerIfNoneProvidedInConstructor() { - new QueueConsumer($this->createPsrContextStub(), $this->createExtension()); + $consumer = new QueueConsumer($this->createContextStub(), null, [], null, 0); + + $this->assertAttributeInstanceOf(NullLogger::class, 'logger', $consumer); } - public function testShouldSetEmptyArrayToBoundProcessorsPropertyInConstructor() + public function testShouldSetProvidedLoggerToThePropertyInConstructor() { - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $expectedLogger = $this->createMock(LoggerInterface::class); - $this->assertAttributeSame([], 'boundProcessors', $consumer); + $consumer = new QueueConsumer($this->createContextStub(), null, [], $expectedLogger, 0); + + $this->assertAttributeSame($expectedLogger, 'logger', $consumer); } - public function testShouldAllowGetConnectionSetInConstructor() + public function testShouldAllowGetContextSetInConstructor() { - $expectedConnection = $this->createPsrContextStub(); + $expectedContext = $this->createContextStub(); - $consumer = new QueueConsumer($expectedConnection, null, 0); + $consumer = new QueueConsumer($expectedContext, null, [], null, 0); - $this->assertSame($expectedConnection, $consumer->getPsrContext()); + $this->assertSame($expectedContext, $consumer->getContext()); } public function testThrowIfQueueNameEmptyOnBind() { $processorMock = $this->createProcessorMock(); - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub()); $this->expectException(\LogicException::class); $this->expectExceptionMessage('The queue name must be not empty.'); @@ -67,7 +98,7 @@ public function testThrowIfQueueAlreadyBoundToProcessorOnBind() { $processorMock = $this->createProcessorMock(); - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub()); $consumer->bind(new NullQueue('theQueueName'), $processorMock); @@ -81,45 +112,31 @@ public function testShouldAllowBindProcessorToQueue() $queue = new NullQueue('theQueueName'); $processorMock = $this->createProcessorMock(); - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub()); $consumer->bind($queue, $processorMock); - $this->assertAttributeSame(['theQueueName' => [$queue, $processorMock]], 'boundProcessors', $consumer); + $this->assertAttributeEquals( + ['theQueueName' => new BoundProcessor($queue, $processorMock)], + 'boundProcessors', + $consumer + ); } public function testThrowIfQueueNeitherInstanceOfQueueNorString() { $processorMock = $this->createProcessorMock(); - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub()); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The argument must be an instance of Interop\Queue\PsrQueue but got stdClass.'); + $this->expectExceptionMessage('The argument must be an instance of Interop\Queue\Queue but got stdClass.'); $consumer->bind(new \stdClass(), $processorMock); } - public function testThrowIfProcessorNeitherInstanceOfProcessorNorCallable() - { - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The argument must be an instance of Interop\Queue\PsrProcessor but got stdClass.'); - $consumer->bind(new NullQueue(''), new \stdClass()); - } - - public function testCouldSetGetIdleTimeout() - { - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); - - $consumer->setIdleTimeout(123456); - - $this->assertSame(123456, $consumer->getIdleTimeout()); - } - public function testCouldSetGetReceiveTimeout() { - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub()); $consumer->setReceiveTimeout(123456); @@ -134,7 +151,7 @@ public function testShouldAllowBindCallbackToQueueName() $queueName = 'theQueueName'; $queue = new NullQueue($queueName); - $context = $this->createMock(PsrContext::class); + $context = $this->createContextWithoutSubscriptionConsumerMock(); $context ->expects($this->once()) ->method('createQueue') @@ -142,49 +159,129 @@ public function testShouldAllowBindCallbackToQueueName() ->willReturn($queue) ; - $consumer = new QueueConsumer($context, null, 0); + $consumer = new QueueConsumer($context); - $consumer->bind($queueName, $callback); + $consumer->bindCallback($queueName, $callback); $boundProcessors = $this->readAttribute($consumer, 'boundProcessors'); - $this->assertInternalType('array', $boundProcessors); + self::assertIsArray($boundProcessors); $this->assertCount(1, $boundProcessors); $this->assertArrayHasKey($queueName, $boundProcessors); - $this->assertInternalType('array', $boundProcessors[$queueName]); - $this->assertCount(2, $boundProcessors[$queueName]); - $this->assertSame($queue, $boundProcessors[$queueName][0]); - $this->assertInstanceOf(CallbackProcessor::class, $boundProcessors[$queueName][1]); + $this->assertInstanceOf(BoundProcessor::class, $boundProcessors[$queueName]); + $this->assertSame($queue, $boundProcessors[$queueName]->getQueue()); + $this->assertInstanceOf(CallbackProcessor::class, $boundProcessors[$queueName]->getProcessor()); } public function testShouldReturnSelfOnBind() { $processorMock = $this->createProcessorMock(); - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub()); - $this->assertSame($consumer, $consumer->bind(new NullQueue('aQueueName'), $processorMock)); + $this->assertSame($consumer, $consumer->bind(new NullQueue('foo_queue'), $processorMock)); + } + + public function testShouldUseContextSubscriptionConsumerIfSupport() + { + $expectedQueue = new NullQueue('theQueueName'); + + $contextSubscriptionConsumer = $this->createSubscriptionConsumerMock(); + $contextSubscriptionConsumer + ->expects($this->once()) + ->method('consume') + ; + + $fallbackSubscriptionConsumer = $this->createSubscriptionConsumerMock(); + $fallbackSubscriptionConsumer + ->expects($this->never()) + ->method('consume') + ; + + $contextMock = $this->createMock(InteropContext::class); + $contextMock + ->expects($this->once()) + ->method('createConsumer') + ->with($this->identicalTo($expectedQueue)) + ->willReturn($this->createConsumerStub()) + ; + $contextMock + ->expects($this->once()) + ->method('createSubscriptionConsumer') + ->willReturn($contextSubscriptionConsumer) + ; + + $processorMock = $this->createProcessorMock(); + $processorMock + ->expects($this->never()) + ->method('process') + ; + + $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($fallbackSubscriptionConsumer); + $queueConsumer->bind($expectedQueue, $processorMock); + $queueConsumer->consume(); + } + + public function testShouldUseFallbackSubscriptionConsumerIfNotSupported() + { + $expectedQueue = new NullQueue('theQueueName'); + + $contextSubscriptionConsumer = $this->createSubscriptionConsumerMock(); + $contextSubscriptionConsumer + ->expects($this->never()) + ->method('consume') + ; + + $fallbackSubscriptionConsumer = $this->createSubscriptionConsumerMock(); + $fallbackSubscriptionConsumer + ->expects($this->once()) + ->method('consume') + ; + + $contextMock = $this->createContextWithoutSubscriptionConsumerMock(); + $contextMock + ->expects($this->once()) + ->method('createConsumer') + ->with($this->identicalTo($expectedQueue)) + ->willReturn($this->createConsumerStub()) + ; + $contextMock + ->expects($this->once()) + ->method('createSubscriptionConsumer') + ->willThrowException(SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt()) + ; + + $processorMock = $this->createProcessorMock(); + $processorMock + ->expects($this->never()) + ->method('process') + ; + + $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($fallbackSubscriptionConsumer); + $queueConsumer->bind($expectedQueue, $processorMock); + $queueConsumer->consume(); } public function testShouldSubscribeToGivenQueueWithExpectedTimeout() { $expectedQueue = new NullQueue('theQueueName'); - $messageConsumerMock = $this->createMock(PsrConsumer::class); - $messageConsumerMock + $subscriptionConsumerMock = $this->createSubscriptionConsumerMock(); + $subscriptionConsumerMock ->expects($this->once()) - ->method('receive') + ->method('consume') ->with(12345) - ->willReturn(null) ; - $contextMock = $this->createMock(PsrContext::class); + $contextMock = $this->createContextWithoutSubscriptionConsumerMock(); $contextMock ->expects($this->once()) ->method('createConsumer') ->with($this->identicalTo($expectedQueue)) - ->willReturn($messageConsumerMock) + ->willReturn($this->createConsumerStub()) ; $processorMock = $this->createProcessorMock(); @@ -193,28 +290,28 @@ public function testShouldSubscribeToGivenQueueWithExpectedTimeout() ->method('process') ; - $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(1), 0, 12345); + $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(1), [], null, 12345); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); $queueConsumer->bind($expectedQueue, $processorMock); $queueConsumer->consume(); } - public function testShouldSubscribeToGivenQueueAndQuitAfterFifthIdleCycle() + public function testShouldSubscribeToGivenQueueAndQuitAfterFifthConsumeCycle() { $expectedQueue = new NullQueue('theQueueName'); - $messageConsumerMock = $this->createMock(PsrConsumer::class); - $messageConsumerMock + $subscriptionConsumerMock = $this->createSubscriptionConsumerMock(); + $subscriptionConsumerMock ->expects($this->exactly(5)) - ->method('receive') - ->willReturn(null) + ->method('consume') ; - $contextMock = $this->createMock(PsrContext::class); + $contextMock = $this->createContextWithoutSubscriptionConsumerMock(); $contextMock ->expects($this->once()) ->method('createConsumer') ->with($this->identicalTo($expectedQueue)) - ->willReturn($messageConsumerMock) + ->willReturn($this->createConsumerStub()) ; $processorMock = $this->createProcessorMock(); @@ -223,17 +320,30 @@ public function testShouldSubscribeToGivenQueueAndQuitAfterFifthIdleCycle() ->method('process') ; - $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(5), 0); + $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(5)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); $queueConsumer->bind($expectedQueue, $processorMock); $queueConsumer->consume(); } public function testShouldProcessFiveMessagesAndQuit() { - $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); + $fooQueue = new NullQueue('foo_queue'); + + $firstMessageMock = $this->createMessageMock(); + $secondMessageMock = $this->createMessageMock(); + $thirdMessageMock = $this->createMessageMock(); + $fourthMessageMock = $this->createMessageMock(); + $fifthMessageMock = $this->createMessageMock(); + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($firstMessageMock, 'foo_queue'); + $subscriptionConsumerMock->addMessage($secondMessageMock, 'foo_queue'); + $subscriptionConsumerMock->addMessage($thirdMessageMock, 'foo_queue'); + $subscriptionConsumerMock->addMessage($fourthMessageMock, 'foo_queue'); + $subscriptionConsumerMock->addMessage($fifthMessageMock, 'foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub(); $processorMock = $this->createProcessorMock(); $processorMock @@ -242,8 +352,9 @@ public function testShouldProcessFiveMessagesAndQuit() ->willReturn(Result::ACK) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(5), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(5)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind($fooQueue, $processorMock); $queueConsumer->consume(); } @@ -251,14 +362,18 @@ public function testShouldProcessFiveMessagesAndQuit() public function testShouldAckMessageIfProcessorReturnSuchStatus() { $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); - $messageConsumerStub + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + $consumerStub ->expects($this->once()) ->method('acknowledge') ->with($this->identicalTo($messageMock)) ; - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -268,8 +383,9 @@ public function testShouldAckMessageIfProcessorReturnSuchStatus() ->willReturn(Result::ACK) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } @@ -277,9 +393,13 @@ public function testShouldAckMessageIfProcessorReturnSuchStatus() public function testThrowIfProcessorReturnNull() { $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -289,8 +409,9 @@ public function testThrowIfProcessorReturnNull() ->willReturn(null) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $this->expectException(\LogicException::class); $this->expectExceptionMessage('Status is not supported'); @@ -300,14 +421,18 @@ public function testThrowIfProcessorReturnNull() public function testShouldRejectMessageIfProcessorReturnSuchStatus() { $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); - $messageConsumerStub + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + $consumerStub ->expects($this->once()) ->method('reject') ->with($this->identicalTo($messageMock), false) ; - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -317,8 +442,43 @@ public function testShouldRejectMessageIfProcessorReturnSuchStatus() ->willReturn(Result::REJECT) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); + + $queueConsumer->consume(); + } + + public function testShouldDoNothingIfProcessorReturnsAlreadyAcknowledged() + { + $messageMock = $this->createMessageMock(); + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + $consumerStub + ->expects($this->never()) + ->method('reject') + ; + $consumerStub + ->expects($this->never()) + ->method('acknowledge') + ; + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorMock(); + $processorMock + ->expects($this->once()) + ->method('process') + ->with($this->identicalTo($messageMock)) + ->willReturn(Result::ALREADY_ACKNOWLEDGED) + ; + + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } @@ -326,14 +486,18 @@ public function testShouldRejectMessageIfProcessorReturnSuchStatus() public function testShouldRequeueMessageIfProcessorReturnSuchStatus() { $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); - $messageConsumerStub + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + $consumerStub ->expects($this->once()) ->method('reject') ->with($this->identicalTo($messageMock), true) ; - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -343,8 +507,9 @@ public function testShouldRequeueMessageIfProcessorReturnSuchStatus() ->willReturn(Result::REQUEUE) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } @@ -352,9 +517,13 @@ public function testShouldRequeueMessageIfProcessorReturnSuchStatus() public function testThrowIfProcessorReturnInvalidStatus() { $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -364,8 +533,9 @@ public function testThrowIfProcessorReturnInvalidStatus() ->willReturn('invalidStatus') ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $this->expectException(\LogicException::class); $this->expectExceptionMessage('Status is not supported: invalidStatus'); @@ -377,17 +547,21 @@ public function testShouldNotPassMessageToProcessorIfItWasProcessedByExtension() $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onPreReceived') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) { - $context->setResult(Result::ACK); + ->method('onMessageReceived') + ->with($this->isInstanceOf(MessageReceived::class)) + ->willReturnCallback(function (MessageReceived $context) { + $context->setResult(Result::ack()); }) ; $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -396,17 +570,46 @@ public function testShouldNotPassMessageToProcessorIfItWasProcessedByExtension() ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); + + $queueConsumer->consume(); + } + + public function testShouldCallOnInitLoggerExtensionMethod() + { + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorMock(); + + $logger = $this->createMock(LoggerInterface::class); + + $extension = $this->createExtension(); + $extension + ->expects($this->once()) + ->method('onInitLogger') + ->with($this->isInstanceOf(InitLogger::class)) + ->willReturnCallback(function (InitLogger $context) use ($logger) { + $this->assertSame($logger, $context->getLogger()); + }) + ; + + $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, [], $logger); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } public function testShouldCallOnStartExtensionMethod() { - $messageConsumerStub = $this->createMessageConsumerStub($message = null); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); @@ -414,472 +617,530 @@ public function testShouldCallOnStartExtensionMethod() $extension ->expects($this->once()) ->method('onStart') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ($contextStub) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertNull($context->getPsrConsumer()); - $this->assertNull($context->getPsrProcessor()); - $this->assertNull($context->getLogger()); - $this->assertNull($context->getPsrMessage()); - $this->assertNull($context->getException()); - $this->assertNull($context->getResult()); - $this->assertNull($context->getPsrQueue()); - $this->assertFalse($context->isExecutionInterrupted()); + ->with($this->isInstanceOf(Start::class)) + ->willReturnCallback(function (Start $context) use ($contextStub) { + $this->assertSame($contextStub, $context->getContext()); + $this->assertInstanceOf(NullLogger::class, $context->getLogger()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallOnIdleExtensionMethod() + public function testShouldCallOnStartWithLoggerProvidedInConstructor() { - $messageConsumerStub = $this->createMessageConsumerStub($message = null); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); + $expectedLogger = $this->createMock(LoggerInterface::class); + $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onIdle') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getPsrMessage()); - $this->assertNull($context->getException()); - $this->assertNull($context->getResult()); - $this->assertFalse($context->isExecutionInterrupted()); + ->method('onStart') + ->with($this->isInstanceOf(Start::class)) + ->willReturnCallback(function (Start $context) use ($expectedLogger) { + $this->assertSame($expectedLogger, $context->getLogger()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, [], $expectedLogger); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallOnBeforeReceiveExtensionMethod() + public function testShouldInterruptExecutionOnStart() { - $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); - $processorMock = $this->createProcessorStub(); + $processorMock = $this->createProcessorMock(); + $processorMock + ->expects($this->never()) + ->method('process') + ; - $queue = new NullQueue('aQueueName'); + $expectedLogger = $this->createMock(LoggerInterface::class); $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onBeforeReceive') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $queue - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getPsrMessage()); - $this->assertNull($context->getException()); - $this->assertNull($context->getResult()); - $this->assertFalse($context->isExecutionInterrupted()); - $this->assertSame($queue, $context->getPsrQueue()); + ->method('onStart') + ->willReturnCallback(function (Start $context) { + $context->interruptExecution(); }) ; + $extension + ->expects($this->once()) + ->method('onEnd') + ; + $extension + ->expects($this->never()) + ->method('onPreConsume') + ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind($queue, $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, [], $expectedLogger); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallOnPreReceivedExtensionMethodWithExpectedContext() + public function testShouldCallPreSubscribeExtensionMethod() { - $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); - $processorMock = $this->createProcessorStub(); + $processorMock = $this->createProcessorMock(); $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onPreReceived') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $expectedMessage - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); + ->method('onPreSubscribe') + ->with($this->isInstanceOf(PreSubscribe::class)) + ->willReturnCallback(function (PreSubscribe $context) use ($contextStub, $consumerStub, $processorMock) { + $this->assertSame($contextStub, $context->getContext()); + $this->assertSame($consumerStub, $context->getConsumer()); + $this->assertSame($processorMock, $context->getProcessor()); $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getException()); - $this->assertNull($context->getResult()); - $this->assertFalse($context->isExecutionInterrupted()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallOnResultExtensionMethodWithExpectedContext() + public function testShouldCallPreSubscribeForEachBoundProcessor() { - $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); - $processorMock = $this->createProcessorStub(); + $processorMock = $this->createProcessorMock(); + + $extension = $this->createExtension(); + $extension + ->expects($this->exactly(3)) + ->method('onPreSubscribe') + ->with($this->isInstanceOf(PreSubscribe::class)) + ; + + $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); + $queueConsumer->bind(new NullQueue('bar_queue'), $processorMock); + $queueConsumer->bind(new NullQueue('baz_queue'), $processorMock); + + $queueConsumer->consume(); + } + + public function testShouldCallOnPostConsumeExtensionMethod() + { + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $subscriptionConsumer = new DummySubscriptionConsumer(); + + $processorMock = $this->createProcessorMock(); $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onResult') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $expectedMessage - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); + ->method('onPostConsume') + ->with($this->isInstanceOf(PostConsume::class)) + ->willReturnCallback(function (PostConsume $context) use ($contextStub, $subscriptionConsumer) { + $this->assertSame($contextStub, $context->getContext()); + $this->assertSame($subscriptionConsumer, $context->getSubscriptionConsumer()); + $this->assertSame(1, $context->getCycle()); + $this->assertSame(0, $context->getReceivedMessagesCount()); + $this->assertGreaterThan(1, $context->getStartTime()); $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getException()); - $this->assertSame(Result::ACK, $context->getResult()); $this->assertFalse($context->isExecutionInterrupted()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumer); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallOnPostReceivedExtensionMethodWithExpectedContext() + public function testShouldCallOnPreConsumeExtensionMethod() { - $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorStub(); + $queue = new NullQueue('foo_queue'); + $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onPostReceived') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $expectedMessage - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); + ->method('onPreConsume') + ->with($this->isInstanceOf(PreConsume::class)) + ->willReturnCallback(function (PreConsume $context) use ($contextStub) { + $this->assertSame($contextStub, $context->getContext()); $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getException()); - $this->assertSame(Result::ACK, $context->getResult()); + $this->assertInstanceOf(SubscriptionConsumer::class, $context->getSubscriptionConsumer()); + $this->assertSame(10000, $context->getReceiveTimeout()); + $this->assertSame(1, $context->getCycle()); + $this->assertGreaterThan(0, $context->getStartTime()); $this->assertFalse($context->isExecutionInterrupted()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind($queue, $processorMock); $queueConsumer->consume(); } - public function testShouldAllowInterruptConsumingOnIdle() + public function testShouldCallOnPreConsumeExpectedAmountOfTimes() { - $messageConsumerStub = $this->createMessageConsumerStub($message = null); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); - $processorMock = $this->createProcessorMock(); + $processorMock = $this->createProcessorStub(); + + $queue = new NullQueue('foo_queue'); $extension = $this->createExtension(); $extension - ->expects($this->once()) - ->method('onIdle') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) { - $context->setExecutionInterrupted(true); - }) + ->expects($this->exactly(3)) + ->method('onPreConsume') ; + + $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(3)]); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind($queue, $processorMock); + + $queueConsumer->consume(); + } + + public function testShouldCallOnPreReceivedExtensionMethodWithExpectedContext() + { + $expectedMessage = $this->createMessageMock(); + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorStub(); + + $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onInterrupted') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( + ->method('onMessageReceived') + ->with($this->isInstanceOf(MessageReceived::class)) + ->willReturnCallback(function (MessageReceived $context) use ( $contextStub, - $messageConsumerStub, - $processorMock + $consumerStub, + $processorMock, + $expectedMessage ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); + $this->assertSame($contextStub, $context->getContext()); + $this->assertSame($consumerStub, $context->getConsumer()); + $this->assertSame($processorMock, $context->getProcessor()); + $this->assertSame($expectedMessage, $context->getMessage()); $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getPsrMessage()); - $this->assertNull($context->getException()); $this->assertNull($context->getResult()); - $this->assertTrue($context->isExecutionInterrupted()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldNotCloseContextWhenConsumptionInterrupted() + public function testShouldCallOnResultExtensionMethodWithExpectedContext() { - $messageConsumerStub = $this->createMessageConsumerStub($message = null); + $expectedMessage = $this->createMessageMock(); - $contextStub = $this->createPsrContextStub($messageConsumerStub); - $contextStub - ->expects($this->never()) - ->method('close') - ; + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); - $processorMock = $this->createProcessorMock(); + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorStub(); $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onIdle') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) { - $context->setExecutionInterrupted(true); + ->method('onResult') + ->with($this->isInstanceOf(MessageResult::class)) + ->willReturnCallback(function (MessageResult $context) use ($contextStub, $expectedMessage) { + $this->assertSame($contextStub, $context->getContext()); + $this->assertSame($expectedMessage, $context->getMessage()); + $this->assertInstanceOf(NullLogger::class, $context->getLogger()); + $this->assertSame(Result::ACK, $context->getResult()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldNotCloseContextWhenConsumptionInterruptedByException() + public function testShouldCallOnProcessorExceptionExtensionMethodWithExpectedContext() { - $expectedException = new \Exception(); + $exception = new \LogicException('Exception exception'); - $messageConsumerStub = $this->createMessageConsumerStub($message = $this->createMessageMock()); + $expectedMessage = $this->createMessageMock(); - $contextStub = $this->createPsrContextStub($messageConsumerStub); - $contextStub - ->expects($this->never()) - ->method('close') - ; + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); - $processorMock = $this->createProcessorMock(); + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorStub(); $processorMock ->expects($this->once()) ->method('process') - ->willThrowException($expectedException) + ->willThrowException($exception) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); - - try { - $queueConsumer->consume(); - } catch (\Exception $e) { - $this->assertSame($expectedException, $e); - $this->assertNull($e->getPrevious()); + $extension = $this->createExtension(); + $extension + ->expects($this->never()) + ->method('onResult') + ; + $extension + ->expects($this->once()) + ->method('onProcessorException') + ->with($this->isInstanceOf(ProcessorException::class)) + ->willReturnCallback(function (ProcessorException $context) use ($contextStub, $expectedMessage, $exception) { + $this->assertSame($contextStub, $context->getContext()); + $this->assertSame($expectedMessage, $context->getMessage()); + $this->assertSame($exception, $context->getException()); + $this->assertGreaterThan(1, $context->getReceivedAt()); + $this->assertInstanceOf(NullLogger::class, $context->getLogger()); + $this->assertNull($context->getResult()); + }) + ; - return; - } + $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); - $this->fail('Exception throw is expected.'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Exception exception'); + $queueConsumer->consume(); } - public function testShouldSetMainExceptionAsPreviousToExceptionThrownOnInterrupt() + public function testShouldContinueConsumptionIfResultSetOnProcessorExceptionExtension() { - $mainException = new \Exception(); - $expectedException = new \Exception(); + $result = Result::ack(); - $messageConsumerStub = $this->createMessageConsumerStub($message = $this->createMessageMock()); + $expectedMessage = $this->createMessageMock(); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); - $processorMock = $this->createProcessorMock(); + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorStub(); $processorMock ->expects($this->once()) ->method('process') - ->willThrowException($mainException) + ->willThrowException(new \LogicException()) ; $extension = $this->createExtension(); $extension - ->expects($this->atLeastOnce()) - ->method('onInterrupted') - ->willThrowException($expectedException) + ->expects($this->once()) + ->method('onProcessorException') + ->willReturnCallback(function (ProcessorException $context) use ($result) { + $context->setResult($result); + }) + ; + $extension + ->expects($this->once()) + ->method('onResult') + ->willReturnCallback(function (MessageResult $context) use ($result) { + $this->assertSame($result, $context->getResult()); + }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); - try { - $queueConsumer->consume(); - } catch (\Exception $e) { - $this->assertSame($expectedException, $e); - $this->assertSame($mainException, $e->getPrevious()); - - return; - } - - $this->fail('Exception throw is expected.'); + $queueConsumer->consume(); } - public function testShouldAllowInterruptConsumingOnPreReceiveButProcessCurrentMessage() + public function testShouldCallOnPostMessageReceivedExtensionMethodWithExpectedContext() { $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); - $processorMock = $this->createProcessorMock(); - $processorMock - ->expects($this->once()) - ->method('process') - ->willReturn(Result::ACK) - ; + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorStub(); $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onPreReceived') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) { - $context->setExecutionInterrupted(true); - }) - ; - $extension - ->expects($this->atLeastOnce()) - ->method('onInterrupted') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( + ->method('onPostMessageReceived') + ->with($this->isInstanceOf(PostMessageReceived::class)) + ->willReturnCallback(function (PostMessageReceived $context) use ( $contextStub, - $messageConsumerStub, - $processorMock, $expectedMessage ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); + $this->assertSame($contextStub, $context->getContext()); + $this->assertSame($expectedMessage, $context->getMessage()); $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getException()); $this->assertSame(Result::ACK, $context->getResult()); - $this->assertTrue($context->isExecutionInterrupted()); + $this->assertFalse($context->isExecutionInterrupted()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldAllowInterruptConsumingOnResult() + public function testShouldAllowInterruptConsumingOnPostConsume() { - $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); - $processorMock - ->expects($this->once()) - ->method('process') - ->willReturn(Result::ACK) - ; $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onResult') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) { - $context->setExecutionInterrupted(true); + ->method('onPostConsume') + ->with($this->isInstanceOf(PostConsume::class)) + ->willReturnCallback(function (PostConsume $context) { + $context->interruptExecution(); }) ; $extension - ->expects($this->atLeastOnce()) - ->method('onInterrupted') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $expectedMessage - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); + ->expects($this->once()) + ->method('onEnd') + ->with($this->isInstanceOf(End::class)) + ->willReturnCallback(function (End $context) use ($contextStub) { + $this->assertSame($contextStub, $context->getContext()); $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getException()); - $this->assertSame(Result::ACK, $context->getResult()); - $this->assertTrue($context->isExecutionInterrupted()); + $this->assertGreaterThan(1, $context->getStartTime()); + $this->assertGreaterThan(1, $context->getEndTime()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldAllowInterruptConsumingOnPostReceive() + public function testShouldSetMainExceptionAsPreviousToExceptionThrownOnProcessorException() + { + $mainException = new \Exception(); + $expectedException = new \Exception(); + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($this->createMessageMock(), 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorMock(); + $processorMock + ->expects($this->once()) + ->method('process') + ->willThrowException($mainException) + ; + + $extension = $this->createExtension(); + $extension + ->expects($this->atLeastOnce()) + ->method('onProcessorException') + ->willThrowException($expectedException) + ; + + $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); + + try { + $queueConsumer->consume(); + } catch (\Exception $e) { + $this->assertSame($expectedException, $e); + $this->assertSame($mainException, $e->getPrevious()); + + return; + } + + $this->fail('Exception throw is expected.'); + } + + public function testShouldAllowInterruptConsumingOnPostMessageReceived() { $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -891,47 +1152,37 @@ public function testShouldAllowInterruptConsumingOnPostReceive() $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onPostReceived') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) { - $context->setExecutionInterrupted(true); + ->method('onPostMessageReceived') + ->with($this->isInstanceOf(PostMessageReceived::class)) + ->willReturnCallback(function (PostMessageReceived $context) { + $context->interruptExecution(); }) ; $extension ->expects($this->atLeastOnce()) - ->method('onInterrupted') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $expectedMessage - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); - $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getException()); - $this->assertSame(Result::ACK, $context->getResult()); - $this->assertTrue($context->isExecutionInterrupted()); - }) + ->method('onEnd') + ->with($this->isInstanceOf(End::class)) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallOnInterruptedIfExceptionThrow() + public function testShouldNotCallOnEndIfExceptionThrow() { $expectedException = new \Exception('Process failed'); $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -942,30 +1193,14 @@ public function testShouldCallOnInterruptedIfExceptionThrow() $extension = $this->createExtension(); $extension - ->expects($this->atLeastOnce()) - ->method('onInterrupted') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $expectedMessage, - $expectedException - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); - $this->assertSame($expectedException, $context->getException()); - $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getResult()); - $this->assertTrue($context->isExecutionInterrupted()); - }) + ->expects($this->never()) + ->method('onEnd') ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $this->expectException(\Exception::class); $this->expectExceptionMessage('Process failed'); @@ -975,9 +1210,13 @@ public function testShouldCallOnInterruptedIfExceptionThrow() public function testShouldCallExtensionPassedOnRuntime() { $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -987,44 +1226,59 @@ public function testShouldCallExtensionPassedOnRuntime() ; $runtimeExtension = $this->createExtension(); + $runtimeExtension + ->expects($this->once()) + ->method('onInitLogger') + ->with($this->isInstanceOf(InitLogger::class)) + ; $runtimeExtension ->expects($this->once()) ->method('onStart') - ->with($this->isInstanceOf(Context::class)) + ->with($this->isInstanceOf(Start::class)) ; $runtimeExtension ->expects($this->once()) - ->method('onBeforeReceive') - ->with($this->isInstanceOf(Context::class)) + ->method('onPreSubscribe') + ->with($this->isInstanceOf(PreSubscribe::class)) ; $runtimeExtension ->expects($this->once()) - ->method('onPreReceived') - ->with($this->isInstanceOf(Context::class)) + ->method('onPreConsume') + ->with($this->isInstanceOf(PreConsume::class)) + ; + $runtimeExtension + ->expects($this->once()) + ->method('onMessageReceived') + ->with($this->isInstanceOf(MessageReceived::class)) ; $runtimeExtension ->expects($this->once()) ->method('onResult') - ->with($this->isInstanceOf(Context::class)) + ->with($this->isInstanceOf(MessageResult::class)) ; $runtimeExtension ->expects($this->once()) - ->method('onPostReceived') - ->with($this->isInstanceOf(Context::class)) + ->method('onPostMessageReceived') + ->with($this->isInstanceOf(PostMessageReceived::class)) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(new ChainExtension([$runtimeExtension])); } - public function testShouldChangeLoggerOnStart() + public function testShouldChangeLoggerOnInitLogger() { $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -1036,136 +1290,204 @@ public function testShouldChangeLoggerOnStart() $expectedLogger = new NullLogger(); $extension = $this->createExtension(); + $extension + ->expects($this->atLeastOnce()) + ->method('onInitLogger') + ->with($this->isInstanceOf(InitLogger::class)) + ->willReturnCallback(function (InitLogger $context) use ($expectedLogger) { + $context->changeLogger($expectedLogger); + }) + ; $extension ->expects($this->atLeastOnce()) ->method('onStart') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ($expectedLogger) { - $context->setLogger($expectedLogger); + ->with($this->isInstanceOf(Start::class)) + ->willReturnCallback(function (Start $context) use ($expectedLogger) { + $this->assertSame($expectedLogger, $context->getLogger()); + }) + ; + $extension + ->expects($this->atLeastOnce()) + ->method('onPreSubscribe') + ->with($this->isInstanceOf(PreSubscribe::class)) + ->willReturnCallback(function (PreSubscribe $context) use ($expectedLogger) { + $this->assertSame($expectedLogger, $context->getLogger()); }) ; $extension ->expects($this->atLeastOnce()) - ->method('onBeforeReceive') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ($expectedLogger) { + ->method('onPreConsume') + ->with($this->isInstanceOf(PreConsume::class)) + ->willReturnCallback(function (PreConsume $context) use ($expectedLogger) { $this->assertSame($expectedLogger, $context->getLogger()); }) ; $extension ->expects($this->atLeastOnce()) - ->method('onPreReceived') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ($expectedLogger) { + ->method('onMessageReceived') + ->with($this->isInstanceOf(MessageReceived::class)) + ->willReturnCallback(function (MessageReceived $context) use ($expectedLogger) { $this->assertSame($expectedLogger, $context->getLogger()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallEachQueueOneByOne() + public function testShouldCallProcessorAsMessageComeAlong() { - $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); + $queue1 = new NullQueue('foo_queue'); + $queue2 = new NullQueue('bar_queue'); + + $firstMessage = $this->createMessageMock(); + $secondMessage = $this->createMessageMock(); + $thirdMessage = $this->createMessageMock(); + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($firstMessage, 'foo_queue'); + $subscriptionConsumerMock->addMessage($secondMessage, 'bar_queue'); + $subscriptionConsumerMock->addMessage($thirdMessage, 'foo_queue'); + + $fooConsumerStub = $this->createConsumerStub($queue1); + $barConsumerStub = $this->createConsumerStub($queue2); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $consumers = [ + 'foo_queue' => $fooConsumerStub, + 'bar_queue' => $barConsumerStub, + ]; + + $contextStub = $this->createContextWithoutSubscriptionConsumerMock(); + $contextStub + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $queueName) { + return new NullQueue($queueName); + }) + ; + $contextStub + ->expects($this->any()) + ->method('createConsumer') + ->willReturnCallback(function (Queue $queue) use ($consumers) { + return $consumers[$queue->getQueueName()]; + }) + ; $processorMock = $this->createProcessorStub(); $anotherProcessorMock = $this->createProcessorStub(); - $queue1 = new NullQueue('aQueueName'); - $queue2 = new NullQueue('aAnotherQueueName'); + /** @var MessageReceived[] $actualContexts */ + $actualContexts = []; $extension = $this->createExtension(); $extension - ->expects($this->at(1)) - ->method('onBeforeReceive') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ($processorMock, $queue1) { - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($queue1, $context->getPsrQueue()); - }) - ; - $extension - ->expects($this->at(5)) - ->method('onBeforeReceive') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ($anotherProcessorMock, $queue2) { - $this->assertSame($anotherProcessorMock, $context->getPsrProcessor()); - $this->assertSame($queue2, $context->getPsrQueue()); + ->expects($this->any()) + ->method('onMessageReceived') + ->with($this->isInstanceOf(MessageReceived::class)) + ->willReturnCallback(function (MessageReceived $context) use (&$actualContexts) { + $actualContexts[] = clone $context; }) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(2), 0); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(3)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); $queueConsumer ->bind($queue1, $processorMock) ->bind($queue2, $anotherProcessorMock) ; $queueConsumer->consume(new ChainExtension([$extension])); + + $this->assertCount(3, $actualContexts); + + $this->assertSame($firstMessage, $actualContexts[0]->getMessage()); + $this->assertSame($secondMessage, $actualContexts[1]->getMessage()); + $this->assertSame($thirdMessage, $actualContexts[2]->getMessage()); + + $this->assertSame($fooConsumerStub, $actualContexts[0]->getConsumer()); + $this->assertSame($barConsumerStub, $actualContexts[1]->getConsumer()); + $this->assertSame($fooConsumerStub, $actualContexts[2]->getConsumer()); + } + + public function testCaptureExitStatus() + { + $testExitCode = 5; + + $stubExtension = $this->createExtension(); + + $stubExtension + ->expects($this->once()) + ->method('onStart') + ->with($this->isInstanceOf(Start::class)) + ->willReturnCallback(function (Start $context) use ($testExitCode) { + $context->interruptExecution($testExitCode); + }) + ; + + $exitExtension = new ExitStatusExtension(); + + $consumer = new QueueConsumer($this->createContextStub(), $stubExtension); + $consumer->consume(new ChainExtension([$exitExtension])); + + $this->assertEquals($testExitCode, $exitExtension->getExitStatus()); } /** - * @param null|mixed $message - * - * @return \PHPUnit_Framework_MockObject_MockObject|PsrConsumer + * @return \PHPUnit\Framework\MockObject\MockObject */ - protected function createMessageConsumerStub($message = null) + private function createContextWithoutSubscriptionConsumerMock(): InteropContext { - $messageConsumerMock = $this->createMock(PsrConsumer::class); - $messageConsumerMock + $contextMock = $this->createMock(InteropContext::class); + $contextMock ->expects($this->any()) - ->method('receive') - ->willReturn($message) + ->method('createSubscriptionConsumer') + ->willThrowException(SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt()) ; - return $messageConsumerMock; + return $contextMock; } /** - * @param null|mixed $messageConsumer - * - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return \PHPUnit\Framework\MockObject\MockObject|InteropContext */ - protected function createPsrContextStub($messageConsumer = null) + private function createContextStub(?Consumer $consumer = null): InteropContext { - $context = $this->createMock(PsrContext::class); - $context - ->expects($this->any()) - ->method('createConsumer') - ->willReturn($messageConsumer) - ; + $context = $this->createContextWithoutSubscriptionConsumerMock(); $context ->expects($this->any()) ->method('createQueue') - ->willReturn($this->createMock(PsrQueue::class)) + ->willReturnCallback(function (string $queueName) { + return new NullQueue($queueName); + }) ; $context ->expects($this->any()) - ->method('close') + ->method('createConsumer') + ->willReturnCallback(function (Queue $queue) use ($consumer) { + return $consumer ?: $this->createConsumerStub($queue); + }) ; return $context; } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor + * @return \PHPUnit\Framework\MockObject\MockObject|Processor */ - protected function createProcessorMock() + private function createProcessorMock() { - return $this->createMock(PsrProcessor::class); + return $this->createMock(Processor::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor + * @return \PHPUnit\Framework\MockObject\MockObject|Processor */ - protected function createProcessorStub() + private function createProcessorStub() { $processorMock = $this->createProcessorMock(); $processorMock @@ -1178,18 +1500,50 @@ protected function createProcessorStub() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrMessage + * @return \PHPUnit\Framework\MockObject\MockObject|Message */ - protected function createMessageMock() + private function createMessageMock(): Message { - return $this->createMock(PsrMessage::class); + return $this->createMock(Message::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ExtensionInterface + * @return \PHPUnit\Framework\MockObject\MockObject|ExtensionInterface */ - protected function createExtension() + private function createExtension() { return $this->createMock(ExtensionInterface::class); } + + /** + * @param mixed|null $queue + * + * @return \PHPUnit\Framework\MockObject\MockObject|Consumer + */ + private function createConsumerStub($queue = null): Consumer + { + if (null === $queue) { + $queue = 'queue'; + } + if (is_string($queue)) { + $queue = new NullQueue($queue); + } + + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue) + ; + + return $consumerMock; + } + + /** + * @return SubscriptionConsumer|\PHPUnit\Framework\MockObject\MockObject + */ + private function createSubscriptionConsumerMock(): SubscriptionConsumer + { + return $this->createMock(SubscriptionConsumer::class); + } } diff --git a/Tests/DoctrineConnectionFactoryFactoryTest.php b/Tests/DoctrineConnectionFactoryFactoryTest.php new file mode 100644 index 0000000..14f7b10 --- /dev/null +++ b/Tests/DoctrineConnectionFactoryFactoryTest.php @@ -0,0 +1,71 @@ +registry = $this->prophesize(ManagerRegistry::class); + $this->fallbackFactory = $this->prophesize(ConnectionFactoryFactoryInterface::class); + + $this->factory = new DoctrineConnectionFactoryFactory($this->registry->reveal(), $this->fallbackFactory->reveal()); + } + + public function testCreateWithoutArray() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The config must be either array or DSN string.'); + + $this->factory->create(true); + } + + public function testCreateWithoutDsn() + { + $this->expectExceptionMessage(\InvalidArgumentException::class); + $this->expectExceptionMessage('The config must have dsn key set.'); + + $this->factory->create(['foo' => 'bar']); + } + + public function testCreateWithDoctrineSchema() + { + $this->assertInstanceOf( + ManagerRegistryConnectionFactory::class, + $this->factory->create('doctrine://localhost:3306') + ); + } + + public function testCreateFallback() + { + $this->fallbackFactory + ->create(['dsn' => 'fallback://']) + ->shouldBeCalled(); + + $this->factory->create(['dsn' => 'fallback://']); + } +} diff --git a/Tests/Functions/DsnToConnectionFactoryFunctionTest.php b/Tests/Functions/DsnToConnectionFactoryFunctionTest.php deleted file mode 100644 index f123b11..0000000 --- a/Tests/Functions/DsnToConnectionFactoryFunctionTest.php +++ /dev/null @@ -1,101 +0,0 @@ -expectException(\LogicException::class); - $this->expectExceptionMessage('The scheme could not be parsed from DSN ""'); - - \Enqueue\dsn_to_connection_factory(''); - } - - public function testThrowIfDsnMissingScheme() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The scheme could not be parsed from DSN "dsnMissingScheme"'); - - \Enqueue\dsn_to_connection_factory('dsnMissingScheme'); - } - - public function testThrowIfDsnNotSupported() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The scheme "http" is not supported. Supported "file", "amqp+ext"'); - - \Enqueue\dsn_to_connection_factory('http://schemeNotSupported'); - } - - /** - * @dataProvider provideDSNs - * - * @param mixed $dsn - * @param mixed $expectedFactoryClass - */ - public function testReturnsExpectedFactoryInstance($dsn, $expectedFactoryClass) - { - $factory = \Enqueue\dsn_to_connection_factory($dsn); - - $this->assertInstanceOf($expectedFactoryClass, $factory); - } - - public static function provideDSNs() - { - yield ['amqp:', AmqpExtConnectionFactory::class]; - - yield ['amqps:', AmqpExtConnectionFactory::class]; - - yield ['amqp+ext:', AmqpExtConnectionFactory::class]; - - yield ['amqps+ext:', AmqpExtConnectionFactory::class]; - - yield ['amqp+lib:', AmqpLibConnectionFactory::class]; - - yield ['amqps+lib:', AmqpLibConnectionFactory::class]; - - yield ['amqp+bunny:', AmqpBunnyConnectionFactory::class]; - - yield ['amqp://user:pass@foo/vhost', AmqpExtConnectionFactory::class]; - - yield ['file:', FsConnectionFactory::class]; - - yield ['file:///foo/bar/baz', FsConnectionFactory::class]; - - yield ['null:', NullConnectionFactory::class]; - - yield ['mysql:', DbalConnectionFactory::class]; - - yield ['pgsql:', DbalConnectionFactory::class]; - - yield ['beanstalk:', PheanstalkConnectionFactory::class]; - -// yield ['gearman:', GearmanConnectionFactory::class]; - - yield ['kafka:', RdKafkaConnectionFactory::class]; - - yield ['redis:', RedisConnectionFactory::class]; - - yield ['stomp:', StompConnectionFactory::class]; - - yield ['sqs:', SqsConnectionFactory::class]; - - yield ['gps:', GpsConnectionFactory::class]; - } -} diff --git a/Tests/Functions/DsnToContextFunctionTest.php b/Tests/Functions/DsnToContextFunctionTest.php deleted file mode 100644 index 2a8bc83..0000000 --- a/Tests/Functions/DsnToContextFunctionTest.php +++ /dev/null @@ -1,73 +0,0 @@ -expectException(\LogicException::class); - $this->expectExceptionMessage('The scheme could not be parsed from DSN ""'); - - \Enqueue\dsn_to_context(''); - } - - public function testThrowIfDsnMissingScheme() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The scheme could not be parsed from DSN "dsnMissingScheme"'); - - \Enqueue\dsn_to_context('dsnMissingScheme'); - } - - public function testThrowIfDsnNotSupported() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The scheme "http" is not supported. Supported "file", "amqp+ext"'); - - \Enqueue\dsn_to_context('http://schemeNotSupported'); - } - - /** - * @dataProvider provideDSNs - * - * @param mixed $dsn - * @param mixed $expectedFactoryClass - */ - public function testReturnsExpectedFactoryInstance($dsn, $expectedFactoryClass) - { - $factory = \Enqueue\dsn_to_context($dsn); - - $this->assertInstanceOf($expectedFactoryClass, $factory); - } - - public static function provideDSNs() - { - yield ['amqp:', AmqpContext::class]; - - yield ['amqp://user:pass@foo/vhost', AmqpContext::class]; - - yield ['file:', FsContext::class]; - - yield ['file://'.sys_get_temp_dir(), FsContext::class]; - - yield ['null:', NullContext::class]; - - yield ['redis:', RedisContext::class]; - - yield ['stomp:', StompContext::class]; - - yield ['sqs:', SqsContext::class]; - - yield ['gps:', GpsContext::class]; - } -} diff --git a/Tests/Mocks/CustomPrepareBodyClientExtension.php b/Tests/Mocks/CustomPrepareBodyClientExtension.php new file mode 100644 index 0000000..dd0e1a6 --- /dev/null +++ b/Tests/Mocks/CustomPrepareBodyClientExtension.php @@ -0,0 +1,20 @@ +getMessage()->setBody('theCommandBodySerializedByCustomExtension'); + } + + public function onPreSendEvent(PreSend $context): void + { + $context->getMessage()->setBody('theEventBodySerializedByCustomExtension'); + } +} diff --git a/Tests/Mocks/JsonSerializableObject.php b/Tests/Mocks/JsonSerializableObject.php new file mode 100644 index 0000000..84885c3 --- /dev/null +++ b/Tests/Mocks/JsonSerializableObject.php @@ -0,0 +1,12 @@ + 'fooVal']; + } +} diff --git a/Tests/ResourcesTest.php b/Tests/ResourcesTest.php new file mode 100644 index 0000000..ec713fd --- /dev/null +++ b/Tests/ResourcesTest.php @@ -0,0 +1,149 @@ +assertTrue($rc->isFinal()); + } + + public function testShouldConstructorBePrivate() + { + $rc = new \ReflectionClass(Resources::class); + + $this->assertTrue($rc->getConstructor()->isPrivate()); + } + + public function testShouldGetAvailableConnectionsInExpectedFormat() + { + $availableConnections = Resources::getAvailableConnections(); + + self::assertIsArray($availableConnections); + $this->assertArrayHasKey(RedisConnectionFactory::class, $availableConnections); + + $connectionInfo = $availableConnections[RedisConnectionFactory::class]; + $this->assertArrayHasKey('schemes', $connectionInfo); + $this->assertSame(['redis', 'rediss'], $connectionInfo['schemes']); + + $this->assertArrayHasKey('supportedSchemeExtensions', $connectionInfo); + $this->assertSame(['predis', 'phpredis'], $connectionInfo['supportedSchemeExtensions']); + + $this->assertArrayHasKey('package', $connectionInfo); + $this->assertSame('enqueue/redis', $connectionInfo['package']); + } + + public function testShouldGetKnownConnectionsInExpectedFormat() + { + $availableConnections = Resources::getKnownConnections(); + + self::assertIsArray($availableConnections); + $this->assertArrayHasKey(RedisConnectionFactory::class, $availableConnections); + + $connectionInfo = $availableConnections[RedisConnectionFactory::class]; + $this->assertArrayHasKey('schemes', $connectionInfo); + $this->assertSame(['redis', 'rediss'], $connectionInfo['schemes']); + + $this->assertArrayHasKey('supportedSchemeExtensions', $connectionInfo); + $this->assertSame(['predis', 'phpredis'], $connectionInfo['supportedSchemeExtensions']); + + $this->assertArrayHasKey('package', $connectionInfo); + $this->assertSame('enqueue/redis', $connectionInfo['package']); + } + + public function testThrowsIfConnectionClassNotImplementConnectionFactoryInterfaceOnAddConnection() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The connection factory class "stdClass" must implement "Interop\Queue\ConnectionFactory" interface.'); + + Resources::addConnection(\stdClass::class, [], [], 'foo'); + } + + public function testThrowsIfNoSchemesProvidedOnAddConnection() + { + $connectionClass = $this->getMockClass(ConnectionFactory::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Schemes could not be empty.'); + + Resources::addConnection($connectionClass, [], [], 'foo'); + } + + public function testThrowsIfNoPackageProvidedOnAddConnection() + { + $connectionClass = $this->getMockClass(ConnectionFactory::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Package name could not be empty.'); + + Resources::addConnection($connectionClass, ['foo'], [], ''); + } + + public function testShouldAllowRegisterConnectionThatIsNotInstalled() + { + Resources::addConnection('theConnectionClass', ['foo'], [], 'foo'); + + $knownConnections = Resources::getKnownConnections(); + self::assertIsArray($knownConnections); + $this->assertArrayHasKey('theConnectionClass', $knownConnections); + + $availableConnections = Resources::getAvailableConnections(); + + self::assertIsArray($availableConnections); + $this->assertArrayNotHasKey('theConnectionClass', $availableConnections); + } + + public function testShouldAllowGetPreviouslyRegisteredConnection() + { + $connectionClass = $this->getMockClass(ConnectionFactory::class); + + Resources::addConnection( + $connectionClass, + ['fooscheme', 'barscheme'], + ['fooextension', 'barextension'], + 'foo/bar' + ); + + $availableConnections = Resources::getAvailableConnections(); + + self::assertIsArray($availableConnections); + $this->assertArrayHasKey($connectionClass, $availableConnections); + + $connectionInfo = $availableConnections[$connectionClass]; + $this->assertArrayHasKey('schemes', $connectionInfo); + $this->assertSame(['fooscheme', 'barscheme'], $connectionInfo['schemes']); + + $this->assertArrayHasKey('supportedSchemeExtensions', $connectionInfo); + $this->assertSame(['fooextension', 'barextension'], $connectionInfo['supportedSchemeExtensions']); + + $this->assertArrayHasKey('package', $connectionInfo); + $this->assertSame('foo/bar', $connectionInfo['package']); + } + + public function testShouldHaveRegisteredWampConfiguration() + { + $availableConnections = Resources::getKnownConnections(); + + self::assertIsArray($availableConnections); + $this->assertArrayHasKey(WampConnectionFactory::class, $availableConnections); + + $connectionInfo = $availableConnections[WampConnectionFactory::class]; + $this->assertArrayHasKey('schemes', $connectionInfo); + $this->assertSame(['wamp', 'ws'], $connectionInfo['schemes']); + + $this->assertArrayHasKey('supportedSchemeExtensions', $connectionInfo); + $this->assertSame([], $connectionInfo['supportedSchemeExtensions']); + + $this->assertArrayHasKey('package', $connectionInfo); + $this->assertSame('enqueue/wamp', $connectionInfo['package']); + } +} diff --git a/Tests/Router/RecipientTest.php b/Tests/Router/RecipientTest.php index 3f37372..57cc83e 100644 --- a/Tests/Router/RecipientTest.php +++ b/Tests/Router/RecipientTest.php @@ -3,26 +3,26 @@ namespace Enqueue\Tests\Router; use Enqueue\Router\Recipient; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrMessage; +use Interop\Queue\Destination; +use Interop\Queue\Message as InteropMessage; use PHPUnit\Framework\TestCase; class RecipientTest extends TestCase { public function testShouldAllowGetMessageSetInConstructor() { - $message = $this->createMock(PsrMessage::class); + $message = $this->createMock(InteropMessage::class); - $recipient = new Recipient($this->createMock(PsrDestination::class), $message); + $recipient = new Recipient($this->createMock(Destination::class), $message); $this->assertSame($message, $recipient->getMessage()); } public function testShouldAllowGetDestinationSetInConstructor() { - $destination = $this->createMock(PsrDestination::class); + $destination = $this->createMock(Destination::class); - $recipient = new Recipient($destination, $this->createMock(PsrMessage::class)); + $recipient = new Recipient($destination, $this->createMock(InteropMessage::class)); $this->assertSame($destination, $recipient->getDestination()); } diff --git a/Tests/Router/RouteRecipientListProcessorTest.php b/Tests/Router/RouteRecipientListProcessorTest.php index b798043..ae878fc 100644 --- a/Tests/Router/RouteRecipientListProcessorTest.php +++ b/Tests/Router/RouteRecipientListProcessorTest.php @@ -9,9 +9,9 @@ use Enqueue\Router\RecipientListRouterInterface; use Enqueue\Router\RouteRecipientListProcessor; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; -use Interop\Queue\PsrProducer; +use Interop\Queue\Context; +use Interop\Queue\Processor; +use Interop\Queue\Producer as InteropProducer; use PHPUnit\Framework\TestCase; class RouteRecipientListProcessorTest extends TestCase @@ -20,12 +20,7 @@ class RouteRecipientListProcessorTest extends TestCase public function testShouldImplementProcessorInterface() { - $this->assertClassImplements(PsrProcessor::class, RouteRecipientListProcessor::class); - } - - public function testCouldBeConstructedWithRouterAsFirstArgument() - { - new RouteRecipientListProcessor($this->createRecipientListRouterMock()); + $this->assertClassImplements(Processor::class, RouteRecipientListProcessor::class); } public function testShouldProduceRecipientsMessagesAndAckOriginalMessage() @@ -55,7 +50,7 @@ public function testShouldProduceRecipientsMessagesAndAckOriginalMessage() ->with($this->identicalTo($barRecipient->getDestination()), $this->identicalTo($barRecipient->getMessage())) ; - $sessionMock = $this->createPsrContextMock(); + $sessionMock = $this->createContextMock(); $sessionMock ->expects($this->once()) ->method('createProducer') @@ -70,23 +65,23 @@ public function testShouldProduceRecipientsMessagesAndAckOriginalMessage() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProducer + * @return \PHPUnit\Framework\MockObject\MockObject|InteropProducer */ protected function createProducerMock() { - return $this->createMock(PsrProducer::class); + return $this->createMock(InteropProducer::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return \PHPUnit\Framework\MockObject\MockObject|Context */ - protected function createPsrContextMock() + protected function createContextMock() { - return $this->createMock(PsrContext::class); + return $this->createMock(Context::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|RecipientListRouterInterface + * @return \PHPUnit\Framework\MockObject\MockObject|RecipientListRouterInterface */ protected function createRecipientListRouterMock() { diff --git a/Tests/Rpc/PromiseTest.php b/Tests/Rpc/PromiseTest.php index 7202e67..6762149 100644 --- a/Tests/Rpc/PromiseTest.php +++ b/Tests/Rpc/PromiseTest.php @@ -50,10 +50,10 @@ public function testOnReceiveShouldCallReceiveCallBackWithTimeout() $receiveInvoked = false; $receivePromise = null; $receiveTimeout = null; - $receivecb = function ($promise, $timout) use (&$receiveInvoked, &$receivePromise, &$receiveTimeout) { + $receivecb = function ($promise, $timeout) use (&$receiveInvoked, &$receivePromise, &$receiveTimeout) { $receiveInvoked = true; $receivePromise = $promise; - $receiveTimeout = $timout; + $receiveTimeout = $timeout; }; $promise = new Promise($receivecb, function () {}, function () {}); @@ -122,7 +122,7 @@ public function testOnReceiveShouldThrowExceptionIfCallbackReturnNotMessageInsta $promise = new Promise($receivecb, function () {}, function () {}); $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Expected "Interop\Queue\PsrMessage" but got: "stdClass"'); + $this->expectExceptionMessage('Expected "Interop\Queue\Message" but got: "stdClass"'); $promise->receive(); } @@ -136,7 +136,7 @@ public function testOnReceiveNoWaitShouldThrowExceptionIfCallbackReturnNotMessag $promise = new Promise(function () {}, $receivecb, function () {}); $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Expected "Interop\Queue\PsrMessage" but got: "stdClass"'); + $this->expectExceptionMessage('Expected "Interop\Queue\Message" but got: "stdClass"'); $promise->receiveNoWait(); } diff --git a/Tests/Rpc/RpcClientTest.php b/Tests/Rpc/RpcClientTest.php index ce7aa27..db4813c 100644 --- a/Tests/Rpc/RpcClientTest.php +++ b/Tests/Rpc/RpcClientTest.php @@ -7,18 +7,14 @@ use Enqueue\Null\NullQueue; use Enqueue\Rpc\Promise; use Enqueue\Rpc\RpcClient; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProducer; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Producer as InteropProducer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class RpcClientTest extends TestCase { - public function testCouldBeConstructedWithPsrContextAsFirstArgument() - { - new RpcClient($this->createPsrContextMock()); - } - public function testShouldSetReplyToIfNotSet() { $context = new NullContext(); @@ -80,14 +76,14 @@ public function testShouldProduceMessageToQueue() $message->setCorrelationId('theCorrelationId'); $message->setReplyTo('theReplyTo'); - $producer = $this->createPsrProducerMock(); + $producer = $this->createInteropProducerMock(); $producer ->expects($this->once()) ->method('send') ->with($this->identicalTo($queue), $this->identicalTo($message)) ; - $context = $this->createPsrContextMock(); + $context = $this->createContextMock(); $context ->expects($this->once()) ->method('createProducer') @@ -110,7 +106,7 @@ public function testShouldReceiveMessageAndAckMessageIfCorrelationEquals() $receivedMessage = new NullMessage(); $receivedMessage->setCorrelationId('theCorrelationId'); - $consumer = $this->createPsrConsumerMock(); + $consumer = $this->createConsumerMock(); $consumer ->expects($this->once()) ->method('receive') @@ -127,11 +123,11 @@ public function testShouldReceiveMessageAndAckMessageIfCorrelationEquals() ->method('reject') ; - $context = $this->createPsrContextMock(); + $context = $this->createContextMock(); $context ->expects($this->once()) ->method('createProducer') - ->willReturn($this->createPsrProducerMock()) + ->willReturn($this->createInteropProducerMock()) ; $context ->expects($this->atLeastOnce()) @@ -162,7 +158,7 @@ public function testShouldReceiveNoWaitMessageAndAckMessageIfCorrelationEquals() $receivedMessage = new NullMessage(); $receivedMessage->setCorrelationId('theCorrelationId'); - $consumer = $this->createPsrConsumerMock(); + $consumer = $this->createConsumerMock(); $consumer ->expects($this->once()) ->method('receiveNoWait') @@ -178,11 +174,11 @@ public function testShouldReceiveNoWaitMessageAndAckMessageIfCorrelationEquals() ->method('reject') ; - $context = $this->createPsrContextMock(); + $context = $this->createContextMock(); $context ->expects($this->once()) ->method('createProducer') - ->willReturn($this->createPsrProducerMock()) + ->willReturn($this->createInteropProducerMock()) ; $context ->expects($this->atLeastOnce()) @@ -213,14 +209,14 @@ public function testShouldDeleteQueueAfterReceiveIfDeleteReplyQueueIsTrue() $receivedMessage = new NullMessage(); $receivedMessage->setCorrelationId('theCorrelationId'); - $consumer = $this->createPsrConsumerMock(); + $consumer = $this->createConsumerMock(); $consumer ->expects($this->once()) ->method('receive') ->willReturn($receivedMessage) ; - $context = $this->getMockBuilder(PsrContext::class) + $context = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->setMethods(['deleteQueue']) ->getMockForAbstractClass() @@ -229,7 +225,7 @@ public function testShouldDeleteQueueAfterReceiveIfDeleteReplyQueueIsTrue() $context ->expects($this->once()) ->method('createProducer') - ->willReturn($this->createPsrProducerMock()) + ->willReturn($this->createInteropProducerMock()) ; $context ->expects($this->atLeastOnce()) @@ -267,18 +263,18 @@ public function testShouldNotCallDeleteQueueIfDeleteReplyQueueIsTrueButContextHa $receivedMessage = new NullMessage(); $receivedMessage->setCorrelationId('theCorrelationId'); - $consumer = $this->createPsrConsumerMock(); + $consumer = $this->createConsumerMock(); $consumer ->expects($this->once()) ->method('receive') ->willReturn($receivedMessage) ; - $context = $this->createPsrContextMock(); + $context = $this->createContextMock(); $context ->expects($this->once()) ->method('createProducer') - ->willReturn($this->createPsrProducerMock()) + ->willReturn($this->createInteropProducerMock()) ; $context ->expects($this->once()) @@ -329,26 +325,26 @@ public function testShouldDoSyncCall() } /** - * @return PsrContext|\PHPUnit_Framework_MockObject_MockObject|PsrProducer + * @return Context|MockObject|InteropProducer */ - private function createPsrProducerMock() + private function createInteropProducerMock() { - return $this->createMock(PsrProducer::class); + return $this->createMock(InteropProducer::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrConsumer + * @return MockObject|Consumer */ - private function createPsrConsumerMock() + private function createConsumerMock() { - return $this->createMock(PsrConsumer::class); + return $this->createMock(Consumer::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject|Context */ - private function createPsrContextMock() + private function createContextMock() { - return $this->createMock(PsrContext::class); + return $this->createMock(Context::class); } } diff --git a/Tests/Symfony/AmqpTransportFactoryTest.php b/Tests/Symfony/AmqpTransportFactoryTest.php deleted file mode 100644 index b27f5b5..0000000 --- a/Tests/Symfony/AmqpTransportFactoryTest.php +++ /dev/null @@ -1,404 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, AmqpTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new AmqpTransportFactory(); - - $this->assertEquals('amqp', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new AmqpTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testThrowIfCouldBeConstructedWithCustomName() - { - $transport = new AmqpTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'read_timeout' => 3., - 'write_timeout' => 3., - 'connection_timeout' => 3., - 'heartbeat' => 0, - 'persisted' => false, - 'lazy' => true, - 'qos_global' => false, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'receive_method' => 'basic_get', - ]]); - - $this->assertEquals([ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'read_timeout' => 3., - 'write_timeout' => 3., - 'connection_timeout' => 3., - 'heartbeat' => 0, - 'persisted' => false, - 'lazy' => true, - 'qos_global' => false, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'receive_method' => 'basic_get', - ], $config); - } - - public function testShouldAllowAddConfigurationWithDriverOptions() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'host' => 'localhost', - 'driver_options' => [ - 'foo' => 'fooVal', - ], - ]]); - - $this->assertEquals([ - 'host' => 'localhost', - 'driver_options' => [ - 'foo' => 'fooVal', - ], - ], $config); - } - - public function testShouldAllowAddSslOptions() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'ssl_on' => true, - 'ssl_verify' => false, - 'ssl_cacert' => '/path/to/cacert.pem', - 'ssl_cert' => '/path/to/cert.pem', - 'ssl_key' => '/path/to/key.pem', - ]]); - - $this->assertEquals([ - 'ssl_on' => true, - 'ssl_verify' => false, - 'ssl_cacert' => '/path/to/cacert.pem', - 'ssl_cert' => '/path/to/cert.pem', - 'ssl_key' => '/path/to/key.pem', - ], $config); - } - - public function testThrowIfNotSupportedDriverSet() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('Invalid configuration for path "foo.driver": Unexpected driver given "invalidDriver"'); - $processor->process($tb->buildTree(), [[ - 'driver' => 'invalidDriver', - ]]); - } - - public function testShouldAllowSetDriver() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'driver' => 'ext', - ]]); - - $this->assertEquals([ - 'driver' => 'ext', - ], $config); - } - - public function testShouldAllowAddConfigurationAsString() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['amqpDSN']); - - $this->assertEquals([ - 'dsn' => 'amqpDSN', - ], $config); - } - - public function testThrowIfInvalidReceiveMethodIsSet() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The value "anInvalidMethod" is not allowed for path "foo.receive_method". Permissible values: "basic_get", "basic_consume"'); - $processor->process($tb->buildTree(), [[ - 'receive_method' => 'anInvalidMethod', - ]]); - } - - public function testShouldAllowChangeReceiveMethod() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'receive_method' => 'basic_consume', - ]]); - - $this->assertEquals([ - 'receive_method' => 'basic_consume', - ], $config); - } - - public function testShouldCreateConnectionFactoryForEmptyConfig() - { - $container = new ContainerBuilder(); - - $transport = new AmqpTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, []); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - - $this->assertSame([[]], $factory->getArguments()); - } - - public function testShouldCreateConnectionFactoryFromDsnString() - { - $container = new ContainerBuilder(); - - $transport = new AmqpTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'dsn' => 'theConnectionDSN:', - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame([['dsn' => 'theConnectionDSN:']], $factory->getArguments()); - } - - public function testShouldCreateConnectionFactoryAndMergeDriverOptionsIfSet() - { - $container = new ContainerBuilder(); - - $transport = new AmqpTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'host' => 'aHost', - 'driver_options' => [ - 'foo' => 'fooVal', - ], - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame([['foo' => 'fooVal', 'host' => 'aHost']], $factory->getArguments()); - } - - public function testShouldCreateConnectionFactoryFromDsnStringPlushArrayOptions() - { - $container = new ContainerBuilder(); - - $transport = new AmqpTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - ]], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new AmqpTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - ]); - - $this->assertEquals('enqueue.transport.amqp.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.amqp.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.amqp.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new AmqpTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.amqp.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(AmqpDriver::class, $driver->getClass()); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(0)); - $this->assertEquals('enqueue.transport.amqp.context', (string) $driver->getArgument(0)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(1)); - $this->assertEquals('enqueue.client.config', (string) $driver->getArgument(1)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(2)); - $this->assertEquals('enqueue.client.meta.queue_meta_registry', (string) $driver->getArgument(2)); - } - - public function testShouldCreateAmqpExtConnectionFactoryBySetDriver() - { - $factory = AmqpTransportFactory::createConnectionFactoryFactory(['driver' => 'ext']); - - $this->assertInstanceOf(\Enqueue\AmqpExt\AmqpConnectionFactory::class, $factory); - } - - public function testShouldCreateAmqpLibConnectionFactoryBySetDriver() - { - $factory = AmqpTransportFactory::createConnectionFactoryFactory(['driver' => 'lib']); - - $this->assertInstanceOf(\Enqueue\AmqpLib\AmqpConnectionFactory::class, $factory); - } - - public function testShouldCreateAmqpBunnyConnectionFactoryBySetDriver() - { - $factory = AmqpTransportFactory::createConnectionFactoryFactory(['driver' => 'bunny']); - - $this->assertInstanceOf(\Enqueue\AmqpBunny\AmqpConnectionFactory::class, $factory); - } - - public function testShouldCreateAmqpExtFromConfigWithoutDriverAndDsn() - { - $factory = AmqpTransportFactory::createConnectionFactoryFactory(['host' => 'aHost']); - - $this->assertInstanceOf(\Enqueue\AmqpExt\AmqpConnectionFactory::class, $factory); - } - - public function testThrowIfInvalidDriverGiven() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Unexpected driver given "invalidDriver"'); - - AmqpTransportFactory::createConnectionFactoryFactory(['driver' => 'invalidDriver']); - } - - public function testShouldCreateAmqpExtFromDsn() - { - $factory = AmqpTransportFactory::createConnectionFactoryFactory(['dsn' => 'amqp:']); - - $this->assertInstanceOf(\Enqueue\AmqpExt\AmqpConnectionFactory::class, $factory); - } - - public function testShouldCreateAmqpBunnyFromDsnWithDriver() - { - $factory = AmqpTransportFactory::createConnectionFactoryFactory(['dsn' => 'amqp+bunny:']); - - $this->assertInstanceOf(\Enqueue\AmqpBunny\AmqpConnectionFactory::class, $factory); - } - - public function testThrowIfNotAmqpDsnProvided() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Factory must be instance of "Interop\Amqp\AmqpConnectionFactory" but got "Enqueue\Sqs\SqsConnectionFactory"'); - - AmqpTransportFactory::createConnectionFactoryFactory(['dsn' => 'sqs:']); - } -} diff --git a/Tests/Symfony/Client/ConsumeCommandTest.php b/Tests/Symfony/Client/ConsumeCommandTest.php new file mode 100644 index 0000000..3758ca9 --- /dev/null +++ b/Tests/Symfony/Client/ConsumeCommandTest.php @@ -0,0 +1,703 @@ +assertClassExtends(Command::class, ConsumeCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(ConsumeCommand::class); + } + + public function testShouldHaveAsCommandAttributeWithCommandName() + { + $commandClass = ConsumeCommand::class; + + $reflectionClass = new \ReflectionClass($commandClass); + + $attributes = $reflectionClass->getAttributes(AsCommand::class); + + $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.'); + + // Get the first attribute instance (assuming there is only one AsCommand attribute) + $asCommandAttribute = $attributes[0]; + + // Verify the 'name' parameter value + $attributeInstance = $asCommandAttribute->newInstance(); + $this->assertEquals('enqueue:consume', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.'); + } + + public function testShouldHaveExpectedOptions() + { + $command = new ConsumeCommand($this->createMock(ContainerInterface::class), 'default'); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(9, $options); + $this->assertArrayHasKey('memory-limit', $options); + $this->assertArrayHasKey('message-limit', $options); + $this->assertArrayHasKey('time-limit', $options); + $this->assertArrayHasKey('receive-timeout', $options); + $this->assertArrayHasKey('niceness', $options); + $this->assertArrayHasKey('client', $options); + $this->assertArrayHasKey('logger', $options); + $this->assertArrayHasKey('skip', $options); + $this->assertArrayHasKey('setup-broker', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new ConsumeCommand($this->createMock(ContainerInterface::class), 'default'); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(1, $arguments); + $this->assertArrayHasKey('client-queue-names', $arguments); + } + + public function testShouldBindDefaultQueueOnly() + { + $queue = new NullQueue(''); + + $routeCollection = new RouteCollection([]); + + $processor = $this->createDelegateProcessorMock(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('bind') + ->with($this->identicalTo($queue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->once()) + ->method('createQueue') + ->with('default', true) + ->willReturn($queue) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + public function testShouldUseRequestedClient() + { + $defaultProcessor = $this->createDelegateProcessorMock(); + + $defaultConsumer = $this->createQueueConsumerMock(); + $defaultConsumer + ->expects($this->never()) + ->method('bind') + ; + $defaultConsumer + ->expects($this->never()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $defaultDriver = $this->createDriverStub(new RouteCollection([])); + $defaultDriver + ->expects($this->never()) + ->method('createQueue') + ; + + $queue = new NullQueue(''); + + $routeCollection = new RouteCollection([]); + + $fooProcessor = $this->createDelegateProcessorMock(); + + $fooConsumer = $this->createQueueConsumerMock(); + $fooConsumer + ->expects($this->once()) + ->method('bind') + ->with($this->identicalTo($queue), $this->identicalTo($fooProcessor)) + ; + $fooConsumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $fooDriver = $this->createDriverStub($routeCollection); + $fooDriver + ->expects($this->once()) + ->method('createQueue') + ->with('default', true) + ->willReturn($queue) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $defaultConsumer, + 'enqueue.client.default.driver' => $defaultDriver, + 'enqueue.client.default.delegate_processor' => $defaultProcessor, + 'enqueue.client.foo.queue_consumer' => $fooConsumer, + 'enqueue.client.foo.driver' => $fooDriver, + 'enqueue.client.foo.delegate_processor' => $fooProcessor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + '--client' => 'foo', + ]); + } + + public function testThrowIfNotDefinedClientRequested() + { + $routeCollection = new RouteCollection([]); + + $processor = $this->createDelegateProcessorMock(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->never()) + ->method('bind') + ; + $consumer + ->expects($this->never()) + ->method('consume') + ; + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->never()) + ->method('createQueue') + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Client "not-defined" is not supported.'); + $tester->execute([ + '--client' => 'not-defined', + ]); + } + + public function testShouldBindDefaultQueueIfRouteUseDifferentQueue() + { + $queue = new NullQueue(''); + + $routeCollection = new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]); + + $processor = $this->createDelegateProcessorMock(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('bind') + ->with($this->identicalTo($queue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->once()) + ->method('createQueue') + ->with('default', true) + ->willReturn($queue) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + public function testShouldBindCustomExecuteConsumptionAndUseCustomClientDestinationName() + { + $defaultQueue = new NullQueue(''); + $customQueue = new NullQueue(''); + + $routeCollection = new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor', ['queue' => 'custom']), + ]); + + $processor = $this->createDelegateProcessorMock(); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->at(3)) + ->method('createQueue') + ->with('default', true) + ->willReturn($defaultQueue) + ; + $driver + ->expects($this->at(4)) + ->method('createQueue') + ->with('custom', true) + ->willReturn($customQueue) + ; + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->at(0)) + ->method('bind') + ->with($this->identicalTo($defaultQueue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(1)) + ->method('bind') + ->with($this->identicalTo($customQueue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(2)) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + public function testShouldBindUserProvidedQueues() + { + $queue = new NullQueue(''); + + $routeCollection = new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor', ['queue' => 'custom']), + new Route('topic', Route::TOPIC, 'processor', ['queue' => 'non-default-queue']), + ]); + + $processor = $this->createDelegateProcessorMock(); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->once()) + ->method('createQueue') + ->with('non-default-queue', true) + ->willReturn($queue) + ; + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('bind') + ->with($this->identicalTo($queue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'client-queue-names' => ['non-default-queue'], + ]); + } + + public function testShouldBindNotPrefixedQueue() + { + $queue = new NullQueue(''); + + $routeCollection = new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor', ['queue' => 'non-prefixed-queue', 'prefix_queue' => false]), + ]); + + $processor = $this->createDelegateProcessorMock(); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->once()) + ->method('createQueue') + ->with('non-prefixed-queue', false) + ->willReturn($queue) + ; + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('bind') + ->with($this->identicalTo($queue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'client-queue-names' => ['non-prefixed-queue'], + ]); + } + + public function testShouldBindQueuesOnlyOnce() + { + $defaultQueue = new NullQueue(''); + $customQueue = new NullQueue(''); + + $routeCollection = new RouteCollection([ + new Route('fooTopic', Route::TOPIC, 'processor', ['queue' => 'custom']), + new Route('barTopic', Route::TOPIC, 'processor', ['queue' => 'custom']), + new Route('ololoTopic', Route::TOPIC, 'processor', []), + ]); + + $processor = $this->createDelegateProcessorMock(); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->at(3)) + ->method('createQueue') + ->with('default', true) + ->willReturn($defaultQueue) + ; + $driver + ->expects($this->at(4)) + ->method('createQueue', true) + ->with('custom') + ->willReturn($customQueue) + ; + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->at(0)) + ->method('bind') + ->with($this->identicalTo($defaultQueue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(1)) + ->method('bind') + ->with($this->identicalTo($customQueue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(2)) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + public function testShouldNotBindExternalRoutes() + { + $defaultQueue = new NullQueue(''); + + $routeCollection = new RouteCollection([ + new Route('barTopic', Route::TOPIC, 'processor', ['queue' => null]), + new Route('fooTopic', Route::TOPIC, 'processor', ['queue' => 'external_queue', 'external' => true]), + ]); + + $processor = $this->createDelegateProcessorMock(); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->exactly(1)) + ->method('createQueue') + ->with('default', true) + ->willReturn($defaultQueue) + ; + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->exactly(1)) + ->method('bind') + ->with($this->identicalTo($defaultQueue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(1)) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + public function testShouldSkipQueueConsumptionAndUseCustomClientDestinationName() + { + $queue = new NullQueue(''); + + $processor = $this->createDelegateProcessorMock(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->exactly(3)) + ->method('bind') + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $routeCollection = new RouteCollection([ + new Route('fooTopic', Route::TOPIC, 'processor', ['queue' => 'fooQueue']), + new Route('barTopic', Route::TOPIC, 'processor', ['queue' => 'barQueue']), + new Route('ololoTopic', Route::TOPIC, 'processor', ['queue' => 'ololoQueue']), + ]); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->at(3)) + ->method('createQueue', true) + ->with('default') + ->willReturn($queue) + ; + $driver + ->expects($this->at(4)) + ->method('createQueue', true) + ->with('fooQueue') + ->willReturn($queue) + ; + $driver + ->expects($this->at(5)) + ->method('createQueue', true) + ->with('ololoQueue') + ->willReturn($queue) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + '--skip' => ['barQueue'], + ]); + } + + public function testShouldReturnExitStatusIfSet() + { + $testExitCode = 678; + + $stubExtension = $this->createExtension(); + + $stubExtension + ->expects($this->once()) + ->method('onStart') + ->with($this->isInstanceOf(Start::class)) + ->willReturnCallback(function (Start $context) use ($testExitCode) { + $context->interruptExecution($testExitCode); + }) + ; + + $defaultQueue = new NullQueue('default'); + + $routeCollection = new RouteCollection([]); + + $processor = $this->createDelegateProcessorMock(); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->exactly(1)) + ->method('createQueue') + ->with('default', true) + ->willReturn($defaultQueue) + ; + + $consumer = new QueueConsumer($this->createContextStub(), $stubExtension); + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertEquals($testExitCode, $tester->getStatusCode()); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DelegateProcessor + */ + private function createDelegateProcessorMock() + { + return $this->createMock(DelegateProcessor::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface + */ + private function createQueueConsumerMock() + { + return $this->createMock(QueueConsumerInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface + */ + private function createDriverStub(?RouteCollection $routeCollection = null): DriverInterface + { + $driverMock = $this->createMock(DriverInterface::class); + $driverMock + ->expects($this->any()) + ->method('getRouteCollection') + ->willReturn($routeCollection ?? new RouteCollection([])) + ; + + $driverMock + ->expects($this->any()) + ->method('getConfig') + ->willReturn(Config::create('aPrefix', 'anApp')) + ; + + return $driverMock; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createContextWithoutSubscriptionConsumerMock(): InteropContext + { + $contextMock = $this->createMock(InteropContext::class); + $contextMock + ->expects($this->any()) + ->method('createSubscriptionConsumer') + ->willThrowException(SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt()) + ; + + return $contextMock; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|InteropContext + */ + private function createContextStub(?Consumer $consumer = null): InteropContext + { + $context = $this->createContextWithoutSubscriptionConsumerMock(); + $context + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $queueName) { + return new NullQueue($queueName); + }) + ; + $context + ->expects($this->any()) + ->method('createConsumer') + ->willReturnCallback(function (Queue $queue) use ($consumer) { + return $consumer ?: $this->createConsumerStub($queue); + }) + ; + + return $context; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|ExtensionInterface + */ + private function createExtension() + { + return $this->createMock(ExtensionInterface::class); + } + + /** + * @param mixed|null $queue + * + * @return \PHPUnit\Framework\MockObject\MockObject|Consumer + */ + private function createConsumerStub($queue = null): Consumer + { + if (null === $queue) { + $queue = 'queue'; + } + if (is_string($queue)) { + $queue = new NullQueue($queue); + } + + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue) + ; + + return $consumerMock; + } +} diff --git a/Tests/Symfony/Client/ConsumeMessagesCommandTest.php b/Tests/Symfony/Client/ConsumeMessagesCommandTest.php deleted file mode 100644 index e87f4df..0000000 --- a/Tests/Symfony/Client/ConsumeMessagesCommandTest.php +++ /dev/null @@ -1,282 +0,0 @@ -createQueueConsumerMock(), - $this->createDelegateProcessorMock(), - $this->createQueueMetaRegistry([]), - $this->createDriverMock() - ); - } - - public function testShouldHaveCommandName() - { - $command = new ConsumeMessagesCommand( - $this->createQueueConsumerMock(), - $this->createDelegateProcessorMock(), - $this->createQueueMetaRegistry([]), - $this->createDriverMock() - ); - - $this->assertEquals('enqueue:consume', $command->getName()); - } - - public function testShouldHaveCommandAliases() - { - $command = new ConsumeMessagesCommand( - $this->createQueueConsumerMock(), - $this->createDelegateProcessorMock(), - $this->createQueueMetaRegistry([]), - $this->createDriverMock() - ); - - $this->assertEquals(['enq:c'], $command->getAliases()); - } - - public function testShouldHaveExpectedOptions() - { - $command = new ConsumeMessagesCommand( - $this->createQueueConsumerMock(), - $this->createDelegateProcessorMock(), - $this->createQueueMetaRegistry([]), - $this->createDriverMock() - ); - - $options = $command->getDefinition()->getOptions(); - - $this->assertCount(7, $options); - $this->assertArrayHasKey('memory-limit', $options); - $this->assertArrayHasKey('message-limit', $options); - $this->assertArrayHasKey('time-limit', $options); - $this->assertArrayHasKey('setup-broker', $options); - $this->assertArrayHasKey('idle-timeout', $options); - $this->assertArrayHasKey('receive-timeout', $options); - $this->assertArrayHasKey('skip', $options); - } - - public function testShouldHaveExpectedArguments() - { - $command = new ConsumeMessagesCommand( - $this->createQueueConsumerMock(), - $this->createDelegateProcessorMock(), - $this->createQueueMetaRegistry([]), - $this->createDriverMock() - ); - - $arguments = $command->getDefinition()->getArguments(); - - $this->assertCount(1, $arguments); - $this->assertArrayHasKey('client-queue-names', $arguments); - } - - public function testShouldExecuteConsumptionAndUseDefaultQueueName() - { - $queue = new NullQueue(''); - - $processor = $this->createDelegateProcessorMock(); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->never()) - ->method('close') - ; - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->once()) - ->method('bind') - ->with($this->identicalTo($queue), $this->identicalTo($processor)) - ; - $consumer - ->expects($this->once()) - ->method('consume') - ->with($this->isInstanceOf(ChainExtension::class)) - ; - - $queueMetaRegistry = $this->createQueueMetaRegistry([ - 'default' => [], - ]); - - $driver = $this->createDriverMock(); - $driver - ->expects($this->once()) - ->method('createQueue') - ->with('default') - ->willReturn($queue) - ; - - $command = new ConsumeMessagesCommand($consumer, $processor, $queueMetaRegistry, $driver); - - $tester = new CommandTester($command); - $tester->execute([]); - } - - public function testShouldExecuteConsumptionAndUseCustomClientDestinationName() - { - $queue = new NullQueue(''); - - $processor = $this->createDelegateProcessorMock(); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->never()) - ->method('close') - ; - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->once()) - ->method('bind') - ->with($this->identicalTo($queue), $this->identicalTo($processor)) - ; - $consumer - ->expects($this->once()) - ->method('consume') - ->with($this->isInstanceOf(ChainExtension::class)) - ; - - $queueMetaRegistry = $this->createQueueMetaRegistry([ - 'non-default-queue' => [], - ]); - - $driver = $this->createDriverMock(); - $driver - ->expects($this->once()) - ->method('createQueue') - ->with('non-default-queue') - ->willReturn($queue) - ; - - $command = new ConsumeMessagesCommand($consumer, $processor, $queueMetaRegistry, $driver); - - $tester = new CommandTester($command); - $tester->execute([ - 'client-queue-names' => ['non-default-queue'], - ]); - } - - public function testShouldSkipQueueConsumptionAndUseCustomClientDestinationName() - { - $queue = new NullQueue(''); - - $processor = $this->createDelegateProcessorMock(); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->never()) - ->method('close') - ; - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->exactly(2)) - ->method('bind') - ; - $consumer - ->expects($this->once()) - ->method('consume') - ->with($this->isInstanceOf(ChainExtension::class)) - ; - - $queueMetaRegistry = $this->createQueueMetaRegistry([ - 'fooQueue' => [ - 'transportName' => 'fooTransportQueue', - ], - 'barQueue' => [ - 'transportName' => 'barTransportQueue', - ], - 'ololoQueue' => [ - 'transportName' => 'ololoTransportQueue', - ], - ]); - - $driver = $this->createDriverMock(); - $driver - ->expects($this->at(0)) - ->method('createQueue') - ->with('fooQueue') - ->willReturn($queue) - ; - $driver - ->expects($this->at(1)) - ->method('createQueue') - ->with('ololoQueue') - ->willReturn($queue) - ; - - $command = new ConsumeMessagesCommand($consumer, $processor, $queueMetaRegistry, $driver); - - $tester = new CommandTester($command); - $tester->execute([ - '--skip' => ['barQueue'], - ]); - } - - /** - * @param array $destinationNames - * - * @return QueueMetaRegistry - */ - private function createQueueMetaRegistry(array $destinationNames) - { - $config = new Config( - 'aPrefix', - 'anApp', - 'aRouterTopicName', - 'aRouterQueueName', - 'aDefaultQueueName', - 'aRouterProcessorName' - ); - - return new QueueMetaRegistry($config, $destinationNames); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext - */ - private function createPsrContextMock() - { - return $this->createMock(PsrContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|DelegateProcessor - */ - private function createDelegateProcessorMock() - { - return $this->createMock(DelegateProcessor::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|QueueConsumer - */ - private function createQueueConsumerMock() - { - return $this->createMock(QueueConsumer::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface - */ - private function createDriverMock() - { - return $this->createMock(DriverInterface::class); - } -} diff --git a/Tests/Symfony/Client/ContainerAwareProcessorRegistryTest.php b/Tests/Symfony/Client/ContainerAwareProcessorRegistryTest.php deleted file mode 100644 index 1d641d4..0000000 --- a/Tests/Symfony/Client/ContainerAwareProcessorRegistryTest.php +++ /dev/null @@ -1,84 +0,0 @@ -assertClassImplements(ProcessorRegistryInterface::class, ContainerAwareProcessorRegistry::class); - } - - public function testCouldBeConstructedWithoutAnyArgument() - { - new ContainerAwareProcessorRegistry(); - } - - public function testShouldThrowExceptionIfProcessorIsNotSet() - { - $this->setExpectedException( - \LogicException::class, - 'Processor was not found. processorName: "processor-name"' - ); - - $registry = new ContainerAwareProcessorRegistry(); - $registry->get('processor-name'); - } - - public function testShouldThrowExceptionIfContainerIsNotSet() - { - $this->setExpectedException(\LogicException::class, 'Container was not set'); - - $registry = new ContainerAwareProcessorRegistry(); - $registry->set('processor-name', 'processor-id'); - - $registry->get('processor-name'); - } - - public function testShouldThrowExceptionIfInstanceOfProcessorIsInvalid() - { - $this->setExpectedException(\LogicException::class, 'Container was not set'); - - $processor = new \stdClass(); - - $container = new Container(); - $container->set('processor-id', $processor); - - $registry = new ContainerAwareProcessorRegistry(); - $registry->set('processor-name', 'processor-id'); - - $registry->get('processor-name'); - } - - public function testShouldReturnInstanceOfProcessor() - { - $this->setExpectedException(\LogicException::class, 'Container was not set'); - - $processor = $this->createProcessorMock(); - - $container = new Container(); - $container->set('processor-id', $processor); - - $registry = new ContainerAwareProcessorRegistry(); - $registry->set('processor-name', 'processor-id'); - - $this->assertSame($processor, $registry->get('processor-name')); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor - */ - protected function createProcessorMock() - { - return $this->createMock(PsrProcessor::class); - } -} diff --git a/Tests/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPassTest.php b/Tests/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPassTest.php new file mode 100644 index 0000000..568de64 --- /dev/null +++ b/Tests/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPassTest.php @@ -0,0 +1,167 @@ +assertClassImplements(CompilerPassInterface::class, AnalyzeRouteCollectionPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(AnalyzeRouteCollectionPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new AnalyzeRouteCollectionPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + + $pass = new AnalyzeRouteCollectionPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found'); + $pass->process($container); + } + + public function testThrowIfExclusiveCommandProcessorOnDefaultQueue() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route( + 'aCommand', + Route::COMMAND, + 'aBarProcessor', + ['exclusive' => true] + ))->toArray(), + ]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The command "aCommand" processor "aBarProcessor" is exclusive but queue is not specified. Exclusive processors could not be run on a default queue.'); + $pass = new AnalyzeRouteCollectionPass(); + + $pass->process($container); + } + + public function testThrowIfTwoExclusiveCommandProcessorsWorkOnSamePrefixedQueue() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route( + 'aFooCommand', + Route::COMMAND, + 'aFooProcessor', + ['exclusive' => true, 'queue' => 'aQueue', 'prefix_queue' => true] + ))->toArray(), + + (new Route( + 'aBarCommand', + Route::COMMAND, + 'aBarProcessor', + ['exclusive' => true, 'queue' => 'aQueue', 'prefix_queue' => true] + ))->toArray(), + ]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The command "aBarCommand" processor "aBarProcessor" is exclusive. The queue "aQueue" already has another exclusive command processor "aFooProcessor" bound to it.'); + $pass = new AnalyzeRouteCollectionPass(); + + $pass->process($container); + } + + public function testThrowIfTwoExclusiveCommandProcessorsWorkOnSameQueue() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route( + 'aFooCommand', + Route::COMMAND, + 'aFooProcessor', + ['exclusive' => true, 'queue' => 'aQueue', 'prefix_queue' => false] + ))->toArray(), + + (new Route( + 'aBarCommand', + Route::COMMAND, + 'aBarProcessor', + ['exclusive' => true, 'queue' => 'aQueue', 'prefix_queue' => false] + ))->toArray(), + ]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The command "aBarCommand" processor "aBarProcessor" is exclusive. The queue "aQueue" already has another exclusive command processor "aFooProcessor" bound to it.'); + $pass = new AnalyzeRouteCollectionPass(); + + $pass->process($container); + } + + public function testThrowIfThereAreTwoQueuesWithSameNameAndOneNotPrefixed() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route( + 'aFooCommand', + Route::COMMAND, + 'aFooProcessor', + ['queue' => 'foo', 'prefix_queue' => false] + ))->toArray(), + + (new Route( + 'aBarCommand', + Route::COMMAND, + 'aBarProcessor', + ['queue' => 'foo', 'prefix_queue' => true] + ))->toArray(), + ]); + + $pass = new AnalyzeRouteCollectionPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('There are prefixed and not prefixed queue with the same name "foo". This is not allowed.'); + $pass->process($container); + } + + public function testThrowIfDefaultQueueNotPrefixed() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route( + 'aFooCommand', + Route::COMMAND, + 'aFooProcessor', + ['queue' => null, 'prefix_queue' => false] + ))->toArray(), + ]); + + $pass = new AnalyzeRouteCollectionPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The default queue must be prefixed.'); + $pass->process($container); + } +} diff --git a/Tests/Symfony/Client/DependencyInjection/BuildClientExtensionsPassTest.php b/Tests/Symfony/Client/DependencyInjection/BuildClientExtensionsPassTest.php new file mode 100644 index 0000000..7537903 --- /dev/null +++ b/Tests/Symfony/Client/DependencyInjection/BuildClientExtensionsPassTest.php @@ -0,0 +1,283 @@ +assertClassImplements(CompilerPassInterface::class, BuildClientExtensionsPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildClientExtensionsPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new BuildClientExtensionsPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoClientExtensionsServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'foo'); + + $pass = new BuildClientExtensionsPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.client_extensions" not found'); + $pass->process($container); + } + + public function testShouldRegisterClientExtension() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.aName.client_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'aName']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'aName']) + ; + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + new Reference('aBarExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldIgnoreOtherClientExtensions() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.aName.client_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'aName']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'anotherName']) + ; + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldAddExtensionIfClientAll() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.aName.client_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'all']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'anotherName']) + ; + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldTreatTagsWithoutClientAsDefaultClient() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.client_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension') + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension') + ; + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + new Reference('aBarExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldOrderExtensionsByPriority() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + + $extensions = new Definition(); + $extensions->addArgument([]); + $container->setDefinition('enqueue.client.foo.client_extensions', $extensions); + + $extension = new Definition(); + $extension->addTag('enqueue.client_extension', ['priority' => 6]); + $container->setDefinition('foo_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.client_extension', ['priority' => -5]); + $container->setDefinition('bar_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.client_extension', ['priority' => 2]); + $container->setDefinition('baz_extension', $extension); + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + $orderedExtensions = $extensions->getArgument(0); + + $this->assertCount(3, $orderedExtensions); + $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[0]); + $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[1]); + $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[2]); + } + + public function testShouldAssumePriorityZeroIfPriorityIsNotSet() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + + $extensions = new Definition(); + $extensions->addArgument([]); + $container->setDefinition('enqueue.client.foo.client_extensions', $extensions); + + $extension = new Definition(); + $extension->addTag('enqueue.client_extension'); + $container->setDefinition('foo_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.client_extension', ['priority' => 1]); + $container->setDefinition('bar_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.client_extension', ['priority' => -1]); + $container->setDefinition('baz_extension', $extension); + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + $orderedExtensions = $extensions->getArgument(0); + + $this->assertCount(3, $orderedExtensions); + $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[0]); + $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[1]); + $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[2]); + } + + public function testShouldMergeWithAddedPreviously() + { + $extensions = new Definition(); + $extensions->addArgument([ + 'aBarExtension' => 'aBarServiceIdAddedPreviously', + 'aOloloExtension' => 'aOloloServiceIdAddedPreviously', + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.client_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension') + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension') + ; + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertCount(4, $extensions->getArgument(0)); + } + + public function testShouldRegisterProcessorWithMatchedNameToCorrespondingExtensions() + { + $fooExtensions = new Definition(); + $fooExtensions->addArgument([]); + + $barExtensions = new Definition(); + $barExtensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.client_extensions', $fooExtensions); + $container->setDefinition('enqueue.client.bar.client_extensions', $barExtensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'bar']) + ; + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + self::assertIsArray($fooExtensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $fooExtensions->getArgument(0)); + + self::assertIsArray($barExtensions->getArgument(0)); + $this->assertEquals([ + new Reference('aBarExtension'), + ], $barExtensions->getArgument(0)); + } +} diff --git a/Tests/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPassTest.php b/Tests/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPassTest.php new file mode 100644 index 0000000..e1ed297 --- /dev/null +++ b/Tests/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPassTest.php @@ -0,0 +1,459 @@ +assertClassImplements(CompilerPassInterface::class, BuildCommandSubscriberRoutesPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildCommandSubscriberRoutesPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new BuildCommandSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'baz'); + + $pass = new BuildCommandSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found'); + $pass->process($container); + } + + public function testThrowIfTaggedProcessorIsBuiltByFactory() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->register('enqueue.client.aName.route_collection', RouteCollection::class) + ->addArgument([]) + ; + $container->register('aProcessor', Processor::class) + ->setFactory('foo') + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The command subscriber tag could not be applied to a service created by factory.'); + $pass->process($container); + } + + public function testShouldRegisterProcessorWithMatchedName() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', get_class($this->createCommandSubscriberProcessor())) + ->addTag('enqueue.command_subscriber', ['client' => 'foo']) + ; + $container->register('aProcessor', get_class($this->createCommandSubscriberProcessor())) + ->addTag('enqueue.command_subscriber', ['client' => 'bar']) + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorWithoutNameToDefaultClient() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', get_class($this->createCommandSubscriberProcessor())) + ->addTag('enqueue.command_subscriber') + ; + $container->register('aProcessor', get_class($this->createCommandSubscriberProcessor())) + ->addTag('enqueue.command_subscriber', ['client' => 'bar']) + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorIfClientNameEqualsAll() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', get_class($this->createCommandSubscriberProcessor())) + ->addTag('enqueue.command_subscriber', ['client' => 'all']) + ; + $container->register('aProcessor', get_class($this->createCommandSubscriberProcessor())) + ->addTag('enqueue.command_subscriber', ['client' => 'bar']) + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorIfCommandsIsString() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor('fooCommand'); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testThrowIfCommandSubscriberReturnsNothing() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor(null); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Command subscriber must return something.'); + $pass->process($container); + } + + public function testShouldRegisterProcessorIfCommandsAreStrings() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor(['fooCommand', 'barCommand']); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(2, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + [ + 'source' => 'barCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegisterProcessorIfParamSingleCommandArray() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor([ + 'command' => 'fooCommand', + 'processor' => 'aCustomFooProcessorName', + 'anOption' => 'aFooVal', + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aCustomFooProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aFooVal', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegisterProcessorIfCommandsAreParamArrays() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor([ + ['command' => 'fooCommand', 'processor' => 'aCustomFooProcessorName', 'anOption' => 'aFooVal'], + ['command' => 'barCommand', 'processor' => 'aCustomBarProcessorName', 'anOption' => 'aBarVal'], + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(2, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aCustomFooProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aFooVal', + ], + [ + 'source' => 'barCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aCustomBarProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aBarVal', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testThrowIfCommandSubscriberParamsInvalid() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor(['fooBar', true]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Command subscriber configuration is invalid'); + $pass->process($container); + } + + public function testShouldMergeExtractedRoutesWithAlreadySetInCollection() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([ + (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(), + (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(), + ]); + + $processor = $this->createCommandSubscriberProcessor(['fooCommand']); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(3, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'aCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aProcessor', + ], + [ + 'source' => 'aCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aProcessor', + ], + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegister08CommandProcessor() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor([ + 'processorName' => 'fooCommand', + 'queueName' => 'a_client_queue_name', + 'queueNameHardcoded' => true, + 'exclusive' => true, + 'anOption' => 'aFooVal', + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['default']); + $container->setParameter('enqueue.default_client', 'default'); + $container->setDefinition('enqueue.client.default.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aFooVal', + 'queue' => 'a_client_queue_name', + 'prefix_queue' => false, + ], + ], + $routeCollection->getArgument(0) + ); + } + + private function createCommandSubscriberProcessor($commandSubscriberReturns = ['aCommand']) + { + $processor = new class implements Processor, CommandSubscriberInterface { + public static $return; + + public function process(InteropMessage $message, Context $context) + { + return self::ACK; + } + + public static function getSubscribedCommand() + { + return static::$return; + } + }; + + $processor::$return = $commandSubscriberReturns; + + return $processor; + } +} diff --git a/Tests/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPassTest.php b/Tests/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPassTest.php new file mode 100644 index 0000000..c297505 --- /dev/null +++ b/Tests/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPassTest.php @@ -0,0 +1,283 @@ +assertClassImplements(CompilerPassInterface::class, BuildConsumptionExtensionsPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildConsumptionExtensionsPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new BuildConsumptionExtensionsPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoConsumptionExtensionsServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'baz'); + + $pass = new BuildConsumptionExtensionsPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.consumption_extensions" not found'); + $pass->process($container); + } + + public function testShouldRegisterClientExtension() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'foo']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + new Reference('aBarExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldIgnoreOtherClientExtensions() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'anotherName']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldAddExtensionIfClientAll() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'all']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'anotherName']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldTreatTagsWithoutClientAsDefaultClient() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension') + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension') + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + new Reference('aBarExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldOrderExtensionsByPriority() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + + $extensions = new Definition(); + $extensions->addArgument([]); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $extension = new Definition(); + $extension->addTag('enqueue.consumption_extension', ['priority' => 6]); + $container->setDefinition('foo_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.consumption_extension', ['priority' => -5]); + $container->setDefinition('bar_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.consumption_extension', ['priority' => 2]); + $container->setDefinition('baz_extension', $extension); + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + $orderedExtensions = $extensions->getArgument(0); + + $this->assertCount(3, $orderedExtensions); + $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[0]); + $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[1]); + $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[2]); + } + + public function testShouldAssumePriorityZeroIfPriorityIsNotSet() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + + $extensions = new Definition(); + $extensions->addArgument([]); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $extension = new Definition(); + $extension->addTag('enqueue.consumption_extension'); + $container->setDefinition('foo_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.consumption_extension', ['priority' => 1]); + $container->setDefinition('bar_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.consumption_extension', ['priority' => -1]); + $container->setDefinition('baz_extension', $extension); + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + $orderedExtensions = $extensions->getArgument(0); + + $this->assertCount(3, $orderedExtensions); + $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[0]); + $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[1]); + $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[2]); + } + + public function testShouldMergeWithAddedPreviously() + { + $extensions = new Definition(); + $extensions->addArgument([ + 'aBarExtension' => 'aBarServiceIdAddedPreviously', + 'aOloloExtension' => 'aOloloServiceIdAddedPreviously', + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension') + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension') + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertCount(4, $extensions->getArgument(0)); + } + + public function testShouldRegisterProcessorWithMatchedNameToCorrespondingExtensions() + { + $fooExtensions = new Definition(); + $fooExtensions->addArgument([]); + + $barExtensions = new Definition(); + $barExtensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $fooExtensions); + $container->setDefinition('enqueue.client.bar.consumption_extensions', $barExtensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'bar']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($fooExtensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $fooExtensions->getArgument(0)); + + self::assertIsArray($barExtensions->getArgument(0)); + $this->assertEquals([ + new Reference('aBarExtension'), + ], $barExtensions->getArgument(0)); + } +} diff --git a/Tests/Symfony/Client/DependencyInjection/BuildProcessorRegistryPassTest.php b/Tests/Symfony/Client/DependencyInjection/BuildProcessorRegistryPassTest.php new file mode 100644 index 0000000..5c9ac48 --- /dev/null +++ b/Tests/Symfony/Client/DependencyInjection/BuildProcessorRegistryPassTest.php @@ -0,0 +1,151 @@ +assertClassImplements(CompilerPassInterface::class, BuildProcessorRegistryPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildProcessorRegistryPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoProcessorRegistryServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.processor_registry" not found'); + $pass->process($container); + } + + public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->register('enqueue.client.foo.processor_registry'); + + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found'); + $pass->process($container); + } + + public function testThrowsIfNoRouteProcessorServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->register('enqueue.client.foo.processor_registry'); + $container->register('enqueue.client.foo.route_collection'); + + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.router_processor" not found'); + $pass->process($container); + } + + public function testThrowIfProcessorServiceIdOptionNotSet() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(), + ]); + $container->register('enqueue.client.aName.processor_registry')->addArgument([]); + $container->register('enqueue.client.aName.router_processor'); + + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The route option "processor_service_id" is required'); + $pass->process($container); + } + + public function testShouldPassLocatorAsFirstArgument() + { + $registry = new Definition(); + $registry->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route( + 'aCommand', + Route::COMMAND, + 'aBarProcessor', + ['processor_service_id' => 'aBarServiceId'] + ))->toArray(), + (new Route( + 'aTopic', + Route::TOPIC, + 'aFooProcessor', + ['processor_service_id' => 'aFooServiceId'] + ))->toArray(), + ]); + $container->setDefinition('enqueue.client.aName.processor_registry', $registry); + $container->register('enqueue.client.aName.router_processor'); + + $pass = new BuildProcessorRegistryPass(); + $pass->process($container); + + $this->assertLocatorServices($container, $registry->getArgument(0), [ + '%enqueue.client.aName.router_processor%' => 'enqueue.client.aName.router_processor', + 'aBarProcessor' => 'aBarServiceId', + 'aFooProcessor' => 'aFooServiceId', + ]); + } + + private function assertLocatorServices(ContainerBuilder $container, $locatorId, array $locatorServices) + { + $this->assertInstanceOf(Reference::class, $locatorId); + $locatorId = (string) $locatorId; + + $this->assertTrue($container->hasDefinition($locatorId)); + $this->assertMatchesRegularExpression('/\.?service_locator\..*?\.enqueue\./', $locatorId); + + $match = []; + if (false == preg_match('/(\.?service_locator\..*?)\.enqueue\./', $locatorId, $match)) { + $this->fail('preg_match should not failed'); + } + + $this->assertTrue($container->hasDefinition($match[1])); + $locator = $container->getDefinition($match[1]); + + $this->assertContainsOnly(ServiceClosureArgument::class, $locator->getArgument(0)); + $actualServices = array_map(function (ServiceClosureArgument $value) { + return (string) $value->getValues()[0]; + }, $locator->getArgument(0)); + + $this->assertEquals($locatorServices, $actualServices); + } +} diff --git a/Tests/Symfony/Client/DependencyInjection/BuildProcessorRoutesPassTest.php b/Tests/Symfony/Client/DependencyInjection/BuildProcessorRoutesPassTest.php new file mode 100644 index 0000000..0351c45 --- /dev/null +++ b/Tests/Symfony/Client/DependencyInjection/BuildProcessorRoutesPassTest.php @@ -0,0 +1,302 @@ +assertClassImplements(CompilerPassInterface::class, BuildProcessorRoutesPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildProcessorRoutesPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new BuildProcessorRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'baz'); + + $pass = new BuildProcessorRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found'); + $pass->process($container); + } + + public function testThrowIfBothTopicAndCommandAttributesAreSet() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['topic' => 'foo', 'command' => 'bar']) + ; + + $pass = new BuildProcessorRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Either "topic" or "command" tag attribute must be set on service "aFooProcessor". Both are set.'); + $pass->process($container); + } + + public function testThrowIfNeitherTopicNorCommandAttributesAreSet() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', []) + ; + + $pass = new BuildProcessorRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Either "topic" or "command" tag attribute must be set on service "aFooProcessor". None is set.'); + $pass->process($container); + } + + public function testShouldRegisterProcessorWithMatchedName() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'bar'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['client' => 'foo', 'topic' => 'foo']) + ; + $container->register('aProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['client' => 'bar', 'command' => 'foo']) + ; + + $pass = new BuildProcessorRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorWithoutNameToDefaultClient() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['topic' => 'foo']) + ; + $container->register('aProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['client' => 'bar', 'command' => 'foo']) + ; + + $pass = new BuildProcessorRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorIfClientNameEqualsAll() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['client' => 'all', 'topic' => 'foo']) + ; + $container->register('aProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['client' => 'bar', 'command' => 'foo']) + ; + + $pass = new BuildProcessorRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterAsTopicProcessor() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['topic' => 'aTopic']) + ; + + $pass = new BuildProcessorRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'aTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegisterAsCommandProcessor() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['command' => 'aCommand']) + ; + + $pass = new BuildProcessorRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'aCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegisterWithCustomProcessorName() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['command' => 'aCommand', 'processor' => 'customProcessorName']) + ; + + $pass = new BuildProcessorRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'aCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'customProcessorName', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldMergeExtractedRoutesWithAlreadySetInCollection() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([ + (new Route('aTopic', Route::TOPIC, 'aProcessor'))->toArray(), + (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(), + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['command' => 'fooCommand']) + ; + + $pass = new BuildProcessorRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(3, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'aTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aProcessor', + ], + [ + 'source' => 'aCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aProcessor', + ], + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } +} diff --git a/Tests/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPassTest.php b/Tests/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPassTest.php new file mode 100644 index 0000000..a954d9a --- /dev/null +++ b/Tests/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPassTest.php @@ -0,0 +1,423 @@ +assertClassImplements(CompilerPassInterface::class, BuildTopicSubscriberRoutesPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildTopicSubscriberRoutesPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new BuildTopicSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'baz'); + + $pass = new BuildTopicSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found'); + $pass->process($container); + } + + public function testThrowIfTaggedProcessorIsBuiltByFactory() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->register('enqueue.client.foo.route_collection', RouteCollection::class) + ->addArgument([]) + ; + $container->register('aProcessor', Processor::class) + ->setFactory('foo') + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The topic subscriber tag could not be applied to a service created by factory.'); + $pass->process($container); + } + + public function testShouldRegisterProcessorWithMatchedName() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'bar'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', get_class($this->createTopicSubscriberProcessor())) + ->addTag('enqueue.topic_subscriber', ['client' => 'foo']) + ; + $container->register('aProcessor', get_class($this->createTopicSubscriberProcessor())) + ->addTag('enqueue.topic_subscriber', ['client' => 'bar']) + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorWithoutNameToDefaultClient() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', get_class($this->createTopicSubscriberProcessor())) + ->addTag('enqueue.topic_subscriber') + ; + $container->register('aProcessor', get_class($this->createTopicSubscriberProcessor())) + ->addTag('enqueue.topic_subscriber', ['client' => 'bar']) + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorIfClientNameEqualsAll() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', get_class($this->createTopicSubscriberProcessor())) + ->addTag('enqueue.topic_subscriber', ['client' => 'all']) + ; + $container->register('aProcessor', get_class($this->createTopicSubscriberProcessor())) + ->addTag('enqueue.topic_subscriber', ['client' => 'bar']) + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorIfTopicsIsString() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createTopicSubscriberProcessor('fooTopic'); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testThrowIfTopicSubscriberReturnsNothing() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createTopicSubscriberProcessor(null); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Topic subscriber must return something.'); + $pass->process($container); + } + + public function testShouldRegisterProcessorIfTopicsAreStrings() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createTopicSubscriberProcessor(['fooTopic', 'barTopic']); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(2, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + [ + 'source' => 'barTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegisterProcessorIfTopicsAreParamArrays() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createTopicSubscriberProcessor([ + ['topic' => 'fooTopic', 'processor' => 'aCustomFooProcessorName', 'anOption' => 'aFooVal'], + ['topic' => 'barTopic', 'processor' => 'aCustomBarProcessorName', 'anOption' => 'aBarVal'], + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(2, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aCustomFooProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aFooVal', + ], + [ + 'source' => 'barTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aCustomBarProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aBarVal', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testThrowIfTopicSubscriberParamsInvalid() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createTopicSubscriberProcessor(['fooBar', true]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Topic subscriber configuration is invalid'); + $pass->process($container); + } + + public function testShouldMergeExtractedRoutesWithAlreadySetInCollection() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([ + (new Route('aTopic', Route::TOPIC, 'aProcessor'))->toArray(), + (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(), + ]); + + $processor = $this->createTopicSubscriberProcessor(['fooTopic']); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(3, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'aTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aProcessor', + ], + [ + 'source' => 'aCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aProcessor', + ], + [ + 'source' => 'fooTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegister08TopicSubscriber() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createTopicSubscriberProcessor([ + 'fooTopic' => ['processorName' => 'aCustomFooProcessorName', 'queueName' => 'fooQueue', 'queueNameHardcoded' => true, 'anOption' => 'aFooVal'], + 'barTopic' => ['processorName' => 'aCustomBarProcessorName', 'anOption' => 'aBarVal'], + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['default']); + $container->setParameter('enqueue.default_client', 'default'); + $container->setDefinition('enqueue.client.default.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(2, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aCustomFooProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aFooVal', + 'queue' => 'fooQueue', + 'prefix_queue' => false, + ], + [ + 'source' => 'barTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aCustomBarProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aBarVal', + ], + ], + $routeCollection->getArgument(0) + ); + } + + private function createTopicSubscriberProcessor($topicSubscriberReturns = ['aTopic']) + { + $processor = new class implements Processor, TopicSubscriberInterface { + public static $return; + + public function process(InteropMessage $message, Context $context) + { + return self::ACK; + } + + public static function getSubscribedTopics() + { + return static::$return; + } + }; + + $processor::$return = $topicSubscriberReturns; + + return $processor; + } +} diff --git a/Tests/Symfony/Client/DependencyInjection/ClientFactoryTest.php b/Tests/Symfony/Client/DependencyInjection/ClientFactoryTest.php new file mode 100644 index 0000000..9f37dff --- /dev/null +++ b/Tests/Symfony/Client/DependencyInjection/ClientFactoryTest.php @@ -0,0 +1,54 @@ +assertClassFinal(ClientFactory::class); + } + + public function testThrowIfEmptyNameGivenOnConstruction() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The name could not be empty.'); + + new ClientFactory(''); + } + + public function testShouldCreateDriverFromDsn() + { + $container = new ContainerBuilder(); + + $transport = new ClientFactory('default'); + + $serviceId = $transport->createDriver($container, ['dsn' => 'foo://bar/baz', 'foo' => 'fooVal']); + + $this->assertEquals('enqueue.client.default.driver', $serviceId); + + $this->assertTrue($container->hasDefinition('enqueue.client.default.driver')); + + $this->assertNotEmpty($container->getDefinition('enqueue.client.default.driver')->getFactory()); + $this->assertEquals( + [new Reference('enqueue.client.default.driver_factory'), 'create'], + $container->getDefinition('enqueue.client.default.driver')->getFactory()) + ; + $this->assertEquals( + [ + new Reference('enqueue.transport.default.connection_factory'), + new Reference('enqueue.client.default.config'), + new Reference('enqueue.client.default.route_collection'), + ], + $container->getDefinition('enqueue.client.default.driver')->getArguments()) + ; + } +} diff --git a/Tests/Symfony/Client/FlushSpoolProducerListenerTest.php b/Tests/Symfony/Client/FlushSpoolProducerListenerTest.php index a1fe06e..539d332 100644 --- a/Tests/Symfony/Client/FlushSpoolProducerListenerTest.php +++ b/Tests/Symfony/Client/FlushSpoolProducerListenerTest.php @@ -23,7 +23,7 @@ public function testShouldSubscribeOnKernelTerminateEvent() { $events = FlushSpoolProducerListener::getSubscribedEvents(); - $this->assertInternalType('array', $events); + self::assertIsArray($events); $this->assertArrayHasKey(KernelEvents::TERMINATE, $events); $this->assertEquals('flushMessages', $events[KernelEvents::TERMINATE]); @@ -33,17 +33,12 @@ public function testShouldSubscribeOnConsoleTerminateEvent() { $events = FlushSpoolProducerListener::getSubscribedEvents(); - $this->assertInternalType('array', $events); + self::assertIsArray($events); $this->assertArrayHasKey(ConsoleEvents::TERMINATE, $events); $this->assertEquals('flushMessages', $events[ConsoleEvents::TERMINATE]); } - public function testCouldBeConstructedWithSpoolProducerAsFirstArgument() - { - new FlushSpoolProducerListener($this->createSpoolProducerMock()); - } - public function testShouldFlushSpoolProducerOnFlushMessagesCall() { $producerMock = $this->createSpoolProducerMock(); @@ -58,7 +53,7 @@ public function testShouldFlushSpoolProducerOnFlushMessagesCall() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|SpoolProducer + * @return \PHPUnit\Framework\MockObject\MockObject|SpoolProducer */ private function createSpoolProducerMock() { diff --git a/Tests/Symfony/Client/Meta/QueuesCommandTest.php b/Tests/Symfony/Client/Meta/QueuesCommandTest.php deleted file mode 100644 index f0720c7..0000000 --- a/Tests/Symfony/Client/Meta/QueuesCommandTest.php +++ /dev/null @@ -1,109 +0,0 @@ -assertClassExtends(Command::class, QueuesCommand::class); - } - - public function testCouldBeConstructedWithQueueMetaRegistryAsFirstArgument() - { - new QueuesCommand($this->createQueueMetaRegistryStub()); - } - - public function testShouldHaveCommandName() - { - $command = new QueuesCommand($this->createQueueMetaRegistryStub()); - - $this->assertEquals('enqueue:queues', $command->getName()); - } - - public function testShouldHaveCommandAliases() - { - $command = new QueuesCommand($this->createQueueMetaRegistryStub()); - - $this->assertEquals(['enq:m:q', 'debug:enqueue:queues'], $command->getAliases()); - } - - public function testShouldShowMessageFoundZeroDestinationsIfAnythingInRegistry() - { - $command = new QueuesCommand($this->createQueueMetaRegistryStub()); - - $output = $this->executeCommand($command); - - $this->assertContains('Found 0 destinations', $output); - } - - public function testShouldShowMessageFoundTwoDestinations() - { - $command = new QueuesCommand($this->createQueueMetaRegistryStub([ - new QueueMeta('aClientName', 'aDestinationName'), - new QueueMeta('anotherClientName', 'anotherDestinationName'), - ])); - - $output = $this->executeCommand($command); - - $this->assertContains('Found 2 destinations', $output); - } - - public function testShouldShowInfoAboutDestinations() - { - $command = new QueuesCommand($this->createQueueMetaRegistryStub([ - new QueueMeta('aFooClientName', 'aFooDestinationName', ['fooSubscriber']), - new QueueMeta('aBarClientName', 'aBarDestinationName', ['barSubscriber']), - ])); - - $output = $this->executeCommand($command); - - $this->assertContains('aFooClientName', $output); - $this->assertContains('aFooDestinationName', $output); - $this->assertContains('fooSubscriber', $output); - $this->assertContains('aBarClientName', $output); - $this->assertContains('aBarDestinationName', $output); - $this->assertContains('barSubscriber', $output); - } - - /** - * @param Command $command - * @param string[] $arguments - * - * @return string - */ - protected function executeCommand(Command $command, array $arguments = []) - { - $tester = new CommandTester($command); - $tester->execute($arguments); - - return $tester->getDisplay(); - } - - /** - * @param mixed $destinations - * - * @return \PHPUnit_Framework_MockObject_MockObject|QueueMetaRegistry - */ - protected function createQueueMetaRegistryStub($destinations = []) - { - $registryMock = $this->createMock(QueueMetaRegistry::class); - $registryMock - ->expects($this->any()) - ->method('getQueuesMeta') - ->willReturn($destinations) - ; - - return $registryMock; - } -} diff --git a/Tests/Symfony/Client/Meta/TopicsCommandTest.php b/Tests/Symfony/Client/Meta/TopicsCommandTest.php deleted file mode 100644 index 4efdd2f..0000000 --- a/Tests/Symfony/Client/Meta/TopicsCommandTest.php +++ /dev/null @@ -1,91 +0,0 @@ -assertClassExtends(Command::class, TopicsCommand::class); - } - - public function testCouldBeConstructedWithTopicMetaRegistryAsFirstArgument() - { - new TopicsCommand(new TopicMetaRegistry([])); - } - - public function testShouldHaveCommandName() - { - $command = new TopicsCommand(new TopicMetaRegistry([])); - - $this->assertEquals('enqueue:topics', $command->getName()); - } - - public function testShouldHaveCommandAliases() - { - $command = new TopicsCommand(new TopicMetaRegistry([])); - - $this->assertEquals(['enq:m:t', 'debug:enqueue:topics'], $command->getAliases()); - } - - public function testShouldShowMessageFoundZeroTopicsIfAnythingInRegistry() - { - $command = new TopicsCommand(new TopicMetaRegistry([])); - - $output = $this->executeCommand($command); - - $this->assertContains('Found 0 topics', $output); - } - - public function testShouldShowMessageFoundTwoTopics() - { - $command = new TopicsCommand(new TopicMetaRegistry([ - 'fooTopic' => [], - 'barTopic' => [], - ])); - - $output = $this->executeCommand($command); - - $this->assertContains('Found 2 topics', $output); - } - - public function testShouldShowInfoAboutTopics() - { - $command = new TopicsCommand(new TopicMetaRegistry([ - 'fooTopic' => ['description' => 'fooDescription', 'processors' => ['fooSubscriber']], - 'barTopic' => ['description' => 'barDescription', 'processors' => ['barSubscriber']], - ])); - - $output = $this->executeCommand($command); - - $this->assertContains('fooTopic', $output); - $this->assertContains('fooDescription', $output); - $this->assertContains('fooSubscriber', $output); - $this->assertContains('barTopic', $output); - $this->assertContains('barDescription', $output); - $this->assertContains('barSubscriber', $output); - } - - /** - * @param Command $command - * @param string[] $arguments - * - * @return string - */ - protected function executeCommand(Command $command, array $arguments = []) - { - $tester = new CommandTester($command); - $tester->execute($arguments); - - return $tester->getDisplay(); - } -} diff --git a/Tests/Symfony/Client/Mock/SetupBrokerExtensionCommand.php b/Tests/Symfony/Client/Mock/SetupBrokerExtensionCommand.php index fe7a352..c217505 100644 --- a/Tests/Symfony/Client/Mock/SetupBrokerExtensionCommand.php +++ b/Tests/Symfony/Client/Mock/SetupBrokerExtensionCommand.php @@ -3,8 +3,8 @@ namespace Enqueue\Tests\Symfony\Client\Mock; use Enqueue\Client\Config; -use Enqueue\Client\Meta\QueueMetaRegistry; -use Enqueue\Null\Client\NullDriver; +use Enqueue\Client\Driver\GenericDriver; +use Enqueue\Client\RouteCollection; use Enqueue\Null\NullContext; use Enqueue\Symfony\Client\SetupBrokerExtensionCommandTrait; use Symfony\Component\Console\Command\Command; @@ -29,12 +29,14 @@ protected function configure() $this->configureSetupBrokerExtension(); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - $this->extension = $this->getSetupBrokerExtension($input, new NullDriver( + $this->extension = $this->getSetupBrokerExtension($input, new GenericDriver( new NullContext(), Config::create(), - new QueueMetaRegistry(Config::create(), []) + new RouteCollection([]) )); + + return 0; } } diff --git a/Tests/Symfony/Client/ProduceCommandTest.php b/Tests/Symfony/Client/ProduceCommandTest.php new file mode 100644 index 0000000..daa9091 --- /dev/null +++ b/Tests/Symfony/Client/ProduceCommandTest.php @@ -0,0 +1,284 @@ +assertClassExtends(Command::class, ProduceCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(ProduceCommand::class); + } + + public function testShouldHaveAsCommandAttributeWithCommandName() + { + $commandClass = ProduceCommand::class; + + $reflectionClass = new \ReflectionClass($commandClass); + + $attributes = $reflectionClass->getAttributes(AsCommand::class); + + $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.'); + + // Get the first attribute instance (assuming there is only one AsCommand attribute) + $asCommandAttribute = $attributes[0]; + + // Verify the 'name' parameter value + $attributeInstance = $asCommandAttribute->newInstance(); + $this->assertEquals('enqueue:produce', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.'); + } + + public function testShouldHaveExpectedOptions() + { + $command = new ProduceCommand($this->createMock(ContainerInterface::class), 'default'); + + $options = $command->getDefinition()->getOptions(); + $this->assertCount(4, $options); + $this->assertArrayHasKey('client', $options); + $this->assertArrayHasKey('topic', $options); + $this->assertArrayHasKey('command', $options); + $this->assertArrayHasKey('header', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new ProduceCommand($this->createMock(ContainerInterface::class), 'default'); + + $arguments = $command->getDefinition()->getArguments(); + $this->assertCount(1, $arguments); + + $this->assertArrayHasKey('message', $arguments); + } + + public function testThrowIfNeitherTopicNorCommandOptionsAreSet() + { + $producerMock = $this->createProducerMock(); + $producerMock + ->expects($this->never()) + ->method('sendEvent') + ; + $producerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $producerMock, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Either topic or command option should be set, none is set.'); + $tester->execute([ + 'message' => 'theMessage', + ]); + } + + public function testThrowIfBothTopicAndCommandOptionsAreSet() + { + $producerMock = $this->createProducerMock(); + $producerMock + ->expects($this->never()) + ->method('sendEvent') + ; + $producerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $producerMock, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Either topic or command option should be set, both are set.'); + $tester->execute([ + 'message' => 'theMessage', + '--topic' => 'theTopic', + '--command' => 'theCommand', + ]); + } + + public function testShouldSendEventToDefaultTransport() + { + $header = 'Content-Type: text/plain'; + $payload = 'theMessage'; + + $producerMock = $this->createProducerMock(); + $producerMock + ->expects($this->once()) + ->method('sendEvent') + ->with('theTopic', new Message($payload, [], [$header])) + ; + $producerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $producerMock, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'message' => $payload, + '--header' => $header, + '--topic' => 'theTopic', + ]); + } + + public function testShouldSendCommandToDefaultTransport() + { + $producerMock = $this->createProducerMock(); + $producerMock + ->expects($this->once()) + ->method('sendCommand') + ->with('theCommand', 'theMessage') + ; + $producerMock + ->expects($this->never()) + ->method('sendEvent') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $producerMock, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'message' => 'theMessage', + '--command' => 'theCommand', + ]); + } + + public function testShouldSendEventToFooTransport() + { + $header = 'Content-Type: text/plain'; + $payload = 'theMessage'; + + $defaultProducerMock = $this->createProducerMock(); + $defaultProducerMock + ->expects($this->never()) + ->method('sendEvent') + ; + $defaultProducerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $fooProducerMock = $this->createProducerMock(); + $fooProducerMock + ->expects($this->once()) + ->method('sendEvent') + ->with('theTopic', new Message($payload, [], [$header])) + ; + $fooProducerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $defaultProducerMock, + 'enqueue.client.foo.producer' => $fooProducerMock, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'message' => $payload, + '--header' => $header, + '--topic' => 'theTopic', + '--client' => 'foo', + ]); + } + + public function testShouldSendCommandToFooTransport() + { + $defaultProducerMock = $this->createProducerMock(); + $defaultProducerMock + ->expects($this->never()) + ->method('sendEvent') + ; + $defaultProducerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $fooProducerMock = $this->createProducerMock(); + $fooProducerMock + ->expects($this->once()) + ->method('sendCommand') + ->with('theCommand', 'theMessage') + ; + $fooProducerMock + ->expects($this->never()) + ->method('sendEvent') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $defaultProducerMock, + 'enqueue.client.foo.producer' => $fooProducerMock, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'message' => 'theMessage', + '--command' => 'theCommand', + '--client' => 'foo', + ]); + } + + public function testThrowIfClientNotFound() + { + $defaultProducerMock = $this->createProducerMock(); + $defaultProducerMock + ->expects($this->never()) + ->method('sendEvent') + ; + $defaultProducerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $defaultProducerMock, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Client "bar" is not supported.'); + $tester->execute([ + 'message' => 'theMessage', + '--command' => 'theCommand', + '--client' => 'bar', + ]); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface + */ + private function createProducerMock() + { + return $this->createMock(ProducerInterface::class); + } +} diff --git a/Tests/Symfony/Client/ProduceMessageCommandTest.php b/Tests/Symfony/Client/ProduceMessageCommandTest.php deleted file mode 100644 index 2cd8095..0000000 --- a/Tests/Symfony/Client/ProduceMessageCommandTest.php +++ /dev/null @@ -1,75 +0,0 @@ -createProducerMock()); - } - - public function testShouldHaveCommandName() - { - $command = new ProduceMessageCommand($this->createProducerMock()); - - $this->assertEquals('enqueue:produce', $command->getName()); - } - - public function testShouldHaveCommandAliases() - { - $command = new ProduceMessageCommand($this->createProducerMock()); - - $this->assertEquals(['enq:p'], $command->getAliases()); - } - - public function testShouldHaveExpectedOptions() - { - $command = new ProduceMessageCommand($this->createProducerMock()); - - $options = $command->getDefinition()->getOptions(); - $this->assertCount(0, $options); - } - - public function testShouldHaveExpectedAttributes() - { - $command = new ProduceMessageCommand($this->createProducerMock()); - - $arguments = $command->getDefinition()->getArguments(); - $this->assertCount(2, $arguments); - - $this->assertArrayHasKey('topic', $arguments); - $this->assertArrayHasKey('message', $arguments); - } - - public function testShouldExecuteConsumptionAndUseDefaultQueueName() - { - $producerMock = $this->createProducerMock(); - $producerMock - ->expects($this->once()) - ->method('sendEvent') - ->with('theTopic', 'theMessage') - ; - - $command = new ProduceMessageCommand($producerMock); - - $tester = new CommandTester($command); - $tester->execute([ - 'topic' => 'theTopic', - 'message' => 'theMessage', - ]); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|ProducerInterface - */ - private function createProducerMock() - { - return $this->createMock(ProducerInterface::class); - } -} diff --git a/Tests/Symfony/Client/RoutesCommandTest.php b/Tests/Symfony/Client/RoutesCommandTest.php new file mode 100644 index 0000000..89bd7f7 --- /dev/null +++ b/Tests/Symfony/Client/RoutesCommandTest.php @@ -0,0 +1,366 @@ +assertClassExtends(Command::class, RoutesCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(RoutesCommand::class); + } + + public function testShouldHaveAsCommandAttributeWithCommandName() + { + $commandClass = RoutesCommand::class; + + $reflectionClass = new \ReflectionClass($commandClass); + + $attributes = $reflectionClass->getAttributes(AsCommand::class); + + $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.'); + + // Get the first attribute instance (assuming there is only one AsCommand attribute) + $asCommandAttribute = $attributes[0]; + + // Verify the 'name' parameter value + $attributeInstance = $asCommandAttribute->newInstance(); + $this->assertEquals('enqueue:routes', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.'); + } + + public function testShouldHaveCommandAliases() + { + $command = new RoutesCommand($this->createMock(ContainerInterface::class), 'default'); + + $this->assertEquals(['debug:enqueue:routes'], $command->getAliases()); + } + + public function testShouldHaveExpectedOptions() + { + $command = new RoutesCommand($this->createMock(ContainerInterface::class), 'default'); + + $options = $command->getDefinition()->getOptions(); + $this->assertCount(2, $options); + + $this->assertArrayHasKey('show-route-options', $options); + $this->assertArrayHasKey('client', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new RoutesCommand($this->createMock(ContainerInterface::class), 'default'); + + $arguments = $command->getDefinition()->getArguments(); + $this->assertCount(0, $arguments); + } + + public function testShouldOutputEmptyRouteCollection() + { + $routeCollection = new RouteCollection([]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $expectedOutput = <<<'OUTPUT' +Found 0 routes + + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + public function testShouldUseFooDriver() + { + $routeCollection = new RouteCollection([ + new Route('fooTopic', Route::TOPIC, 'processor'), + ]); + + $defaultDriverMock = $this->createMock(DriverInterface::class); + $defaultDriverMock + ->expects($this->never()) + ->method('getConfig') + ; + + $defaultDriverMock + ->expects($this->never()) + ->method('getRouteCollection') + ; + + $fooDriverMock = $this->createDriverStub(Config::create(), $routeCollection); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $defaultDriverMock, + 'enqueue.client.foo.driver' => $fooDriverMock, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + '--client' => 'foo', + ]); + + $this->assertStringContainsString('Found 1 routes', $tester->getDisplay()); + } + + public function testThrowIfClientNotFound() + { + $defaultDriverMock = $this->createMock(DriverInterface::class); + $defaultDriverMock + ->expects($this->never()) + ->method('getConfig') + ; + + $defaultDriverMock + ->expects($this->never()) + ->method('getRouteCollection') + ; + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $defaultDriverMock, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Client "foo" is not supported.'); + $tester->execute([ + '--client' => 'foo', + ]); + } + + public function testShouldOutputTopicRouteInfo() + { + $routeCollection = new RouteCollection([ + new Route('fooTopic', Route::TOPIC, 'processor'), + new Route('barTopic', Route::TOPIC, 'processor'), + ]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $expectedOutput = <<<'OUTPUT' +Found 2 routes + ++-------+----------+--------------------+-----------+----------+ +| Type | Source | Queue | Processor | Options | ++-------+----------+--------------------+-----------+----------+ +| topic | fooTopic | default (prefixed) | processor | (hidden) | +| topic | barTopic | default (prefixed) | processor | (hidden) | ++-------+----------+--------------------+-----------+----------+ + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + public function testShouldOutputCommandRouteInfo() + { + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'processor', ['foo' => 'fooVal', 'bar' => 'barVal']), + new Route('barCommand', Route::COMMAND, 'processor', ['foo' => 'fooVal', 'bar' => 'barVal']), + ]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $expectedOutput = <<<'OUTPUT' +Found 2 routes + ++---------+------------+--------------------+-----------+----------+ +| Type | Source | Queue | Processor | Options | ++---------+------------+--------------------+-----------+----------+ +| command | fooCommand | default (prefixed) | processor | (hidden) | +| command | barCommand | default (prefixed) | processor | (hidden) | ++---------+------------+--------------------+-----------+----------+ + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + public function testShouldCorrectlyOutputPrefixedCustomQueue() + { + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'processor', ['queue' => 'foo']), + new Route('barTopic', Route::TOPIC, 'processor', ['queue' => 'bar']), + ]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $exitCode = $tester->execute([]); + $this->assertSame(0, $exitCode); + + $expectedOutput = <<<'OUTPUT' +Found 2 routes + ++---------+------------+----------------+-----------+----------+ +| Type | Source | Queue | Processor | Options | ++---------+------------+----------------+-----------+----------+ +| topic | barTopic | bar (prefixed) | processor | (hidden) | +| command | fooCommand | foo (prefixed) | processor | (hidden) | ++---------+------------+----------------+-----------+----------+ + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + public function testShouldCorrectlyOutputNotPrefixedCustomQueue() + { + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'processor', ['queue' => 'foo', 'prefix_queue' => false]), + new Route('barTopic', Route::TOPIC, 'processor', ['queue' => 'bar', 'prefix_queue' => false]), + ]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $expectedOutput = <<<'OUTPUT' +Found 2 routes + ++---------+------------+-------------+-----------+----------+ +| Type | Source | Queue | Processor | Options | ++---------+------------+-------------+-----------+----------+ +| topic | barTopic | bar (as is) | processor | (hidden) | +| command | fooCommand | foo (as is) | processor | (hidden) | ++---------+------------+-------------+-----------+----------+ + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + public function testShouldCorrectlyOutputExternalRoute() + { + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'processor', ['external' => true]), + new Route('barTopic', Route::TOPIC, 'processor', ['external' => true]), + ]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $expectedOutput = <<assertCommandOutput($expectedOutput, $tester); + } + + public function testShouldOutputRouteOptions() + { + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'processor', ['foo' => 'fooVal']), + new Route('barTopic', Route::TOPIC, 'processor', ['bar' => 'barVal']), + ]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute(['--show-route-options' => true]); + + $expectedOutput = <<<'OUTPUT' +Found 2 routes + ++---------+------------+--------------------+-----------+----------------------+ +| Type | Source | Queue | Processor | Options | ++---------+------------+--------------------+-----------+----------------------+ +| topic | barTopic | default (prefixed) | processor | array ( | +| | | | | 'bar' => 'barVal', | +| | | | | ) | +| command | fooCommand | default (prefixed) | processor | array ( | +| | | | | 'foo' => 'fooVal', | +| | | | | ) | ++---------+------------+--------------------+-----------+----------------------+ + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface + */ + private function createDriverStub(Config $config, RouteCollection $routeCollection): DriverInterface + { + $driverMock = $this->createMock(DriverInterface::class); + $driverMock + ->expects($this->any()) + ->method('getConfig') + ->willReturn($config) + ; + + $driverMock + ->expects($this->any()) + ->method('getRouteCollection') + ->willReturn($routeCollection) + ; + + return $driverMock; + } + + private function assertCommandOutput(string $expected, CommandTester $tester): void + { + $this->assertSame(0, $tester->getStatusCode()); + $this->assertSame($expected, $tester->getDisplay()); + } +} diff --git a/Tests/Symfony/Client/SetupBrokerCommandTest.php b/Tests/Symfony/Client/SetupBrokerCommandTest.php index 740d6d1..c81c4e1 100644 --- a/Tests/Symfony/Client/SetupBrokerCommandTest.php +++ b/Tests/Symfony/Client/SetupBrokerCommandTest.php @@ -3,32 +3,74 @@ namespace Enqueue\Tests\Symfony\Client; use Enqueue\Client\DriverInterface; +use Enqueue\Container\Container; use Enqueue\Symfony\Client\SetupBrokerCommand; +use Enqueue\Test\ClassExtensionTrait; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; class SetupBrokerCommandTest extends TestCase { - public function testCouldBeConstructedWithRequiredAttributes() + use ClassExtensionTrait; + + public function testShouldBeSubClassOfCommand() { - new \Enqueue\Symfony\Client\SetupBrokerCommand($this->createClientDriverMock()); + $this->assertClassExtends(Command::class, SetupBrokerCommand::class); } - public function testShouldHaveCommandName() + public function testShouldNotBeFinal() { - $command = new SetupBrokerCommand($this->createClientDriverMock()); + $this->assertClassNotFinal(SetupBrokerCommand::class); + } + + public function testShouldHaveAsCommandAttributeWithCommandName() + { + $commandClass = SetupBrokerCommand::class; + + $reflectionClass = new \ReflectionClass($commandClass); - $this->assertEquals('enqueue:setup-broker', $command->getName()); + $attributes = $reflectionClass->getAttributes(AsCommand::class); + + $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.'); + + // Get the first attribute instance (assuming there is only one AsCommand attribute) + $asCommandAttribute = $attributes[0]; + + // Verify the 'name' parameter value + $attributeInstance = $asCommandAttribute->newInstance(); + $this->assertEquals('enqueue:setup-broker', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.'); } public function testShouldHaveCommandAliases() { - $command = new SetupBrokerCommand($this->createClientDriverMock()); + $command = new SetupBrokerCommand($this->createMock(ContainerInterface::class), 'default'); $this->assertEquals(['enq:sb'], $command->getAliases()); } - public function testShouldCreateQueues() + public function testShouldHaveExpectedOptions() + { + $command = new SetupBrokerCommand($this->createMock(ContainerInterface::class), 'default'); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(1, $options); + $this->assertArrayHasKey('client', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new SetupBrokerCommand($this->createMock(ContainerInterface::class), 'default'); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(0, $arguments); + } + + public function testShouldCallDriverSetupBrokerMethod() { $driver = $this->createClientDriverMock(); $driver @@ -36,16 +78,66 @@ public function testShouldCreateQueues() ->method('setupBroker') ; - $command = new SetupBrokerCommand($driver); + $command = new SetupBrokerCommand(new Container([ + 'enqueue.client.default.driver' => $driver, + ]), 'default'); $tester = new CommandTester($command); $tester->execute([]); - $this->assertContains('Setup Broker', $tester->getDisplay()); + $this->assertStringContainsString('Broker set up', $tester->getDisplay()); + } + + public function testShouldCallRequestedClientDriverSetupBrokerMethod() + { + $defaultDriver = $this->createClientDriverMock(); + $defaultDriver + ->expects($this->never()) + ->method('setupBroker') + ; + + $fooDriver = $this->createClientDriverMock(); + $fooDriver + ->expects($this->once()) + ->method('setupBroker') + ; + + $command = new SetupBrokerCommand(new Container([ + 'enqueue.client.default.driver' => $defaultDriver, + 'enqueue.client.foo.driver' => $fooDriver, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + '--client' => 'foo', + ]); + + $this->assertStringContainsString('Broker set up', $tester->getDisplay()); + } + + public function testShouldThrowIfClientNotFound() + { + $defaultDriver = $this->createClientDriverMock(); + $defaultDriver + ->expects($this->never()) + ->method('setupBroker') + ; + + $command = new SetupBrokerCommand(new Container([ + 'enqueue.client.default.driver' => $defaultDriver, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Client "foo" is not supported.'); + $tester->execute([ + '--client' => 'foo', + ]); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface + * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface */ private function createClientDriverMock() { diff --git a/Tests/Symfony/Client/SimpleConsumeCommandTest.php b/Tests/Symfony/Client/SimpleConsumeCommandTest.php new file mode 100644 index 0000000..21c491e --- /dev/null +++ b/Tests/Symfony/Client/SimpleConsumeCommandTest.php @@ -0,0 +1,130 @@ +assertClassExtends(ConsumeCommand::class, SimpleConsumeCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(SimpleConsumeCommand::class); + } + + public function testShouldHaveExpectedOptions() + { + $command = new SimpleConsumeCommand($this->createQueueConsumerMock(), $this->createDriverStub(), $this->createDelegateProcessorMock()); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(9, $options); + $this->assertArrayHasKey('memory-limit', $options); + $this->assertArrayHasKey('message-limit', $options); + $this->assertArrayHasKey('time-limit', $options); + $this->assertArrayHasKey('receive-timeout', $options); + $this->assertArrayHasKey('niceness', $options); + $this->assertArrayHasKey('client', $options); + $this->assertArrayHasKey('logger', $options); + $this->assertArrayHasKey('skip', $options); + $this->assertArrayHasKey('setup-broker', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new SimpleConsumeCommand($this->createQueueConsumerMock(), $this->createDriverStub(), $this->createDelegateProcessorMock()); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(1, $arguments); + $this->assertArrayHasKey('client-queue-names', $arguments); + } + + public function testShouldBindDefaultQueueOnly() + { + $queue = new NullQueue(''); + + $routeCollection = new RouteCollection([]); + + $processor = $this->createDelegateProcessorMock(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('bind') + ->with($this->identicalTo($queue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->once()) + ->method('createQueue') + ->with('default', true) + ->willReturn($queue) + ; + + $command = new SimpleConsumeCommand($consumer, $driver, $processor); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DelegateProcessor + */ + private function createDelegateProcessorMock() + { + return $this->createMock(DelegateProcessor::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface + */ + private function createQueueConsumerMock() + { + return $this->createMock(QueueConsumerInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface + */ + private function createDriverStub(?RouteCollection $routeCollection = null): DriverInterface + { + $driverMock = $this->createMock(DriverInterface::class); + $driverMock + ->expects($this->any()) + ->method('getRouteCollection') + ->willReturn($routeCollection ?? new RouteCollection([])) + ; + + $driverMock + ->expects($this->any()) + ->method('getConfig') + ->willReturn(Config::create('aPrefix', 'anApp')) + ; + + return $driverMock; + } +} diff --git a/Tests/Symfony/Client/SimpleProduceCommandTest.php b/Tests/Symfony/Client/SimpleProduceCommandTest.php new file mode 100644 index 0000000..3ff81bf --- /dev/null +++ b/Tests/Symfony/Client/SimpleProduceCommandTest.php @@ -0,0 +1,78 @@ +assertClassExtends(ProduceCommand::class, SimpleProduceCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(SimpleProduceCommand::class); + } + + public function testShouldHaveExpectedOptions() + { + $command = new SimpleProduceCommand($this->createProducerMock()); + + $options = $command->getDefinition()->getOptions(); + $this->assertCount(4, $options); + $this->assertArrayHasKey('client', $options); + $this->assertArrayHasKey('topic', $options); + $this->assertArrayHasKey('command', $options); + $this->assertArrayHasKey('header', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new SimpleProduceCommand($this->createProducerMock()); + + $arguments = $command->getDefinition()->getArguments(); + $this->assertCount(1, $arguments); + + $this->assertArrayHasKey('message', $arguments); + } + + public function testThrowIfNeitherTopicNorCommandOptionsAreSet() + { + $producerMock = $this->createProducerMock(); + $producerMock + ->expects($this->never()) + ->method('sendEvent') + ; + $producerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $command = new SimpleProduceCommand($producerMock); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Either topic or command option should be set, none is set.'); + $tester->execute([ + 'message' => 'theMessage', + ]); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface + */ + private function createProducerMock() + { + return $this->createMock(ProducerInterface::class); + } +} diff --git a/Tests/Symfony/Client/SimpleRoutesCommandTest.php b/Tests/Symfony/Client/SimpleRoutesCommandTest.php new file mode 100644 index 0000000..20ee454 --- /dev/null +++ b/Tests/Symfony/Client/SimpleRoutesCommandTest.php @@ -0,0 +1,107 @@ +assertClassExtends(RoutesCommand::class, SimpleRoutesCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(SimpleRoutesCommand::class); + } + + public function testShouldHaveCommandAliases() + { + $command = new SimpleRoutesCommand($this->createDriverMock()); + + $this->assertEquals(['debug:enqueue:routes'], $command->getAliases()); + } + + public function testShouldHaveExpectedOptions() + { + $command = new SimpleRoutesCommand($this->createDriverMock()); + + $options = $command->getDefinition()->getOptions(); + $this->assertCount(2, $options); + + $this->assertArrayHasKey('show-route-options', $options); + $this->assertArrayHasKey('client', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new SimpleRoutesCommand($this->createDriverMock()); + + $arguments = $command->getDefinition()->getArguments(); + $this->assertCount(0, $arguments); + } + + public function testShouldOutputEmptyRouteCollection() + { + $routeCollection = new RouteCollection([]); + + $command = new SimpleRoutesCommand($this->createDriverStub(Config::create(), $routeCollection)); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $expectedOutput = <<<'OUTPUT' +Found 0 routes + + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverMock(): DriverInterface + { + return $this->createMock(DriverInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverStub(Config $config, RouteCollection $routeCollection): DriverInterface + { + $driverMock = $this->createDriverMock(); + $driverMock + ->expects($this->any()) + ->method('getConfig') + ->willReturn($config) + ; + + $driverMock + ->expects($this->any()) + ->method('getRouteCollection') + ->willReturn($routeCollection) + ; + + return $driverMock; + } + + private function assertCommandOutput(string $expected, CommandTester $tester): void + { + $this->assertSame(0, $tester->getStatusCode()); + $this->assertSame($expected, $tester->getDisplay()); + } +} diff --git a/Tests/Symfony/Client/SimpleSetupBrokerCommandTest.php b/Tests/Symfony/Client/SimpleSetupBrokerCommandTest.php new file mode 100644 index 0000000..3702dbf --- /dev/null +++ b/Tests/Symfony/Client/SimpleSetupBrokerCommandTest.php @@ -0,0 +1,75 @@ +assertClassExtends(SetupBrokerCommand::class, SimpleSetupBrokerCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(SimpleSetupBrokerCommand::class); + } + + public function testShouldHaveCommandAliases() + { + $command = new SimpleSetupBrokerCommand($this->createClientDriverMock()); + + $this->assertEquals(['enq:sb'], $command->getAliases()); + } + + public function testShouldHaveExpectedOptions() + { + $command = new SimpleSetupBrokerCommand($this->createClientDriverMock()); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(1, $options); + $this->assertArrayHasKey('client', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new SimpleSetupBrokerCommand($this->createClientDriverMock()); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(0, $arguments); + } + + public function testShouldCallDriverSetupBrokerMethod() + { + $driver = $this->createClientDriverMock(); + $driver + ->expects($this->once()) + ->method('setupBroker') + ; + + $command = new SimpleSetupBrokerCommand($driver); + + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertStringContainsString('Broker set up', $tester->getDisplay()); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface + */ + private function createClientDriverMock() + { + return $this->createMock(DriverInterface::class); + } +} diff --git a/Tests/Symfony/Consumption/ConfigurableConsumeCommandTest.php b/Tests/Symfony/Consumption/ConfigurableConsumeCommandTest.php new file mode 100644 index 0000000..251e264 --- /dev/null +++ b/Tests/Symfony/Consumption/ConfigurableConsumeCommandTest.php @@ -0,0 +1,306 @@ +assertClassExtends(Command::class, ConfigurableConsumeCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(ConfigurableConsumeCommand::class); + } + + public function testShouldHaveAsCommandAttributeWithCommandName() + { + $commandClass = ConfigurableConsumeCommand::class; + + $reflectionClass = new \ReflectionClass($commandClass); + + $attributes = $reflectionClass->getAttributes(AsCommand::class); + + $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.'); + + // Get the first attribute instance (assuming there is only one AsCommand attribute) + $asCommandAttribute = $attributes[0]; + + // Verify the 'name' parameter value + $attributeInstance = $asCommandAttribute->newInstance(); + $this->assertEquals('enqueue:transport:consume', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.'); + } + + public function testShouldHaveExpectedOptions() + { + $command = new ConfigurableConsumeCommand($this->createMock(ContainerInterface::class), 'default'); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(7, $options); + $this->assertArrayHasKey('memory-limit', $options); + $this->assertArrayHasKey('message-limit', $options); + $this->assertArrayHasKey('time-limit', $options); + $this->assertArrayHasKey('receive-timeout', $options); + $this->assertArrayHasKey('niceness', $options); + $this->assertArrayHasKey('transport', $options); + $this->assertArrayHasKey('logger', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new ConfigurableConsumeCommand($this->createMock(ContainerInterface::class), 'default'); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(2, $arguments); + $this->assertArrayHasKey('processor', $arguments); + $this->assertArrayHasKey('queues', $arguments); + } + + public function testThrowIfNeitherQueueOptionNorProcessorImplementsQueueSubscriberInterface() + { + $processor = $this->createProcessor(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->never()) + ->method('bind') + ; + $consumer + ->expects($this->never()) + ->method('consume') + ; + + $command = new ConfigurableConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + 'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['aProcessor' => $processor]), + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The queue is not provided. The processor must implement "Enqueue\Consumption\QueueSubscriberInterface" interface and it must return not empty array of queues or a queue set using as a second argument.'); + $tester->execute([ + 'processor' => 'aProcessor', + ]); + } + + public function testShouldExecuteConsumptionWithExplicitlySetQueue() + { + $processor = $this->createProcessor(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('bind') + ->with('queue-name', $this->identicalTo($processor)) + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConfigurableConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + 'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]), + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'processor' => 'processor-service', + 'queues' => ['queue-name'], + ]); + } + + public function testThrowIfTransportNotDefined() + { + $processor = $this->createProcessor(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->never()) + ->method('bind') + ; + $consumer + ->expects($this->never()) + ->method('consume') + ; + + $command = new ConfigurableConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + 'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]), + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Transport "not-defined" is not supported.'); + $tester->execute([ + 'processor' => 'processor-service', + 'queues' => ['queue-name'], + '--transport' => 'not-defined', + ]); + } + + public function testShouldExecuteConsumptionWithSeveralCustomQueues() + { + $processor = $this->createProcessor(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->at(0)) + ->method('bind') + ->with('queue-name', $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(1)) + ->method('bind') + ->with('another-queue-name', $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(2)) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConfigurableConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + 'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]), + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'processor' => 'processor-service', + 'queues' => ['queue-name', 'another-queue-name'], + ]); + } + + public function testShouldExecuteConsumptionWhenProcessorImplementsQueueSubscriberInterface() + { + $processor = new class implements Processor, QueueSubscriberInterface { + public function process(InteropMessage $message, Context $context): void + { + } + + public static function getSubscribedQueues() + { + return ['fooSubscribedQueues', 'barSubscribedQueues']; + } + }; + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->at(0)) + ->method('bind') + ->with('fooSubscribedQueues', $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(1)) + ->method('bind') + ->with('barSubscribedQueues', $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(2)) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConfigurableConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + 'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]), + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'processor' => 'processor-service', + ]); + } + + public function testShouldExecuteConsumptionWithCustomTransportExplicitlySetQueue() + { + $processor = $this->createProcessor(); + + $fooConsumer = $this->createQueueConsumerMock(); + $fooConsumer + ->expects($this->never()) + ->method('bind') + ; + $fooConsumer + ->expects($this->never()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $barConsumer = $this->createQueueConsumerMock(); + $barConsumer + ->expects($this->once()) + ->method('bind') + ->with('queue-name', $this->identicalTo($processor)) + ; + $barConsumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConfigurableConsumeCommand(new Container([ + 'enqueue.transport.foo.queue_consumer' => $fooConsumer, + 'enqueue.transport.foo.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]), + 'enqueue.transport.bar.queue_consumer' => $barConsumer, + 'enqueue.transport.bar.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]), + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'processor' => 'processor-service', + 'queues' => ['queue-name'], + '--transport' => 'bar', + ]); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|InteropQueue + */ + protected function createQueueMock() + { + return $this->createMock(InteropQueue::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|Processor + */ + protected function createProcessor() + { + return $this->createMock(Processor::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface + */ + protected function createQueueConsumerMock() + { + return $this->createMock(QueueConsumerInterface::class); + } +} diff --git a/Tests/Symfony/Consumption/ConsumeCommandTest.php b/Tests/Symfony/Consumption/ConsumeCommandTest.php new file mode 100644 index 0000000..f07bef0 --- /dev/null +++ b/Tests/Symfony/Consumption/ConsumeCommandTest.php @@ -0,0 +1,247 @@ +assertClassExtends(Command::class, ConsumeCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(ConsumeCommand::class); + } + + public function testShouldHaveAsCommandAttributeWithCommandName() + { + $commandClass = ConsumeCommand::class; + + $reflectionClass = new \ReflectionClass($commandClass); + + $attributes = $reflectionClass->getAttributes(AsCommand::class); + + $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.'); + + // Get the first attribute instance (assuming there is only one AsCommand attribute) + $asCommandAttribute = $attributes[0]; + + // Verify the 'name' parameter value + $attributeInstance = $asCommandAttribute->newInstance(); + $this->assertEquals('enqueue:transport:consume', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.'); + } + + public function testShouldHaveExpectedOptions() + { + $command = new ConsumeCommand($this->createMock(ContainerInterface::class), 'default'); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(7, $options); + $this->assertArrayHasKey('memory-limit', $options); + $this->assertArrayHasKey('message-limit', $options); + $this->assertArrayHasKey('time-limit', $options); + $this->assertArrayHasKey('receive-timeout', $options); + $this->assertArrayHasKey('niceness', $options); + $this->assertArrayHasKey('transport', $options); + $this->assertArrayHasKey('logger', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new ConsumeCommand($this->createMock(ContainerInterface::class), 'default'); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(0, $arguments); + } + + public function testShouldExecuteDefaultConsumption() + { + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + public function testShouldExecuteCustomConsumption() + { + $defaultConsumer = $this->createQueueConsumerMock(); + $defaultConsumer + ->expects($this->never()) + ->method('consume') + ; + + $customConsumer = $this->createQueueConsumerMock(); + $customConsumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $defaultConsumer, + 'enqueue.transport.custom.queue_consumer' => $customConsumer, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute(['--transport' => 'custom']); + } + + public function testThrowIfNotDefinedTransportRequested() + { + $defaultConsumer = $this->createQueueConsumerMock(); + $defaultConsumer + ->expects($this->never()) + ->method('consume') + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $defaultConsumer, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Transport "not-defined" is not supported.'); + $tester->execute(['--transport' => 'not-defined']); + } + + public function testShouldReturnExitStatusIfSet() + { + $testExitCode = 678; + + $stubExtension = $this->createExtension(); + + $stubExtension + ->expects($this->once()) + ->method('onStart') + ->with($this->isInstanceOf(Start::class)) + ->willReturnCallback(function (Start $context) use ($testExitCode) { + $context->interruptExecution($testExitCode); + }) + ; + + $consumer = new QueueConsumer($this->createContextStub(), $stubExtension); + + $command = new ConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $this->assertEquals($testExitCode, $tester->getStatusCode()); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface + */ + private function createQueueConsumerMock() + { + return $this->createMock(QueueConsumerInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createContextWithoutSubscriptionConsumerMock(): InteropContext + { + $contextMock = $this->createMock(InteropContext::class); + $contextMock + ->expects($this->any()) + ->method('createSubscriptionConsumer') + ->willThrowException(SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt()) + ; + + return $contextMock; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|InteropContext + */ + private function createContextStub(?Consumer $consumer = null): InteropContext + { + $context = $this->createContextWithoutSubscriptionConsumerMock(); + $context + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $queueName) { + return new NullQueue($queueName); + }) + ; + $context + ->expects($this->any()) + ->method('createConsumer') + ->willReturnCallback(function (Queue $queue) use ($consumer) { + return $consumer ?: $this->createConsumerStub($queue); + }) + ; + + return $context; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|ExtensionInterface + */ + private function createExtension() + { + return $this->createMock(ExtensionInterface::class); + } + + /** + * @param mixed|null $queue + * + * @return \PHPUnit\Framework\MockObject\MockObject|Consumer + */ + private function createConsumerStub($queue = null): Consumer + { + if (null === $queue) { + $queue = 'queue'; + } + if (is_string($queue)) { + $queue = new NullQueue($queue); + } + + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue) + ; + + return $consumerMock; + } +} diff --git a/Tests/Symfony/Consumption/ConsumeMessagesCommandTest.php b/Tests/Symfony/Consumption/ConsumeMessagesCommandTest.php deleted file mode 100644 index d83505a..0000000 --- a/Tests/Symfony/Consumption/ConsumeMessagesCommandTest.php +++ /dev/null @@ -1,85 +0,0 @@ -createQueueConsumerMock()); - } - - public function testShouldHaveCommandName() - { - $command = new ConsumeMessagesCommand($this->createQueueConsumerMock()); - - $this->assertEquals('enqueue:transport:consume', $command->getName()); - } - - public function testShouldHaveExpectedOptions() - { - $command = new ConsumeMessagesCommand($this->createQueueConsumerMock()); - - $options = $command->getDefinition()->getOptions(); - - $this->assertCount(5, $options); - $this->assertArrayHasKey('memory-limit', $options); - $this->assertArrayHasKey('message-limit', $options); - $this->assertArrayHasKey('time-limit', $options); - $this->assertArrayHasKey('idle-timeout', $options); - $this->assertArrayHasKey('receive-timeout', $options); - } - - public function testShouldHaveExpectedAttributes() - { - $command = new ConsumeMessagesCommand($this->createQueueConsumerMock()); - - $arguments = $command->getDefinition()->getArguments(); - - $this->assertCount(0, $arguments); - } - - public function testShouldExecuteConsumption() - { - $context = $this->createContextMock(); - $context - ->expects($this->never()) - ->method('close') - ; - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->once()) - ->method('consume') - ->with($this->isInstanceOf(ChainExtension::class)) - ; - - $command = new ConsumeMessagesCommand($consumer); - - $tester = new CommandTester($command); - $tester->execute([]); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext - */ - private function createContextMock() - { - return $this->createMock(PsrContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|QueueConsumer - */ - private function createQueueConsumerMock() - { - return $this->createMock(QueueConsumer::class); - } -} diff --git a/Tests/Symfony/Consumption/ContainerAwareConsumeMessagesCommandTest.php b/Tests/Symfony/Consumption/ContainerAwareConsumeMessagesCommandTest.php deleted file mode 100644 index b451dc7..0000000 --- a/Tests/Symfony/Consumption/ContainerAwareConsumeMessagesCommandTest.php +++ /dev/null @@ -1,207 +0,0 @@ -createQueueConsumerMock()); - } - - public function testShouldHaveCommandName() - { - $command = new ContainerAwareConsumeMessagesCommand($this->createQueueConsumerMock()); - - $this->assertEquals('enqueue:transport:consume', $command->getName()); - } - - public function testShouldHaveExpectedOptions() - { - $command = new ContainerAwareConsumeMessagesCommand($this->createQueueConsumerMock()); - - $options = $command->getDefinition()->getOptions(); - - $this->assertCount(6, $options); - $this->assertArrayHasKey('memory-limit', $options); - $this->assertArrayHasKey('message-limit', $options); - $this->assertArrayHasKey('time-limit', $options); - $this->assertArrayHasKey('queue', $options); - $this->assertArrayHasKey('idle-timeout', $options); - $this->assertArrayHasKey('receive-timeout', $options); - } - - public function testShouldHaveExpectedAttributes() - { - $command = new ContainerAwareConsumeMessagesCommand($this->createQueueConsumerMock()); - - $arguments = $command->getDefinition()->getArguments(); - - $this->assertCount(1, $arguments); - $this->assertArrayHasKey('processor-service', $arguments); - } - - public function testShouldThrowExceptionIfProcessorInstanceHasWrongClass() - { - $container = new Container(); - $container->set('processor-service', new \stdClass()); - - $command = new ContainerAwareConsumeMessagesCommand($this->createQueueConsumerMock()); - $command->setContainer($container); - - $tester = new CommandTester($command); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Invalid message processor service given. It must be an instance of Interop\Queue\PsrProcessor but stdClass'); - $tester->execute([ - 'processor-service' => 'processor-service', - '--queue' => ['queue-name'], - ]); - } - - public function testThrowIfNeitherQueueOptionNorProcessorImplementsQueueSubscriberInterface() - { - $processor = $this->createProcessor(); - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->never()) - ->method('bind') - ; - $consumer - ->expects($this->never()) - ->method('consume') - ; - - $container = new Container(); - $container->set('processor-service', $processor); - - $command = new ContainerAwareConsumeMessagesCommand($consumer); - $command->setContainer($container); - - $tester = new CommandTester($command); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The queues are not provided. The processor must implement "Enqueue\Consumption\QueueSubscriberInterface" interface and it must return not empty array of queues or queues set using --queue option.'); - $tester->execute([ - 'processor-service' => 'processor-service', - ]); - } - - public function testShouldExecuteConsumptionWithExplicitlySetQueueViaQueueOption() - { - $processor = $this->createProcessor(); - - $context = $this->createContextMock(); - $context - ->expects($this->never()) - ->method('close') - ; - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->once()) - ->method('bind') - ->with('queue-name', $this->identicalTo($processor)) - ; - $consumer - ->expects($this->once()) - ->method('consume') - ->with($this->isInstanceOf(ChainExtension::class)) - ; - - $container = new Container(); - $container->set('processor-service', $processor); - - $command = new ContainerAwareConsumeMessagesCommand($consumer); - $command->setContainer($container); - - $tester = new CommandTester($command); - $tester->execute([ - 'processor-service' => 'processor-service', - '--queue' => ['queue-name'], - ]); - } - - public function testShouldExecuteConsumptionWhenProcessorImplementsQueueSubscriberInterface() - { - $processor = new QueueSubscriberProcessor(); - - $context = $this->createContextMock(); - $context - ->expects($this->never()) - ->method('close') - ; - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->at(0)) - ->method('bind') - ->with('fooSubscribedQueues', $this->identicalTo($processor)) - ; - $consumer - ->expects($this->at(1)) - ->method('bind') - ->with('barSubscribedQueues', $this->identicalTo($processor)) - ; - $consumer - ->expects($this->at(2)) - ->method('consume') - ->with($this->isInstanceOf(ChainExtension::class)) - ; - - $container = new Container(); - $container->set('processor-service', $processor); - - $command = new ContainerAwareConsumeMessagesCommand($consumer); - $command->setContainer($container); - - $tester = new CommandTester($command); - $tester->execute([ - 'processor-service' => 'processor-service', - ]); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext - */ - protected function createContextMock() - { - return $this->createMock(PsrContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrQueue - */ - protected function createQueueMock() - { - return $this->createMock(PsrQueue::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor - */ - protected function createProcessor() - { - return $this->createMock(PsrProcessor::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|QueueConsumer - */ - protected function createQueueConsumerMock() - { - return $this->createMock(QueueConsumer::class); - } -} diff --git a/Tests/Symfony/Consumption/LimitsExtensionsCommandTraitTest.php b/Tests/Symfony/Consumption/LimitsExtensionsCommandTraitTest.php index d9704d3..f47a321 100644 --- a/Tests/Symfony/Consumption/LimitsExtensionsCommandTraitTest.php +++ b/Tests/Symfony/Consumption/LimitsExtensionsCommandTraitTest.php @@ -5,6 +5,7 @@ use Enqueue\Consumption\Extension\LimitConsumedMessagesExtension; use Enqueue\Consumption\Extension\LimitConsumerMemoryExtension; use Enqueue\Consumption\Extension\LimitConsumptionTimeExtension; +use Enqueue\Consumption\Extension\NicenessExtension; use Enqueue\Tests\Symfony\Consumption\Mock\LimitsExtensionsCommand; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; @@ -17,10 +18,11 @@ public function testShouldAddExtensionsOptions() $options = $trait->getDefinition()->getOptions(); - $this->assertCount(3, $options); + $this->assertCount(4, $options); $this->assertArrayHasKey('memory-limit', $options); $this->assertArrayHasKey('message-limit', $options); $this->assertArrayHasKey('time-limit', $options); + $this->assertArrayHasKey('niceness', $options); } public function testShouldAddMessageLimitExtension() @@ -57,7 +59,8 @@ public function testShouldAddTimeLimitExtension() public function testShouldThrowExceptionIfTimeLimitExpressionIsNotValid() { - $this->setExpectedException(\Exception::class, 'Failed to parse time string (time is not valid) at position'); + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Failed to parse time string (time is not valid) at position'); $command = new LimitsExtensionsCommand('name'); @@ -104,4 +107,36 @@ public function testShouldAddThreeLimitExtensions() $this->assertInstanceOf(LimitConsumptionTimeExtension::class, $result[1]); $this->assertInstanceOf(LimitConsumerMemoryExtension::class, $result[2]); } + + /** + * @dataProvider provideNicenessValues + */ + public function testShouldAddNicenessExtension($inputValue, bool $enabled) + { + $command = new LimitsExtensionsCommand('name'); + $tester = new CommandTester($command); + $tester->execute([ + '--niceness' => $inputValue, + ]); + + $result = $command->getExtensions(); + + if ($enabled) { + $this->assertCount(1, $result); + $this->assertInstanceOf(NicenessExtension::class, $result[0]); + } else { + $this->assertEmpty($result); + } + } + + public function provideNicenessValues(): \Generator + { + yield [1, true]; + yield ['1', true]; + yield [-1.0, true]; + yield ['100', true]; + yield ['', false]; + yield ['0', false]; + yield [0.0, false]; + } } diff --git a/Tests/Symfony/Consumption/Mock/LimitsExtensionsCommand.php b/Tests/Symfony/Consumption/Mock/LimitsExtensionsCommand.php index 7b67223..05e0c56 100644 --- a/Tests/Symfony/Consumption/Mock/LimitsExtensionsCommand.php +++ b/Tests/Symfony/Consumption/Mock/LimitsExtensionsCommand.php @@ -25,8 +25,10 @@ protected function configure() $this->configureLimitsExtensions(); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->extensions = $this->getLimitsExtensions($input, $output); + + return 0; } } diff --git a/Tests/Symfony/Consumption/Mock/QueueConsumerOptionsCommand.php b/Tests/Symfony/Consumption/Mock/QueueConsumerOptionsCommand.php index 88a0d8c..147a3b9 100644 --- a/Tests/Symfony/Consumption/Mock/QueueConsumerOptionsCommand.php +++ b/Tests/Symfony/Consumption/Mock/QueueConsumerOptionsCommand.php @@ -2,7 +2,7 @@ namespace Enqueue\Tests\Symfony\Consumption\Mock; -use Enqueue\Consumption\QueueConsumer; +use Enqueue\Consumption\QueueConsumerInterface; use Enqueue\Symfony\Consumption\QueueConsumerOptionsCommandTrait; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,11 +13,11 @@ class QueueConsumerOptionsCommand extends Command use QueueConsumerOptionsCommandTrait; /** - * @var QueueConsumer + * @var QueueConsumerInterface */ private $consumer; - public function __construct(QueueConsumer $consumer) + public function __construct(QueueConsumerInterface $consumer) { parent::__construct('queue-consumer-options'); @@ -31,8 +31,10 @@ protected function configure() $this->configureQueueConsumerOptions(); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->setQueueConsumerOptions($this->consumer, $input); + + return 0; } } diff --git a/Tests/Symfony/Consumption/Mock/QueueSubscriberProcessor.php b/Tests/Symfony/Consumption/Mock/QueueSubscriberProcessor.php index e71ad16..a210b0e 100644 --- a/Tests/Symfony/Consumption/Mock/QueueSubscriberProcessor.php +++ b/Tests/Symfony/Consumption/Mock/QueueSubscriberProcessor.php @@ -3,14 +3,15 @@ namespace Enqueue\Tests\Symfony\Consumption\Mock; use Enqueue\Consumption\QueueSubscriberInterface; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message as InteropMessage; +use Interop\Queue\Processor; -class QueueSubscriberProcessor implements PsrProcessor, QueueSubscriberInterface +class QueueSubscriberProcessor implements Processor, QueueSubscriberInterface { - public function process(PsrMessage $message, PsrContext $context) + public function process(InteropMessage $message, Context $context) { + return self::ACK; } public static function getSubscribedQueues() diff --git a/Tests/Symfony/Consumption/QueueConsumerOptionsCommandTraitTest.php b/Tests/Symfony/Consumption/QueueConsumerOptionsCommandTraitTest.php index 57106aa..b44c89a 100644 --- a/Tests/Symfony/Consumption/QueueConsumerOptionsCommandTraitTest.php +++ b/Tests/Symfony/Consumption/QueueConsumerOptionsCommandTraitTest.php @@ -2,7 +2,7 @@ namespace Enqueue\Tests\Symfony\Consumption; -use Enqueue\Consumption\QueueConsumer; +use Enqueue\Consumption\QueueConsumerInterface; use Enqueue\Tests\Symfony\Consumption\Mock\QueueConsumerOptionsCommand; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; @@ -15,19 +15,13 @@ public function testShouldAddExtensionsOptions() $options = $trait->getDefinition()->getOptions(); - $this->assertCount(2, $options); - $this->assertArrayHasKey('idle-timeout', $options); + $this->assertCount(1, $options); $this->assertArrayHasKey('receive-timeout', $options); } public function testShouldSetQueueConsumerOptions() { $consumer = $this->createQueueConsumer(); - $consumer - ->expects($this->once()) - ->method('setIdleTimeout') - ->with($this->identicalTo(123)) - ; $consumer ->expects($this->once()) ->method('setReceiveTimeout') @@ -38,16 +32,15 @@ public function testShouldSetQueueConsumerOptions() $tester = new CommandTester($trait); $tester->execute([ - '--idle-timeout' => '123', '--receive-timeout' => '456', ]); } /** - * @return QueueConsumer|\PHPUnit_Framework_MockObject_MockObject|QueueConsumer + * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface */ private function createQueueConsumer() { - return $this->createMock(QueueConsumer::class); + return $this->createMock(QueueConsumerInterface::class); } } diff --git a/Tests/Symfony/Consumption/SimpleConsumeCommandTest.php b/Tests/Symfony/Consumption/SimpleConsumeCommandTest.php new file mode 100644 index 0000000..eeb38bf --- /dev/null +++ b/Tests/Symfony/Consumption/SimpleConsumeCommandTest.php @@ -0,0 +1,74 @@ +assertClassExtends(ConsumeCommand::class, SimpleConsumeCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(SimpleConsumeCommand::class); + } + + public function testShouldHaveExpectedOptions() + { + $command = new SimpleConsumeCommand($this->createQueueConsumerMock()); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(7, $options); + $this->assertArrayHasKey('memory-limit', $options); + $this->assertArrayHasKey('message-limit', $options); + $this->assertArrayHasKey('time-limit', $options); + $this->assertArrayHasKey('receive-timeout', $options); + $this->assertArrayHasKey('niceness', $options); + $this->assertArrayHasKey('transport', $options); + $this->assertArrayHasKey('logger', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new SimpleConsumeCommand($this->createQueueConsumerMock()); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(0, $arguments); + } + + public function testShouldExecuteDefaultConsumption() + { + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new SimpleConsumeCommand($consumer); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface + */ + private function createQueueConsumerMock() + { + return $this->createMock(QueueConsumerInterface::class); + } +} diff --git a/Tests/Symfony/ContainerProcessorRegistryTest.php b/Tests/Symfony/ContainerProcessorRegistryTest.php new file mode 100644 index 0000000..5504e8e --- /dev/null +++ b/Tests/Symfony/ContainerProcessorRegistryTest.php @@ -0,0 +1,107 @@ +assertClassImplements(ProcessorRegistryInterface::class, ContainerProcessorRegistry::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(ContainerProcessorRegistry::class); + } + + public function testShouldAllowGetProcessor() + { + $processorMock = $this->createProcessorMock(); + + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->once()) + ->method('has') + ->with('processor-name') + ->willReturn(true) + ; + $containerMock + ->expects($this->once()) + ->method('get') + ->with('processor-name') + ->willReturn($processorMock) + ; + + $registry = new ContainerProcessorRegistry($containerMock); + $this->assertSame($processorMock, $registry->get('processor-name')); + } + + public function testThrowErrorIfServiceDoesNotImplementProcessorReturnType() + { + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->once()) + ->method('has') + ->with('processor-name') + ->willReturn(true) + ; + $containerMock + ->expects($this->once()) + ->method('get') + ->with('processor-name') + ->willReturn(new \stdClass()) + ; + + $registry = new ContainerProcessorRegistry($containerMock); + + $this->expectException(\TypeError::class); + // Exception messages vary slightly between versions + $this->expectExceptionMessageMatches( + '/Enqueue\\\\Symfony\\\\ContainerProcessorRegistry::get\(\).+ Interop\\\\Queue\\\\Processor,.*stdClass returned/' + ); + + $registry->get('processor-name'); + } + + public function testShouldThrowExceptionIfProcessorIsNotSet() + { + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->once()) + ->method('has') + ->with('processor-name') + ->willReturn(false) + ; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service locator does not have a processor with name "processor-name".'); + + $registry = new ContainerProcessorRegistry($containerMock); + $registry->get('processor-name'); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createProcessorMock(): Processor + { + return $this->createMock(Processor::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createContainerMock(): ContainerInterface + { + return $this->createMock(ContainerInterface::class); + } +} diff --git a/Tests/Symfony/DefaultTransportFactoryTest.php b/Tests/Symfony/DefaultTransportFactoryTest.php deleted file mode 100644 index 9a013fd..0000000 --- a/Tests/Symfony/DefaultTransportFactoryTest.php +++ /dev/null @@ -1,293 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, DefaultTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new DefaultTransportFactory(); - - $this->assertEquals('default', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new DefaultTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfigurationAsAliasAsString() - { - $transport = new DefaultTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['the_alias']); - - $this->assertEquals(['alias' => 'the_alias'], $config); - } - - public function testShouldAllowAddConfigurationAsAliasAsOption() - { - $transport = new DefaultTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [['alias' => 'the_alias']]); - - $this->assertEquals(['alias' => 'the_alias'], $config); - } - - public function testShouldAllowAddConfigurationAsDsn() - { - $transport = new DefaultTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['dsn://']); - - $this->assertEquals(['dsn' => 'dsn://'], $config); - } - - /** - * @see https://github.com/php-enqueue/enqueue-dev/issues/356 - * - * @group bug - */ - public function testShouldAllowAddConfigurationAsDsnWithoutSlashes() - { - $transport = new DefaultTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['dsn:']); - - $this->assertEquals(['dsn' => 'dsn:'], $config); - } - - public function testShouldSetNullTransportByDefault() - { - $transport = new DefaultTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $config = $processor->process($tb->buildTree(), [null]); - $this->assertEquals(['dsn' => 'null:'], $config); - - $config = $processor->process($tb->buildTree(), ['']); - $this->assertEquals(['dsn' => 'null:'], $config); - } - - public function testThrowIfNeitherDsnNorAliasConfigured() - { - $transport = new DefaultTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Either dsn or alias option must be set'); - $processor->process($tb->buildTree(), [[]]); - } - - public function testShouldCreateConnectionFactoryFromAlias() - { - $container = new ContainerBuilder(); - - $transport = new DefaultTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, ['alias' => 'foo']); - - $this->assertEquals('enqueue.transport.default.connection_factory', $serviceId); - - $this->assertTrue($container->hasAlias('enqueue.transport.default.connection_factory')); - $this->assertEquals( - 'enqueue.transport.foo.connection_factory', - (string) $container->getAlias('enqueue.transport.default.connection_factory') - ); - - $this->assertTrue($container->hasAlias('enqueue.transport.connection_factory')); - $this->assertEquals( - 'enqueue.transport.default.connection_factory', - (string) $container->getAlias('enqueue.transport.connection_factory') - ); - } - - public function testShouldCreateContextFromAlias() - { - $container = new ContainerBuilder(); - - $transport = new DefaultTransportFactory(); - - $serviceId = $transport->createContext($container, ['alias' => 'the_alias']); - - $this->assertEquals('enqueue.transport.default.context', $serviceId); - - $this->assertTrue($container->hasAlias($serviceId)); - $context = $container->getAlias($serviceId); - $this->assertEquals('enqueue.transport.the_alias.context', (string) $context); - - $this->assertTrue($container->hasAlias('enqueue.transport.context')); - $context = $container->getAlias('enqueue.transport.context'); - $this->assertEquals($serviceId, (string) $context); - } - - public function testShouldCreateDriverFromAlias() - { - $container = new ContainerBuilder(); - - $transport = new DefaultTransportFactory(); - - $driverId = $transport->createDriver($container, ['alias' => 'the_alias']); - - $this->assertEquals('enqueue.client.default.driver', $driverId); - - $this->assertTrue($container->hasAlias($driverId)); - $context = $container->getAlias($driverId); - $this->assertEquals('enqueue.client.the_alias.driver', (string) $context); - - $this->assertTrue($container->hasAlias('enqueue.client.driver')); - $context = $container->getAlias('enqueue.client.driver'); - $this->assertEquals($driverId, (string) $context); - } - - /** - * @dataProvider provideDSNs - * - * @param mixed $dsn - * @param mixed $expectedName - */ - public function testShouldCreateConnectionFactoryFromDSN($dsn, $expectedName) - { - $container = new ContainerBuilder(); - - $transport = new DefaultTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, ['dsn' => $dsn]); - - $this->assertEquals('enqueue.transport.default.connection_factory', $serviceId); - - $this->assertTrue($container->hasAlias('enqueue.transport.default.connection_factory')); - $this->assertEquals( - sprintf('enqueue.transport.%s.connection_factory', $expectedName), - (string) $container->getAlias('enqueue.transport.default.connection_factory') - ); - - $this->assertTrue($container->hasAlias('enqueue.transport.connection_factory')); - $this->assertEquals( - 'enqueue.transport.default.connection_factory', - (string) $container->getAlias('enqueue.transport.connection_factory') - ); - } - - /** - * @dataProvider provideDSNs - * - * @param mixed $dsn - * @param mixed $expectedName - */ - public function testShouldCreateContextFromDsn($dsn, $expectedName) - { - $container = new ContainerBuilder(); - - $transport = new DefaultTransportFactory(); - - $serviceId = $transport->createContext($container, ['dsn' => $dsn]); - - $this->assertEquals('enqueue.transport.default.context', $serviceId); - - $this->assertTrue($container->hasAlias($serviceId)); - $context = $container->getAlias($serviceId); - $this->assertEquals( - sprintf('enqueue.transport.%s.context', $expectedName), - (string) $context - ); - - $this->assertTrue($container->hasAlias('enqueue.transport.context')); - $context = $container->getAlias('enqueue.transport.context'); - $this->assertEquals($serviceId, (string) $context); - } - - /** - * @dataProvider provideDSNs - * - * @param mixed $dsn - * @param mixed $expectedName - */ - public function testShouldCreateDriverFromDsn($dsn, $expectedName) - { - $container = new ContainerBuilder(); - - $transport = new DefaultTransportFactory(); - - $driverId = $transport->createDriver($container, ['dsn' => $dsn]); - - $this->assertEquals('enqueue.client.default.driver', $driverId); - - $this->assertTrue($container->hasAlias($driverId)); - $context = $container->getAlias($driverId); - $this->assertEquals( - sprintf('enqueue.client.%s.driver', $expectedName), - (string) $context - ); - - $this->assertTrue($container->hasAlias('enqueue.client.driver')); - $context = $container->getAlias('enqueue.client.driver'); - $this->assertEquals($driverId, (string) $context); - } - - public static function provideDSNs() - { - yield ['amqp+ext:', 'default_amqp']; - - yield ['amqp+lib:', 'default_amqp']; - - yield ['amqp+bunny:', 'default_amqp']; - - yield ['null:', 'default_null']; - - yield ['file:', 'default_fs']; - - yield ['mysql:', 'default_dbal']; - - yield ['pgsql:', 'default_dbal']; - - yield ['gps:', 'default_gps']; - - yield ['sqs:', 'default_sqs']; - - yield ['redis:', 'default_redis']; - - yield ['stomp:', 'default_stomp']; - } -} diff --git a/Tests/Symfony/DependencyInjection/BuildConsumptionExtensionsPassTest.php b/Tests/Symfony/DependencyInjection/BuildConsumptionExtensionsPassTest.php new file mode 100644 index 0000000..bdccd33 --- /dev/null +++ b/Tests/Symfony/DependencyInjection/BuildConsumptionExtensionsPassTest.php @@ -0,0 +1,283 @@ +assertClassImplements(CompilerPassInterface::class, BuildConsumptionExtensionsPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildConsumptionExtensionsPass::class); + } + + public function testThrowIfEnqueueTransportsParameterNotSet() + { + $pass = new BuildConsumptionExtensionsPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.transports" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoConsumptionExtensionsServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo', 'bar']); + $container->setParameter('enqueue.default_transport', 'foo'); + + $pass = new BuildConsumptionExtensionsPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.transport.foo.consumption_extensions" not found'); + $pass->process($container); + } + + public function testShouldRegisterTransportExtension() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'foo']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + new Reference('aBarExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldIgnoreOtherTransportExtensions() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'anotherName']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldAddExtensionIfTransportAll() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'all']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'anotherName']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldTreatTagsWithoutTransportAsDefaultTransport() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension') + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension') + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + new Reference('aBarExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldOrderExtensionsByPriority() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + + $extensions = new Definition(); + $extensions->addArgument([]); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $extension = new Definition(); + $extension->addTag('enqueue.transport.consumption_extension', ['priority' => 6]); + $container->setDefinition('foo_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.transport.consumption_extension', ['priority' => -5]); + $container->setDefinition('bar_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.transport.consumption_extension', ['priority' => 2]); + $container->setDefinition('baz_extension', $extension); + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + $orderedExtensions = $extensions->getArgument(0); + + $this->assertCount(3, $orderedExtensions); + $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[0]); + $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[1]); + $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[2]); + } + + public function testShouldAssumePriorityZeroIfPriorityIsNotSet() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + + $extensions = new Definition(); + $extensions->addArgument([]); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $extension = new Definition(); + $extension->addTag('enqueue.transport.consumption_extension'); + $container->setDefinition('foo_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.transport.consumption_extension', ['priority' => 1]); + $container->setDefinition('bar_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.transport.consumption_extension', ['priority' => -1]); + $container->setDefinition('baz_extension', $extension); + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + $orderedExtensions = $extensions->getArgument(0); + + $this->assertCount(3, $orderedExtensions); + $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[0]); + $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[1]); + $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[2]); + } + + public function testShouldMergeWithAddedPreviously() + { + $extensions = new Definition(); + $extensions->addArgument([ + 'aBarExtension' => 'aBarServiceIdAddedPreviously', + 'aOloloExtension' => 'aOloloServiceIdAddedPreviously', + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension') + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension') + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertCount(4, $extensions->getArgument(0)); + } + + public function testShouldRegisterProcessorWithMatchedNameToCorrespondingRegistries() + { + $fooExtensions = new Definition(); + $fooExtensions->addArgument([]); + + $barExtensions = new Definition(); + $barExtensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo', 'bar']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $fooExtensions); + $container->setDefinition('enqueue.transport.bar.consumption_extensions', $barExtensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'bar']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($fooExtensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $fooExtensions->getArgument(0)); + + self::assertIsArray($barExtensions->getArgument(0)); + $this->assertEquals([ + new Reference('aBarExtension'), + ], $barExtensions->getArgument(0)); + } +} diff --git a/Tests/Symfony/DependencyInjection/BuildProcessorRegistryPassTest.php b/Tests/Symfony/DependencyInjection/BuildProcessorRegistryPassTest.php new file mode 100644 index 0000000..134c216 --- /dev/null +++ b/Tests/Symfony/DependencyInjection/BuildProcessorRegistryPassTest.php @@ -0,0 +1,214 @@ +assertClassImplements(CompilerPassInterface::class, BuildProcessorRegistryPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildProcessorRegistryPass::class); + } + + public function testThrowIfEnqueueTransportsParameterNotSet() + { + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.transports" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoRegistryServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo', 'bar']); + $container->setParameter('enqueue.default_transport', 'baz'); + + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.transport.foo.processor_registry" not found'); + $pass->process($container); + } + + public function testShouldRegisterProcessorWithMatchedName() + { + $registry = new Definition(ProcessorRegistryInterface::class); + $registry->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.processor_registry', $registry); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'foo']) + ; + $container->register('aProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'bar']) + ; + + $pass = new BuildProcessorRegistryPass(); + + $pass->process($container); + + $this->assertInstanceOf(Reference::class, $registry->getArgument(0)); + + $this->assertLocatorServices($container, $registry->getArgument(0), [ + 'aFooProcessor' => 'aFooProcessor', + ]); + } + + public function testShouldRegisterProcessorWithMatchedNameToCorrespondingRegistries() + { + $fooRegistry = new Definition(ProcessorRegistryInterface::class); + $fooRegistry->addArgument([]); + + $barRegistry = new Definition(ProcessorRegistryInterface::class); + $barRegistry->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo', 'bar']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.processor_registry', $fooRegistry); + $container->setDefinition('enqueue.transport.bar.processor_registry', $barRegistry); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'foo']) + ; + $container->register('aBarProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'bar']) + ; + + $pass = new BuildProcessorRegistryPass(); + + $pass->process($container); + + $this->assertInstanceOf(Reference::class, $fooRegistry->getArgument(0)); + $this->assertLocatorServices($container, $fooRegistry->getArgument(0), [ + 'aFooProcessor' => 'aFooProcessor', + ]); + + $this->assertInstanceOf(Reference::class, $barRegistry->getArgument(0)); + $this->assertLocatorServices($container, $barRegistry->getArgument(0), [ + 'aBarProcessor' => 'aBarProcessor', + ]); + } + + public function testShouldRegisterProcessorWithoutNameToDefaultTransport() + { + $registry = new Definition(ProcessorRegistryInterface::class); + $registry->addArgument(null); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.processor_registry', $registry); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', []) + ; + $container->register('aProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'bar']) + ; + + $pass = new BuildProcessorRegistryPass(); + + $pass->process($container); + + $this->assertInstanceOf(Reference::class, $registry->getArgument(0)); + + $this->assertLocatorServices($container, $registry->getArgument(0), [ + 'aFooProcessor' => 'aFooProcessor', + ]); + } + + public function testShouldRegisterProcessorIfTransportNameEqualsAll() + { + $registry = new Definition(ProcessorRegistryInterface::class); + $registry->addArgument(null); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.processor_registry', $registry); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'all']) + ; + $container->register('aProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'bar']) + ; + + $pass = new BuildProcessorRegistryPass(); + + $pass->process($container); + + $this->assertInstanceOf(Reference::class, $registry->getArgument(0)); + + $this->assertLocatorServices($container, $registry->getArgument(0), [ + 'aFooProcessor' => 'aFooProcessor', + ]); + } + + public function testShouldRegisterWithCustomProcessorName() + { + $registry = new Definition(ProcessorRegistryInterface::class); + $registry->addArgument(null); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.processor_registry', $registry); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['processor' => 'customProcessorName']) + ; + + $pass = new BuildProcessorRegistryPass(); + + $pass->process($container); + + $this->assertInstanceOf(Reference::class, $registry->getArgument(0)); + + $this->assertLocatorServices($container, $registry->getArgument(0), [ + 'customProcessorName' => 'aFooProcessor', + ]); + } + + private function assertLocatorServices(ContainerBuilder $container, $locatorId, array $locatorServices) + { + $this->assertInstanceOf(Reference::class, $locatorId); + $locatorId = (string) $locatorId; + + $this->assertTrue($container->hasDefinition($locatorId)); + $this->assertMatchesRegularExpression('/\.?service_locator\..*?\.enqueue\./', $locatorId); + + $match = []; + if (false == preg_match('/(\.?service_locator\..*?)\.enqueue\./', $locatorId, $match)) { + $this->fail('preg_match should not failed'); + } + + $this->assertTrue($container->hasDefinition($match[1])); + $locator = $container->getDefinition($match[1]); + + $this->assertContainsOnly(ServiceClosureArgument::class, $locator->getArgument(0)); + $actualServices = array_map(function (ServiceClosureArgument $value) { + return (string) $value->getValues()[0]; + }, $locator->getArgument(0)); + + $this->assertEquals($locatorServices, $actualServices); + } +} diff --git a/Tests/Symfony/DependencyInjection/TransportFactoryTest.php b/Tests/Symfony/DependencyInjection/TransportFactoryTest.php new file mode 100644 index 0000000..9094074 --- /dev/null +++ b/Tests/Symfony/DependencyInjection/TransportFactoryTest.php @@ -0,0 +1,478 @@ +assertClassFinal(TransportFactory::class); + } + + public function testThrowIfEmptyNameGivenOnConstruction() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The name could not be empty.'); + + new TransportFactory(''); + } + + public function testShouldAllowAddConfigurationAsStringDsn() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + $config = $processor->process($tb->buildTree(), [['transport' => 'dsn://']]); + + $this->assertEquals([ + 'transport' => [ + 'dsn' => 'dsn://', + ], + ], $config); + } + + /** + * @see https://github.com/php-enqueue/enqueue-dev/issues/356 + * + * @group bug + */ + public function testShouldAllowAddConfigurationAsDsnWithoutSlashes() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + $config = $processor->process($tb->buildTree(), [['transport' => 'dsn:']]); + + $this->assertEquals([ + 'transport' => [ + 'dsn' => 'dsn:', + ], + ], $config); + } + + public function testShouldSetNullTransportIfNullGiven() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + $config = $processor->process($tb->buildTree(), [['transport' => null]]); + + $this->assertEquals([ + 'transport' => [ + 'dsn' => 'null:', + ], + ], $config); + } + + public function testShouldSetNullTransportIfEmptyStringGiven() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + $config = $processor->process($tb->buildTree(), [['transport' => '']]); + + $this->assertEquals([ + 'transport' => [ + 'dsn' => 'null:', + ], + ], $config); + } + + public function testShouldSetNullTransportIfEmptyArrayGiven() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + $config = $processor->process($tb->buildTree(), [['transport' => []]]); + + $this->assertEquals([ + 'transport' => [ + 'dsn' => 'null:', + ], + ], $config); + } + + public function testThrowIfEmptyDsnGiven() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The path "foo.transport.dsn" cannot contain an empty value, but got "".'); + $processor->process($tb->buildTree(), [['transport' => ['dsn' => '']]]); + } + + public function testThrowIfFactoryClassAndFactoryServiceSetAtTheSameTime() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Both options factory_class and factory_service are set. Please choose one.'); + $processor->process($tb->buildTree(), [[ + 'transport' => [ + 'dsn' => 'foo:', + 'factory_class' => 'aFactoryClass', + 'factory_service' => 'aFactoryService', + ], ]]); + } + + public function testThrowIfConnectionFactoryClassUsedWithFactoryClassAtTheSameTime() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The option connection_factory_class must not be used with factory_class or factory_service at the same time. Please choose one.'); + $processor->process($tb->buildTree(), [[ + 'transport' => [ + 'dsn' => 'foo:', + 'connection_factory_class' => 'aFactoryClass', + 'factory_service' => 'aFactoryService', + ], ]]); + } + + public function testThrowIfConnectionFactoryClassUsedWithFactoryServiceAtTheSameTime() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + $processor = new Processor(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The option connection_factory_class must not be used with factory_class or factory_service at the same time. Please choose one.'); + $processor->process($tb->buildTree(), [[ + 'transport' => [ + 'dsn' => 'foo:', + 'connection_factory_class' => 'aFactoryClass', + 'factory_service' => 'aFactoryService', + ], ]]); + } + + public function testShouldAllowSetFactoryClass() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + $processor = new Processor(); + + $config = $processor->process($tb->buildTree(), [[ + 'transport' => [ + 'dsn' => 'foo:', + 'factory_class' => 'theFactoryClass', + ], ]]); + + $this->assertArrayHasKey('factory_class', $config['transport']); + $this->assertSame('theFactoryClass', $config['transport']['factory_class']); + } + + public function testShouldAllowSetFactoryService() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + $processor = new Processor(); + + $config = $processor->process($tb->buildTree(), [[ + 'transport' => [ + 'dsn' => 'foo:', + 'factory_service' => 'theFactoryService', + ], ]]); + + $this->assertArrayHasKey('factory_service', $config['transport']); + $this->assertSame('theFactoryService', $config['transport']['factory_service']); + } + + public function testShouldAllowSetConnectionFactoryClass() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + $processor = new Processor(); + + $config = $processor->process($tb->buildTree(), [[ + 'transport' => [ + 'dsn' => 'foo:', + 'connection_factory_class' => 'theFactoryClass', + ], ]]); + + $this->assertArrayHasKey('connection_factory_class', $config['transport']); + $this->assertSame('theFactoryClass', $config['transport']['connection_factory_class']); + } + + public function testThrowIfExtraOptionGiven() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + $processor = new Processor(); + + $config = $processor->process($tb->buildTree(), [['transport' => ['dsn' => 'foo:', 'extraOption' => 'aVal']]]); + $this->assertEquals([ + 'transport' => [ + 'dsn' => 'foo:', + 'extraOption' => 'aVal', + ], ], $config + ); + } + + public function testShouldBuildConnectionFactoryFromDSN() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $config = [ + 'dsn' => 'foo://bar/baz', + 'connection_factory_class' => null, + 'factory_service' => null, + 'factory_class' => null, + ]; + + $transport->buildConnectionFactory($container, $config); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory')); + + $this->assertNotEmpty($container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()); + $this->assertEquals( + [new Reference('enqueue.transport.default.connection_factory_factory'), 'create'], + $container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()) + ; + $this->assertSame( + [['dsn' => 'foo://bar/baz']], + $container->getDefinition('enqueue.transport.default.connection_factory')->getArguments()) + ; + } + + public function testShouldBuildConnectionFactoryUsingCustomFactoryClass() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $transport->buildConnectionFactory($container, ['dsn' => 'foo:', 'factory_class' => 'theFactoryClass']); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory_factory')); + $this->assertSame( + 'theFactoryClass', + $container->getDefinition('enqueue.transport.default.connection_factory_factory')->getClass() + ); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory')); + + $this->assertNotEmpty($container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()); + $this->assertEquals( + [new Reference('enqueue.transport.default.connection_factory_factory'), 'create'], + $container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()) + ; + $this->assertSame( + [['dsn' => 'foo:']], + $container->getDefinition('enqueue.transport.default.connection_factory')->getArguments()) + ; + } + + public function testShouldBuildConnectionFactoryUsingCustomFactoryService() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $transport->buildConnectionFactory($container, ['dsn' => 'foo:', 'factory_service' => 'theFactoryService']); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory')); + + $this->assertNotEmpty($container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()); + $this->assertEquals( + [new Reference('theFactoryService'), 'create'], + $container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()) + ; + $this->assertSame( + [['dsn' => 'foo:']], + $container->getDefinition('enqueue.transport.default.connection_factory')->getArguments()) + ; + } + + public function testShouldBuildConnectionFactoryUsingConnectionFactoryClassWithoutFactory() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $transport->buildConnectionFactory($container, ['dsn' => 'foo:', 'connection_factory_class' => 'theFactoryClass']); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory')); + + $this->assertEmpty($container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()); + $this->assertSame('theFactoryClass', $container->getDefinition('enqueue.transport.default.connection_factory')->getClass()); + $this->assertSame( + [['dsn' => 'foo:']], + $container->getDefinition('enqueue.transport.default.connection_factory')->getArguments()) + ; + } + + public function testShouldBuildContext() + { + $container = new ContainerBuilder(); + $container->register('enqueue.transport.default.connection_factory', ConnectionFactory::class); + + $transport = new TransportFactory('default'); + + $transport->buildContext($container, []); + + $this->assertNotEmpty($container->getDefinition('enqueue.transport.default.context')->getFactory()); + $this->assertEquals( + [new Reference('enqueue.transport.default.connection_factory'), 'createContext'], + $container->getDefinition('enqueue.transport.default.context')->getFactory()) + ; + $this->assertSame( + [], + $container->getDefinition('enqueue.transport.default.context')->getArguments()) + ; + } + + public function testThrowIfBuildContextCalledButConnectionFactoryServiceDoesNotExist() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The service "enqueue.transport.default.connection_factory" does not exist.'); + $transport->buildContext($container, []); + } + + public function testShouldBuildQueueConsumerWithDefaultOptions() + { + $container = new ContainerBuilder(); + $container->register('enqueue.transport.default.context', Context::class); + + $transport = new TransportFactory('default'); + + $transport->buildQueueConsumer($container, []); + + $this->assertSame(10000, $container->getParameter('enqueue.transport.default.receive_timeout')); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.consumption_extensions')); + $this->assertSame(ChainExtension::class, $container->getDefinition('enqueue.transport.default.consumption_extensions')->getClass()); + $this->assertSame([[]], $container->getDefinition('enqueue.transport.default.consumption_extensions')->getArguments()); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.queue_consumer')); + $this->assertSame(QueueConsumer::class, $container->getDefinition('enqueue.transport.default.queue_consumer')->getClass()); + $this->assertEquals([ + new Reference('enqueue.transport.default.context'), + new Reference('enqueue.transport.default.consumption_extensions'), + [], + new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE), + '%enqueue.transport.default.receive_timeout%', + ], $container->getDefinition('enqueue.transport.default.queue_consumer')->getArguments()); + } + + public function testShouldBuildQueueConsumerWithCustomOptions() + { + $container = new ContainerBuilder(); + $container->register('enqueue.transport.default.context', Context::class); + + $transport = new TransportFactory('default'); + + $transport->buildQueueConsumer($container, [ + 'receive_timeout' => 567, + ]); + + $this->assertSame(567, $container->getParameter('enqueue.transport.default.receive_timeout')); + } + + public function testThrowIfBuildQueueConsumerCalledButContextServiceDoesNotExist() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The service "enqueue.transport.default.context" does not exist.'); + $transport->buildQueueConsumer($container, []); + } + + public function testShouldBuildRpcClientWithDefaultOptions() + { + $container = new ContainerBuilder(); + $container->register('enqueue.transport.default.context', Context::class); + + $transport = new TransportFactory('default'); + + $transport->buildRpcClient($container, []); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.rpc_factory')); + $this->assertSame(RpcFactory::class, $container->getDefinition('enqueue.transport.default.rpc_factory')->getClass()); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.rpc_client')); + $this->assertSame(RpcClient::class, $container->getDefinition('enqueue.transport.default.rpc_client')->getClass()); + $this->assertEquals([ + new Reference('enqueue.transport.default.context'), + new Reference('enqueue.transport.default.rpc_factory'), + ], $container->getDefinition('enqueue.transport.default.rpc_client')->getArguments()); + } + + public function testThrowIfBuildRpcClientCalledButContextServiceDoesNotExist() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The service "enqueue.transport.default.context" does not exist.'); + $transport->buildRpcClient($container, []); + } + + /** + * @return [TreeBuilder, NodeDefinition] + */ + private function getRootNode(): array + { + if (method_exists(TreeBuilder::class, 'getRootNode')) { + $tb = new TreeBuilder('foo'); + + return [$tb, $tb->getRootNode()]; + } + + $tb = new TreeBuilder(); + + return [$tb, $tb->root('foo')]; + } +} diff --git a/Tests/Symfony/LazyProducerTest.php b/Tests/Symfony/LazyProducerTest.php new file mode 100644 index 0000000..c8ba596 --- /dev/null +++ b/Tests/Symfony/LazyProducerTest.php @@ -0,0 +1,128 @@ +assertClassImplements(ProducerInterface::class, LazyProducer::class); + } + + public function testShouldNotCallRealProducerInConstructor() + { + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->never()) + ->method('get') + ; + + new LazyProducer($containerMock, 'realProducerId'); + } + + public function testShouldProxyAllArgumentOnSendEvent() + { + $topic = 'theTopic'; + $message = 'theMessage'; + + $realProducerMock = $this->createProducerMock(); + $realProducerMock + ->expects($this->once()) + ->method('sendEvent') + ->with($topic, $message) + ; + + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->once()) + ->method('get') + ->with('realProducerId') + ->willReturn($realProducerMock) + ; + + $lazyProducer = new LazyProducer($containerMock, 'realProducerId'); + + $lazyProducer->sendEvent($topic, $message); + } + + public function testShouldProxyAllArgumentOnSendCommand() + { + $command = 'theCommand'; + $message = 'theMessage'; + $needReply = false; + + $realProducerMock = $this->createProducerMock(); + $realProducerMock + ->expects($this->once()) + ->method('sendCommand') + ->with($command, $message, $needReply) + ; + + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->once()) + ->method('get') + ->with('realProducerId') + ->willReturn($realProducerMock) + ; + + $lazyProducer = new LazyProducer($containerMock, 'realProducerId'); + + $result = $lazyProducer->sendCommand($command, $message, $needReply); + + $this->assertNull($result); + } + + public function testShouldProxyReturnedPromiseBackOnSendCommand() + { + $expectedPromise = $this->createMock(Promise::class); + + $realProducerMock = $this->createProducerMock(); + $realProducerMock + ->expects($this->once()) + ->method('sendCommand') + ->willReturn($expectedPromise) + ; + + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->once()) + ->method('get') + ->with('realProducerId') + ->willReturn($realProducerMock) + ; + + $lazyProducer = new LazyProducer($containerMock, 'realProducerId'); + + $actualPromise = $lazyProducer->sendCommand('aCommand', 'aMessage', true); + + $this->assertSame($expectedPromise, $actualPromise); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface + */ + private function createProducerMock(): ProducerInterface + { + return $this->createMock(ProducerInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|ContainerInterface + */ + private function createContainerMock(): ContainerInterface + { + return $this->createMock(ContainerInterface::class); + } +} diff --git a/Tests/Symfony/MissingTransportFactoryTest.php b/Tests/Symfony/MissingTransportFactoryTest.php deleted file mode 100644 index b466a5b..0000000 --- a/Tests/Symfony/MissingTransportFactoryTest.php +++ /dev/null @@ -1,73 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, MissingTransportFactory::class); - } - - public function testCouldBeConstructedWithNameAndPackages() - { - $transport = new MissingTransportFactory('aMissingTransportName', ['aPackage', 'anotherPackage']); - - $this->assertEquals('aMissingTransportName', $transport->getName()); - } - - public function testThrowOnProcessForOnePackageToInstall() - { - $transport = new MissingTransportFactory('aMissingTransportName', ['aFooPackage']); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('Invalid configuration for path "foo": In order to use the transport "aMissingTransportName" install a package "aFooPackage"'); - $processor->process($tb->buildTree(), [[]]); - } - - public function testThrowOnProcessForSeveralPackagesToInstall() - { - $transport = new MissingTransportFactory('aMissingTransportName', ['aFooPackage', 'aBarPackage']); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('Invalid configuration for path "foo": In order to use the transport "aMissingTransportName" install one of the packages "aFooPackage", "aBarPackage"'); - $processor->process($tb->buildTree(), [[]]); - } - - public function testThrowEvenIfThereAreSomeOptionsPassed() - { - $transport = new MissingTransportFactory('aMissingTransportName', ['aFooPackage', 'aBarPackage']); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('In order to use the transport "aMissingTransportName"'); - $processor->process($tb->buildTree(), [[ - 'foo' => 'fooVal', - 'bar' => 'barVal', - ]]); - } -} diff --git a/Tests/Symfony/RabbitMqAmqpTransportFactoryTest.php b/Tests/Symfony/RabbitMqAmqpTransportFactoryTest.php deleted file mode 100644 index cfe77e6..0000000 --- a/Tests/Symfony/RabbitMqAmqpTransportFactoryTest.php +++ /dev/null @@ -1,129 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, RabbitMqAmqpTransportFactory::class); - } - - public function testShouldExtendAmqpTransportFactoryClass() - { - $this->assertClassExtends(AmqpTransportFactory::class, RabbitMqAmqpTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new RabbitMqAmqpTransportFactory(); - - $this->assertEquals('rabbitmq_amqp', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new RabbitMqAmqpTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new RabbitMqAmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), []); - - $this->assertEquals([ - 'delay_strategy' => 'dlx', - ], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqAmqpTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'delay_strategy' => null, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'delay_strategy' => null, - ]], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqAmqpTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'delay_strategy' => null, - ]); - - $this->assertEquals('enqueue.transport.rabbitmq_amqp.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.rabbitmq_amqp.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.rabbitmq_amqp.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqAmqpTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.rabbitmq_amqp.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(RabbitMqDriver::class, $driver->getClass()); - } -} diff --git a/Tests/Util/Fixtures/JsonSerializableClass.php b/Tests/Util/Fixtures/JsonSerializableClass.php index b612978..1a77ce0 100644 --- a/Tests/Util/Fixtures/JsonSerializableClass.php +++ b/Tests/Util/Fixtures/JsonSerializableClass.php @@ -6,7 +6,8 @@ class JsonSerializableClass implements \JsonSerializable { public $keyPublic = 'public'; - public function jsonSerialize() + #[\ReturnTypeWillChange] + public function jsonSerialize(): array { return [ 'key' => 'value', diff --git a/Tests/Util/JSONTest.php b/Tests/Util/JSONTest.php index c37862e..1a3df42 100644 --- a/Tests/Util/JSONTest.php +++ b/Tests/Util/JSONTest.php @@ -16,7 +16,8 @@ public function testShouldDecodeString() public function testThrowIfMalformedJson() { - $this->setExpectedException(\InvalidArgumentException::class, 'The malformed json given. '); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The malformed json given. '); $this->assertSame(['foo' => 'fooVal'], JSON::decode('{]')); } @@ -38,15 +39,11 @@ public function nonStringDataProvider() /** * @dataProvider nonStringDataProvider - * - * @param mixed $value */ public function testShouldThrowExceptionIfInputIsNotString($value) { - $this->setExpectedException( - \InvalidArgumentException::class, - 'Accept only string argument but got:' - ); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Accept only string argument but got:'); $this->assertSame(0, JSON::decode($value)); } @@ -96,10 +93,8 @@ public function testShouldEncodeObjectOfJsonSerializableClass() public function testThrowIfValueIsResource() { - $this->setExpectedException( - \InvalidArgumentException::class, - 'Could not encode value into json. Error 8 and message Type is not supported' - ); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Could not encode value into json. Error 8 and message Type is not supported'); $resource = fopen('php://memory', 'r'); fclose($resource); diff --git a/Tests/Util/UUIDTest.php b/Tests/Util/UUIDTest.php index ac30903..f21693e 100644 --- a/Tests/Util/UUIDTest.php +++ b/Tests/Util/UUIDTest.php @@ -11,7 +11,7 @@ public function testShouldGenerateUniqueId() { $uuid = UUID::generate(); - $this->assertInternalType('string', $uuid); + $this->assertIsString($uuid); $this->assertEquals(36, strlen($uuid)); } diff --git a/Tests/Util/VarExportTest.php b/Tests/Util/VarExportTest.php index 1d2384a..b71e78a 100644 --- a/Tests/Util/VarExportTest.php +++ b/Tests/Util/VarExportTest.php @@ -7,16 +7,8 @@ class VarExportTest extends TestCase { - public function testCouldBeConstructedWithValueAsArgument() - { - new VarExport('aVal'); - } - /** * @dataProvider provideValues - * - * @param mixed $value - * @param mixed $expected */ public function testShouldConvertValueToStringUsingVarExportFunction($value, $expected) { diff --git a/Tests/fix_composer_json.php b/Tests/fix_composer_json.php index bce1ebb..324f184 100644 --- a/Tests/fix_composer_json.php +++ b/Tests/fix_composer_json.php @@ -4,8 +4,8 @@ $composerJson = json_decode(file_get_contents(__DIR__.'/../composer.json'), true); -$composerJson['config']['platform']['ext-amqp'] = '1.7'; +$composerJson['config']['platform']['ext-amqp'] = '1.9.3'; $composerJson['config']['platform']['ext-rdkafka'] = '3.3'; -$composerJson['config']['platform']['ext-gearman'] = '1.1'; +$composerJson['config']['platform']['ext-gearman'] = '2'; -file_put_contents(__DIR__.'/../composer.json', json_encode($composerJson, JSON_PRETTY_PRINT)); +file_put_contents(__DIR__.'/../composer.json', json_encode($composerJson, \JSON_PRETTY_PRINT)); diff --git a/Util/JSON.php b/Util/JSON.php index f85738e..67411af 100644 --- a/Util/JSON.php +++ b/Util/JSON.php @@ -14,10 +14,7 @@ class JSON public static function decode($string) { if (!is_string($string)) { - throw new \InvalidArgumentException(sprintf( - 'Accept only string argument but got: "%s"', - is_object($string) ? get_class($string) : gettype($string) - )); + throw new \InvalidArgumentException(sprintf('Accept only string argument but got: "%s"', is_object($string) ? $string::class : gettype($string))); } // PHP7 fix - empty string and null cause syntax error @@ -26,32 +23,22 @@ public static function decode($string) } $decoded = json_decode($string, true); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'The malformed json given. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); } return $decoded; } /** - * @param mixed $value - * * @return string */ public static function encode($value) { - $encoded = json_encode($value, JSON_UNESCAPED_UNICODE); - - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'Could not encode value into json. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + $encoded = json_encode($value, \JSON_UNESCAPED_UNICODE); + + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('Could not encode value into json. Error %s and message %s', json_last_error(), json_last_error_msg())); } return $encoded; diff --git a/Util/Stringify.php b/Util/Stringify.php new file mode 100644 index 0000000..d8a48a8 --- /dev/null +++ b/Util/Stringify.php @@ -0,0 +1,30 @@ +value = $value; + } + + public function __toString(): string + { + if (is_string($this->value) || is_scalar($this->value)) { + return $this->value; + } + + return json_encode($this->value, \JSON_UNESCAPED_SLASHES); + } + + public static function that($value): self + { + return new self($value); + } +} diff --git a/Util/VarExport.php b/Util/VarExport.php index 4a48afa..9a91470 100644 --- a/Util/VarExport.php +++ b/Util/VarExport.php @@ -7,14 +7,8 @@ */ class VarExport { - /** - * @var mixed - */ private $value; - /** - * @param mixed $value - */ public function __construct($value) { $this->value = $value; diff --git a/composer.json b/composer.json index 11f9def..c336c4b 100644 --- a/composer.json +++ b/composer.json @@ -6,39 +6,45 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": "^7.1.3", - "queue-interop/queue-interop": "^0.7@dev", - "enqueue/null": "^0.9@dev", - "ramsey/uuid": "^2|^3.5", - "psr/log": "^1" + "php": "^8.1", + "queue-interop/amqp-interop": "^0.8.2", + "queue-interop/queue-interop": "^0.8", + "enqueue/null": "^0.10", + "enqueue/dsn": "^0.10", + "ramsey/uuid": "^3.5|^4", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "psr/container": "^1.1 || ^2.0" }, "require-dev": { - "phpunit/phpunit": "~5.5", - "symfony/console": "^3.4|^4", - "symfony/dependency-injection": "^3.4|^4", - "symfony/config": "^3.4|^4", - "symfony/event-dispatcher": "^3.4|^4", - "symfony/http-kernel": "^3.4|^4", - "enqueue/amqp-ext": "^0.9@dev", - "enqueue/amqp-lib": "^0.9@dev", - "enqueue/amqp-bunny": "^0.9@dev", - "enqueue/pheanstalk": "^0.9@dev", - "enqueue/gearman": "^0.9@dev", - "enqueue/rdkafka": "^0.9@dev", - "enqueue/dbal": "^0.9@dev", - "enqueue/fs": "^0.9@dev", - "enqueue/gps": "^0.9@dev", - "enqueue/redis": "^0.9@dev", - "enqueue/sqs": "^0.9@dev", - "enqueue/stomp": "^0.9@dev", - "enqueue/test": "^0.9@dev", - "enqueue/simple-client": "^0.9@dev", - "empi89/php-amqp-stubs": "*@dev" + "phpunit/phpunit": "^9.5", + "symfony/console": "^5.41|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "enqueue/amqp-ext": "0.10.x-dev", + "enqueue/amqp-lib": "0.10.x-dev", + "enqueue/amqp-bunny": "0.10.x-dev", + "enqueue/pheanstalk": "0.10.x-dev", + "enqueue/gearman": "0.10.x-dev", + "enqueue/rdkafka": "0.10.x-dev", + "enqueue/dbal": "0.10.x-dev", + "enqueue/fs": "0.10.x-dev", + "enqueue/gps": "0.10.x-dev", + "enqueue/redis": "0.10.x-dev", + "enqueue/sqs": "0.10.x-dev", + "enqueue/stomp": "0.10.x-dev", + "enqueue/test": "0.10.x-dev", + "enqueue/simple-client": "0.10.x-dev", + "enqueue/mongodb": "0.10.x-dev", + "empi89/php-amqp-stubs": "*@dev", + "enqueue/dsn": "0.10.x-dev" }, "suggest": { - "symfony/console": "^2.8|^3|^4 If you want to use li commands", - "symfony/dependency-injection": "^3.4|^4", - "symfony/config": "^3.4|^4", + "symfony/console": "^5.4|^6.0 If you want to use cli commands", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", "enqueue/amqp-ext": "AMQP transport (based on php extension)", "enqueue/stomp": "STOMP transport", "enqueue/fs": "Filesystem transport", @@ -55,7 +61,6 @@ }, "autoload": { "psr-4": { "Enqueue\\": "" }, - "files": ["functions_include.php"], "exclude-from-classmap": [ "/Tests/" ] @@ -68,7 +73,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.9.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/functions.php b/functions.php deleted file mode 100644 index 8960785..0000000 --- a/functions.php +++ /dev/null @@ -1,176 +0,0 @@ -createContext(); -} - -/** - * @param PsrContext $c - * @param string $topic - * @param string $body - */ -function send_topic(PsrContext $c, $topic, $body) -{ - $topic = $c->createTopic($topic); - $message = $c->createMessage($body); - - $c->createProducer()->send($topic, $message); -} - -/** - * @param PsrContext $c - * @param string $queue - * @param string $body - */ -function send_queue(PsrContext $c, $queue, $body) -{ - $queue = $c->createQueue($queue); - $message = $c->createMessage($body); - - $c->createProducer()->send($queue, $message); -} - -/** - * @param PsrContext $c - * @param string $queue - * @param callable $callback - */ -function consume(PsrContext $c, $queue, callable $callback) -{ - $queueConsumer = new QueueConsumer($c); - $queueConsumer->bind($queue, $callback); - - $queueConsumer->consume(); -} diff --git a/functions_include.php b/functions_include.php deleted file mode 100644 index cf5502a..0000000 --- a/functions_include.php +++ /dev/null @@ -1,6 +0,0 @@ - - +