├── .github ├── CODEOWNERS └── workflows │ └── ci.yml ├── src └── AmqpBundle │ ├── Tests │ ├── Fixtures │ │ ├── queue-defaults.yml │ │ └── queue-arguments-config.yml │ └── Units │ │ ├── Factory │ │ ├── Mock │ │ │ ├── MockAMQPExchange.php │ │ │ ├── MockAMQPQueue.php │ │ │ └── MockAMQPChannel.php │ │ ├── ProducerFactory.php │ │ └── ConsumerFactory.php │ │ ├── DependencyInjection │ │ └── M6WebAmqpExtension.php │ │ └── Amqp │ │ ├── Producer.php │ │ └── Consumer.php │ ├── Amqp │ ├── Exception.php │ ├── Locator.php │ ├── DataCollector.php │ ├── AbstractAmqp.php │ ├── Producer.php │ └── Consumer.php │ ├── Sandbox │ ├── NullChannel.php │ ├── NullExchange.php │ ├── NullConnection.php │ ├── NullQueue.php │ └── NullEnvelope.php │ ├── Resources │ ├── config │ │ ├── data_collector.yml │ │ ├── sandbox_services.yml │ │ └── services.yml │ └── views │ │ └── Collector │ │ └── amqp.html.twig │ ├── Event │ ├── PurgeEvent.php │ ├── AckEvent.php │ ├── NackEvent.php │ ├── PreRetrieveEvent.php │ ├── DispatcherInterface.php │ ├── PrePublishEvent.php │ └── Command.php │ ├── M6WebAmqpBundle.php │ ├── Factory │ ├── AMQPFactory.php │ ├── ProducerFactory.php │ └── ConsumerFactory.php │ └── DependencyInjection │ ├── CompilerPass │ └── M6webAmqpLocatorPass.php │ ├── M6WebAmqpExtension.php │ └── Configuration.php ├── rector.php ├── phpstan.neon.dist ├── composer.json ├── LICENSE ├── CONTRIBUTING.md ├── Makefile ├── .php-cs-fixer.dist.php └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @BedrockStreaming/messagequeuing 2 | -------------------------------------------------------------------------------- /src/AmqpBundle/Tests/Fixtures/queue-defaults.yml: -------------------------------------------------------------------------------- 1 | m6_web_amqp: 2 | connections: 3 | default: 4 | host: 'localhost' 5 | lazy: true 6 | -------------------------------------------------------------------------------- /src/AmqpBundle/Amqp/Exception.php: -------------------------------------------------------------------------------- 1 | withPaths([ 10 | __DIR__ . '/src', 11 | ]) 12 | // uncomment to reach your current PHP version 13 | // ->withPhpSets() 14 | ->withRules([ 15 | AddVoidReturnTypeWhereNoReturnRector::class, 16 | ]); 17 | -------------------------------------------------------------------------------- /src/AmqpBundle/Resources/config/sandbox_services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | m6_web_amqp.exchange.class: 'M6Web\Bundle\AmqpBundle\Sandbox\NullExchange' 3 | m6_web_amqp.queue.class: 'M6Web\Bundle\AmqpBundle\Sandbox\NullQueue' 4 | m6_web_amqp.connection.class: 'M6Web\Bundle\AmqpBundle\Sandbox\NullConnection' 5 | m6_web_amqp.channel.class: 'M6Web\Bundle\AmqpBundle\Sandbox\NullChannel' 6 | m6_web_amqp.envelope.class: 'M6Web\Bundle\AmqpBundle\Sandbox\NullEnvelope' 7 | 8 | -------------------------------------------------------------------------------- /src/AmqpBundle/Event/PurgeEvent.php: -------------------------------------------------------------------------------- 1 | queue; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | paths: 3 | - 'src' 4 | excludePaths: 5 | - 'src/AmqpBundle/Tests' 6 | - 'src/AmqpBundle/Sandbox' 7 | - 'src/AmqpBundle/DependencyInjection' 8 | 9 | level: 8 10 | checkMissingIterableValueType: false 11 | checkGenericClassInNonGenericObjectType: false 12 | treatPhpDocTypesAsCertain: false 13 | 14 | ignoreErrors: 15 | - '#Unable to resolve the template type T in call to method Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface::dispatch\(\)#' 16 | -------------------------------------------------------------------------------- /src/AmqpBundle/Tests/Units/Factory/Mock/MockAMQPQueue.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new M6webAmqpLocatorPass()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AmqpBundle/Tests/Units/Factory/Mock/MockAMQPChannel.php: -------------------------------------------------------------------------------- 1 | deliveryTag; 23 | } 24 | 25 | public function getFlags(): int 26 | { 27 | return $this->flags; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AmqpBundle/Event/NackEvent.php: -------------------------------------------------------------------------------- 1 | deliveryTag; 23 | } 24 | 25 | public function getFlags(): int 26 | { 27 | return $this->flags; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AmqpBundle/Event/PreRetrieveEvent.php: -------------------------------------------------------------------------------- 1 | envelope; 23 | } 24 | 25 | public function setEnvelope(?\AMQPEnvelope $envelope): void 26 | { 27 | $this->envelope = $envelope; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AmqpBundle/Amqp/Locator.php: -------------------------------------------------------------------------------- 1 | consumers[$id]; 18 | } 19 | 20 | /** 21 | * @param Consumer[] $consumers 22 | */ 23 | public function setConsumers(array $consumers): void 24 | { 25 | $this->consumers = $consumers; 26 | } 27 | 28 | public function getProducer(string $id): Producer 29 | { 30 | return $this->producers[$id]; 31 | } 32 | 33 | /** 34 | * @param Producer[] $producers 35 | */ 36 | public function setProducers(array $producers): void 37 | { 38 | $this->producers = $producers; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/AmqpBundle/Sandbox/NullConnection.php: -------------------------------------------------------------------------------- 1 | =2.0", 13 | "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0", 14 | "symfony/framework-bundle": "^5.4 || ^6.4 || ^7.0", 15 | "symfony/http-kernel": "^5.4 || ^6.4 || ^7.0", 16 | "symfony/yaml": "^5.4 || 6.4 || ^7.0", 17 | "twig/twig": "^2.13 || ^3.0" 18 | }, 19 | "require-dev" : { 20 | "atoum/atoum": "~4.0", 21 | "m6web/php-cs-fixer-config": "^3.2", 22 | "phpstan/phpstan": "^1.10" 23 | }, 24 | "suggest": { 25 | "ocramius/proxy-manager": "Required for lazy connections" 26 | }, 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "3.x-dev" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/AmqpBundle/Event/DispatcherInterface.php: -------------------------------------------------------------------------------- 1 | envelopes = new \SplQueue(); 25 | } 26 | 27 | /** 28 | * Enqueue message or no message. 29 | */ 30 | public function enqueue(?\AMQPEnvelope $envelope = null): void 31 | { 32 | $this->envelopes->enqueue($envelope); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function get($flags = AMQP_NOPARAM): ?\AMQPEnvelope 39 | { 40 | if (!$this->envelopes->isEmpty()) { 41 | return $this->envelopes->dequeue(); 42 | } 43 | 44 | return null; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function declareQueue(): int 51 | { 52 | return $this->envelopes->count(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/AmqpBundle/Factory/AMQPFactory.php: -------------------------------------------------------------------------------- 1 | setName($exchangeOptions['name']); 20 | 21 | // If the type is not specified, the exchange must exist 22 | if (isset($exchangeOptions['type'])) { 23 | $exchange->setType($exchangeOptions['type']); 24 | $exchange->setArguments($exchangeOptions['arguments']); 25 | $exchange->setFlags( 26 | ($exchangeOptions['passive'] ? AMQP_PASSIVE : AMQP_NOPARAM) | 27 | ($exchangeOptions['durable'] ? AMQP_DURABLE : AMQP_NOPARAM) | 28 | ($exchangeOptions['auto_delete'] ? AMQP_AUTODELETE : AMQP_NOPARAM), 29 | ); 30 | $exchange->declareExchange(); 31 | } 32 | 33 | return $exchange; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/AmqpBundle/DependencyInjection/CompilerPass/M6webAmqpLocatorPass.php: -------------------------------------------------------------------------------- 1 | has('m6_web_amqp.locator')) { 16 | return; 17 | } 18 | $locator = $container->getDefinition('m6_web_amqp.locator'); 19 | $consumers = []; 20 | $producers = []; 21 | 22 | $taggedServices = $container->findTaggedServiceIds('m6_web_amqp.consumers'); 23 | foreach ($taggedServices as $id => $taggedService) { 24 | $consumers[$id] = new Reference($id); 25 | } 26 | $locator->addMethodCall('setConsumers', [$consumers]); 27 | 28 | $taggedServices = $container->findTaggedServiceIds('m6_web_amqp.producers'); 29 | foreach ($taggedServices as $id => $taggedService) { 30 | $producers[$id] = new Reference($id); 31 | } 32 | $locator->addMethodCall('setProducers', [$producers]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/AmqpBundle/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | m6_web_amqp.connection.class : AMQPConnection 3 | m6_web_amqp.channel.class : AMQPChannel 4 | m6_web_amqp.exchange.class : AMQPExchange 5 | m6_web_amqp.queue.class: AMQPQueue 6 | m6_web_amqp.envelope.class: AMQPEnvelope 7 | 8 | m6_web_amqp.event.command.class: M6Web\Bundle\AmqpBundle\Event\Command 9 | 10 | m6_web_amqp.producer.class: M6Web\Bundle\AmqpBundle\Amqp\Producer 11 | m6_web_amqp.consumer.class: M6Web\Bundle\AmqpBundle\Amqp\Consumer 12 | m6_web_amqp.locator.class: M6Web\Bundle\AmqpBundle\Amqp\Locator 13 | 14 | m6_web_amqp.producer_factory.class: M6Web\Bundle\AmqpBundle\Factory\ProducerFactory 15 | m6_web_amqp.consumer_factory.class: M6Web\Bundle\AmqpBundle\Factory\ConsumerFactory 16 | 17 | services: 18 | m6_web_amqp.producer_factory: 19 | class: "%m6_web_amqp.producer_factory.class%" 20 | arguments: 21 | - "%m6_web_amqp.channel.class%" 22 | - "%m6_web_amqp.exchange.class%" 23 | - "%m6_web_amqp.queue.class%" 24 | 25 | m6_web_amqp.consumer_factory: 26 | class: "%m6_web_amqp.consumer_factory.class%" 27 | arguments: 28 | - "%m6_web_amqp.channel.class%" 29 | - "%m6_web_amqp.queue.class%" 30 | - "%m6_web_amqp.exchange.class%" 31 | 32 | m6_web_amqp.locator: 33 | class: "%m6_web_amqp.locator.class%" 34 | lazy: true 35 | public: true 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | This bundle was originally created for M6Web projects purpose. As we strongly believe in open source, we share it to you. 4 | 5 | If you want to learn more about our opinion on open source, you can read the [OSS article](http://tech.m6web.fr/oss/) on our website. 6 | 7 | ## Developing 8 | 9 | The features available for now are only those we need, but you're welcome to open an issue or pull-request if you need more. 10 | 11 | To ensure good code quality, we use our awesome tool "[coke](https://github.com/M6Web/Coke)" to check there is no coding standards violations. 12 | We use [Symfony2 coding standards](https://github.com/M6Web/Symfony2-coding-standard). 13 | 14 | To execute coke, you need to install dependencies in dev mode 15 | ```bash 16 | composer install --dev 17 | ``` 18 | 19 | And you can launch coke 20 | ```bash 21 | ./vendor/bin/coke 22 | ``` 23 | 24 | ## Testing 25 | 26 | This bundle is tested with [atoum](https://github.com/atoum/atoum). 27 | 28 | To launch tests, you need to install dependencies in dev mode 29 | ```bash 30 | composer install --dev 31 | ``` 32 | 33 | And you can now launch tests 34 | ```bash 35 | ./vendor/bin/atoum 36 | ``` 37 | 38 | ## Pull-request 39 | 40 | If you are currently reading this section, you are a really good guy who share our vision about open source. 41 | 42 | So, we don't want to harass you with tons of constraints. There is only 2 things we care about : 43 | * testing 44 | * coding standards 45 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCE_DIR = $(shell pwd) 2 | BIN_DIR ?= ${SOURCE_DIR}/bin 3 | 4 | define printSection 5 | @printf "\033[36m\n==================================================\033[0m\n" 6 | @printf "\033[36m $1 \033[0m" 7 | @printf "\033[36m\n==================================================\033[0m\n" 8 | endef 9 | 10 | .PHONY: all 11 | all: install ci 12 | 13 | .PHONY: ci 14 | ci: quality test 15 | 16 | .PHONY: install 17 | install: clean-vendor composer-install 18 | 19 | .PHONY: quality 20 | quality: cs-ci phpstan 21 | 22 | .PHONY: test 23 | test: atoum 24 | 25 | .PHONY: clean-vendor 26 | clean-vendor: 27 | $(call printSection,CLEAN VENDOR) 28 | rm -rf ${SOURCE_DIR}/vendor 29 | 30 | .PHONY: phpstan 31 | phpstan: phpstan-cache-clear 32 | $(call printSection,PHPSTAN) 33 | ${BIN_DIR}/phpstan.phar analyse --memory-limit=1G 34 | 35 | .PHONY: phpstan-cache-clear 36 | phpstan-cache-clear: 37 | ${BIN_DIR}/phpstan.phar clear-result-cache 38 | 39 | composer-install: 40 | $(call printSection,COMPOSER INSTALL) 41 | composer --no-interaction install --ansi --no-progress --prefer-dist 42 | 43 | atoum: 44 | $(call printSection,TESTING) 45 | ${BIN_DIR}/atoum --no-code-coverage --verbose 46 | 47 | .PHONY: cs 48 | cs: composer-install 49 | ${BIN_DIR}/php-cs-fixer fix --dry-run --stop-on-violation --diff 50 | 51 | .PHONY: cs-fix 52 | cs-fix: composer-install 53 | ${BIN_DIR}/php-cs-fixer fix 54 | 55 | .PHONY: cs-ci 56 | cs-ci: composer-install 57 | $(call printSection,PHPCS) 58 | ${BIN_DIR}/php-cs-fixer fix --dry-run --using-cache=no --verbose 59 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__.'/src', 6 | ]); 7 | 8 | $config = new class() extends PhpCsFixer\Config { 9 | public function __construct() 10 | { 11 | parent::__construct('Bedrock Streaming'); 12 | 13 | $this->setRiskyAllowed(true); 14 | } 15 | 16 | public function getRules(): array 17 | { 18 | return array_merge((new M6Web\CS\Config\BedrockStreaming())->getRules(), [ 19 | 'no_unreachable_default_argument_value' => true, 20 | 'trailing_comma_in_multiline' => [ 21 | 'after_heredoc' => true, 22 | 'elements' => ['arrays', 'arguments', 'parameters'], 23 | ], 24 | 'native_function_invocation' => [ 25 | 'include' => ['@compiler_optimized'] 26 | ], 27 | 'simplified_null_return' => false, 28 | 'void_return' => true, 29 | 'phpdoc_order' => true, 30 | 'phpdoc_types_order' => false, 31 | 'no_superfluous_phpdoc_tags' => true, 32 | 'php_unit_test_case_static_method_calls' => [ 33 | 'call_type' => 'static', 34 | ], 35 | 'yoda_style' => [ 36 | 'equal' => false, 37 | 'identical' => false, 38 | 'less_and_greater' => false 39 | ], 40 | ]); 41 | } 42 | }; 43 | 44 | $config 45 | ->setFinder($finder) 46 | ->setCacheFile('var/cache/tools/.php-cs-fixer.cache'); 47 | 48 | return $config; 49 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | tests-old-versions: 6 | name: Tests old versions 7 | runs-on: ubuntu-20.04 8 | strategy: 9 | matrix: 10 | php-version: ['8.0', '8.1', '8.2' , '8.3' ] 11 | fail-fast: false 12 | steps: 13 | - uses: actions/checkout@master 14 | - uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: ${{ matrix.php-version }} 17 | coverage: xdebug2 18 | extensions: amqp-2 19 | - name: Install symfony v5.4 20 | env: 21 | SYMFONY_VERSION: '^5.4' 22 | run: composer require symfony/symfony:$SYMFONY_VERSION --no-update 23 | - name: Install dependencies 24 | run: composer update --prefer-dist --no-interaction 25 | - name: Unit tests 26 | run: bin/atoum 27 | 28 | tests-current-versions: 29 | name: Tests current versions 30 | runs-on: ubuntu-20.04 31 | strategy: 32 | matrix: 33 | php-version: ['8.2' , '8.3' ] 34 | symfony-version: ['^6.4', '^7.0'] 35 | fail-fast: false 36 | steps: 37 | - uses: actions/checkout@master 38 | - uses: shivammathur/setup-php@v2 39 | with: 40 | php-version: ${{ matrix.php-version }} 41 | coverage: xdebug 42 | extensions: amqp-2 43 | - name: Install symfony version from matrix 44 | env: 45 | SYMFONY_VERSION: ${{ matrix.symfony-version }} 46 | run: composer require symfony/symfony:$SYMFONY_VERSION --no-update 47 | - name: Install dependencies 48 | run: composer update --prefer-dist --no-interaction 49 | - name: Unit tests 50 | run: bin/atoum 51 | -------------------------------------------------------------------------------- /src/AmqpBundle/Event/PrePublishEvent.php: -------------------------------------------------------------------------------- 1 | canPublish = true; 20 | } 21 | 22 | public function canPublish(): bool 23 | { 24 | return $this->canPublish; 25 | } 26 | 27 | public function allowPublish(): void 28 | { 29 | $this->canPublish = true; 30 | } 31 | 32 | public function denyPublish(): void 33 | { 34 | $this->canPublish = false; 35 | } 36 | 37 | public function getMessage(): string 38 | { 39 | return $this->message; 40 | } 41 | 42 | public function setMessage(string $message): void 43 | { 44 | $this->message = $message; 45 | } 46 | 47 | public function getRoutingKeys(): array 48 | { 49 | return $this->routingKeys; 50 | } 51 | 52 | public function setRoutingKeys(array $routingKeys): void 53 | { 54 | $this->routingKeys = $routingKeys; 55 | } 56 | 57 | public function getFlags(): int 58 | { 59 | return $this->flags; 60 | } 61 | 62 | public function setFlags(int $flags): void 63 | { 64 | $this->flags = $flags; 65 | } 66 | 67 | public function getAttributes(): array 68 | { 69 | return $this->attributes; 70 | } 71 | 72 | public function setAttributes(array $attributes): void 73 | { 74 | $this->attributes = $attributes; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/AmqpBundle/Tests/Fixtures/queue-arguments-config.yml: -------------------------------------------------------------------------------- 1 | m6_web_amqp: 2 | prototype: true 3 | sandbox: 4 | enabled: true 5 | connections: 6 | default: 7 | host: 'localhost' 8 | port: 5672 9 | timeout: 2 10 | login: 'guest' 11 | password: 'guest' 12 | vhost: '/' 13 | lazy: true 14 | with_heartbeat: 15 | host: 'localhost' 16 | port: 5672 17 | timeout: 2 18 | login: 'guest' 19 | password: 'guest' 20 | vhost: '/' 21 | lazy: true 22 | heartbeat: 1 23 | producers: 24 | producer_1: 25 | connection: default 26 | exchange_options: 27 | name: 'exchange_1' 28 | type: direct 29 | routing_keys: ['super_routing_key'] 30 | producer_2: 31 | connection: default 32 | exchange_options: 33 | name: 'exchange_2' 34 | type: direct 35 | routing_keys: ['super_routing_key'] 36 | queue_options: 37 | name: 'ha.queue_exchange_2' 38 | arguments: 39 | x-message-ttl: 1000 40 | consumers: 41 | #Assume that exchange is already defined 42 | consumer_1: 43 | connection: default 44 | exchange_options: 45 | name: 'exchange_1' 46 | queue_options: 47 | name: 'ha.queue_exchange_1' 48 | routing_keys: ['super_routing_key'] 49 | arguments: 50 | x-dead-letter-exchange: 'exchange_2' 51 | #Test how exchange is defined 52 | consumer_2: 53 | connection: default 54 | exchange_options: 55 | name: 'exchange_2' 56 | type: direct 57 | routing_keys: ['super_routing_key'] 58 | queue_options: 59 | name: 'ha.queue_exchange_2' 60 | -------------------------------------------------------------------------------- /src/AmqpBundle/Event/Command.php: -------------------------------------------------------------------------------- 1 | command = $command; 26 | 27 | return $this; 28 | } 29 | 30 | /** 31 | * Get the command associated with this event. 32 | */ 33 | public function getCommand(): string 34 | { 35 | return $this->command; 36 | } 37 | 38 | /** 39 | * Set the arguments. 40 | */ 41 | public function setArguments(array $v): self 42 | { 43 | $this->arguments = $v; 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * Get the arguments. 50 | */ 51 | public function getArguments(): array 52 | { 53 | return $this->arguments; 54 | } 55 | 56 | /** 57 | * set the return value. 58 | * 59 | * @param mixed $v value 60 | */ 61 | public function setReturn(mixed $v): self 62 | { 63 | $this->return = $v; 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * get the return value. 70 | */ 71 | public function getReturn(): mixed 72 | { 73 | return $this->return; 74 | } 75 | 76 | /** 77 | * {@inheritDoc} 78 | */ 79 | public function setExecutionTime(float $v): self 80 | { 81 | $this->executionTime = $v; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * return the exec time. 88 | * 89 | * @return float $v temps 90 | */ 91 | public function getExecutionTime(): float 92 | { 93 | return $this->executionTime; 94 | } 95 | 96 | /** 97 | * Alias of getExecutionTime for the statsd bundle 98 | * In ms. 99 | */ 100 | public function getTiming(): float 101 | { 102 | return $this->getExecutionTime() * 1000; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/AmqpBundle/Amqp/DataCollector.php: -------------------------------------------------------------------------------- 1 | data['name'] = $name; 20 | $this->reset(); 21 | } 22 | 23 | /** 24 | * Collect the data. 25 | * 26 | * @param Request $request The request object 27 | * @param Response $response The response object 28 | * @param \Throwable|null $exception An exception 29 | */ 30 | public function collect(Request $request, Response $response, ?\Throwable $exception = null): void 31 | { 32 | } 33 | 34 | /** 35 | * Listen for command event. 36 | * 37 | * @param DispatcherInterface $event The event object 38 | */ 39 | public function onCommand(DispatcherInterface $event): void 40 | { 41 | $this->data['commands'][] = ['command' => $event->getCommand(), 'arguments' => $event->getArguments(), 'executiontime' => $event->getExecutionTime()]; 42 | } 43 | 44 | /** 45 | * Return command list and number of times they were called. 46 | * 47 | * @return array The command list and number of times called 48 | */ 49 | public function getCommands(): array 50 | { 51 | return $this->data['commands']; 52 | } 53 | 54 | /** 55 | * Return the name of the collector. 56 | */ 57 | public function getName(): string 58 | { 59 | return $this->data['name']; 60 | } 61 | 62 | /** 63 | * Return total command execution time. 64 | */ 65 | public function getTotalExecutionTime(): float 66 | { 67 | return (float) array_sum(array_column($this->data['commands'], 'executiontime')); 68 | } 69 | 70 | /** 71 | * Get average execution time. 72 | */ 73 | public function getAvgExecutionTime(): float 74 | { 75 | return $this->getTotalExecutionTime() ? ($this->getTotalExecutionTime() / \count($this->data['commands'])) : (float) 0; 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function reset(): void 82 | { 83 | // Reset the current data, while keeping the 'name' intact. 84 | $this->data = [ 85 | 'name' => $this->data['name'], 86 | 'commands' => [], 87 | ]; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/AmqpBundle/Amqp/AbstractAmqp.php: -------------------------------------------------------------------------------- 1 | eventDispatcher) { 34 | $event = new $this->eventClass(); 35 | $event->setCommand($command) 36 | ->setArguments($arguments) 37 | ->setReturn($return) 38 | ->setExecutionTime($time); 39 | 40 | $this->eventDispatcher->dispatch($event, 'amqp.command'); 41 | } 42 | } 43 | 44 | /** 45 | * Call a method and notify an event. 46 | * 47 | * @param object $object Method object 48 | * @param string $name Method name 49 | * @param array $arguments Method arguments 50 | */ 51 | protected function call(object $object, string $name, array $arguments = []): mixed 52 | { 53 | $start = microtime(true); 54 | 55 | /** @var callable $callable */ 56 | $callable = [$object, $name]; 57 | $ret = \call_user_func_array($callable, $arguments); 58 | 59 | $this->notifyEvent($name, $arguments, $ret, microtime(true) - $start); 60 | 61 | return $ret; 62 | } 63 | 64 | /** 65 | * Set an event dispatcher to notify amqp command. 66 | * 67 | * @param EventDispatcherInterface $eventDispatcher The eventDispatcher object, which implement the notify method 68 | * @param class-string $eventClass The event class used to create an event and send it to the event dispatcher 69 | * 70 | * @throws \Exception 71 | */ 72 | public function setEventDispatcher(EventDispatcherInterface $eventDispatcher, string $eventClass): void 73 | { 74 | $class = new \ReflectionClass($eventClass); 75 | if (!$class->implementsInterface(\M6Web\Bundle\AmqpBundle\Event\DispatcherInterface::class)) { 76 | throw new Exception('The Event class : '.$eventClass.' must implement DispatcherInterface'); 77 | } 78 | 79 | $this->eventDispatcher = $eventDispatcher; 80 | $this->eventClass = $eventClass; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/AmqpBundle/Tests/Units/Factory/ProducerFactory.php: -------------------------------------------------------------------------------- 1 | if($channelClass = '\AMQPChannel') 18 | ->and($exchangeClass = '\AMQPExchange') 19 | ->and($queueClass = '\AMQPQueue') 20 | ->object($factory = new Base($channelClass, $exchangeClass, $queueClass)) 21 | ->isInstanceOf(Base::class); 22 | 23 | $this 24 | ->if($channelClass = '\DateTime') 25 | ->and($exchangeClass = '\AMQPExchange') 26 | ->and($queueClass = '\AMQPQueue') 27 | ->exception( 28 | function () use ($channelClass, $exchangeClass, $queueClass): void { 29 | $factory = new Base($channelClass, $exchangeClass, $queueClass); 30 | }, 31 | ) 32 | ->isInstanceOf('InvalidArgumentException') 33 | ->hasMessage("channelClass '\DateTime' doesn't exist or not a AMQPChannel"); 34 | 35 | $this 36 | ->if($channelClass = '\AMQPChannel') 37 | ->and($exchangeClass = '\DateTime') 38 | ->and($queueClass = '\AMQPQueue') 39 | ->exception( 40 | function () use ($channelClass, $exchangeClass, $queueClass): void { 41 | $factory = new Base($channelClass, $exchangeClass, $queueClass); 42 | }, 43 | ) 44 | ->isInstanceOf('InvalidArgumentException') 45 | ->hasMessage("exchangeClass '\DateTime' doesn't exist or not a AMQPExchange"); 46 | 47 | $this 48 | ->if($channelClass = '\AMQPChannel') 49 | ->and($exchangeClass = '\AMQPExchange') 50 | ->and($queueClass = '\DateTime') 51 | ->exception( 52 | function () use ($channelClass, $exchangeClass, $queueClass): void { 53 | $factory = new Base($channelClass, $exchangeClass, $queueClass); 54 | }, 55 | ) 56 | ->isInstanceOf('InvalidArgumentException') 57 | ->hasMessage("queueClass '\DateTime' doesn't exist or not a AMQPQueue"); 58 | } 59 | 60 | public function testFactory(): void 61 | { 62 | $this->mockGenerator->orphanize('__construct'); 63 | $this->mockGenerator->shuntParentClassCalls(); 64 | 65 | $connexion = new \mock\AMQPConnection(); 66 | 67 | $this 68 | ->if($channelClass = '\mock\M6Web\Bundle\AmqpBundle\Tests\Units\Factory\Mock\MockAMQPChannel') 69 | ->and($exchangeClass = '\mock\M6Web\Bundle\AmqpBundle\Tests\Units\Factory\Mock\MockAMQPExchange') 70 | ->and($producerClass = '\mock\M6Web\Bundle\AmqpBundle\Amqp\Producer') 71 | ->and($queueClass = '\mock\M6Web\Bundle\AmqpBundle\Tests\Units\Factory\Mock\MockAMQPQueue') 72 | ->and($exchangeOptions = [ 73 | 'name' => 'myexchange', 74 | 'type' => 'type', 75 | 'passive' => false, 76 | 'durable' => true, 77 | 'auto_delete' => false, 78 | 'routing_keys' => ['key'], 79 | 'arguments' => ['alternate-exchange' => 'my-ae'], 80 | ]) 81 | ->and($queueOptions = [ 82 | 'name' => 'myqueue', 83 | 'passive' => false, 84 | 'durable' => true, 85 | 'auto_delete' => false, 86 | 'arguments' => [], 87 | 'routing_keys' => [], 88 | ]) 89 | ->and($factory = new Base($channelClass, $exchangeClass, $queueClass)) 90 | ->object($factory->get($producerClass, $connexion, $exchangeOptions, $queueOptions)) 91 | ->isInstanceOf($producerClass); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/AmqpBundle/Factory/ProducerFactory.php: -------------------------------------------------------------------------------- 1 | channelClass = $channelClass; 45 | $this->exchangeClass = $exchangeClass; 46 | $this->queueClass = $queueClass; 47 | } 48 | 49 | /** 50 | * build the producer class. 51 | * 52 | * @param class-string $class Provider class name 53 | * @param \AMQPConnection $connexion AMQP connexion 54 | * @param array $exchangeOptions Exchange Options 55 | * @param array $queueOptions Queue Options 56 | * @param bool $lazy Specifies if it should connect 57 | */ 58 | public function get(string $class, \AMQPConnection $connexion, array $exchangeOptions, array $queueOptions, bool $lazy = false): Producer 59 | { 60 | if (!class_exists($class)) { 61 | throw new \InvalidArgumentException( 62 | sprintf("Producer class '%s' doesn't exist", $class), 63 | ); 64 | } 65 | 66 | if ($lazy) { 67 | if (!$connexion->isConnected()) { 68 | $connexion->connect(); 69 | } 70 | } 71 | 72 | // Open a new channel 73 | /** @var \AMQPChannel $channel */ 74 | $channel = new $this->channelClass($connexion); 75 | $exchange = $this->createExchange($this->exchangeClass, $channel, $exchangeOptions); 76 | 77 | if (isset($queueOptions['name'])) { 78 | // create, declare queue, and bind it to exchange 79 | /** @var \AMQPQueue $queue */ 80 | $queue = new $this->queueClass($channel); 81 | $queue->setName($queueOptions['name']); 82 | $queue->setArguments($queueOptions['arguments']); 83 | $queue->setFlags( 84 | ($queueOptions['passive'] ? AMQP_PASSIVE : AMQP_NOPARAM) | 85 | ($queueOptions['durable'] ? AMQP_DURABLE : AMQP_NOPARAM) | 86 | ($queueOptions['auto_delete'] ? AMQP_AUTODELETE : AMQP_NOPARAM), 87 | ); 88 | $queue->declareQueue(); 89 | $queue->bind($exchangeOptions['name']); 90 | 91 | // Bind the queue to some routing keys 92 | foreach ($queueOptions['routing_keys'] as $routingKey) { 93 | $queue->bind($exchangeOptions['name'], $routingKey); 94 | } 95 | } 96 | 97 | // Create the producer 98 | /** @var Producer */ 99 | return new $class($exchange, $exchangeOptions); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/AmqpBundle/Amqp/Producer.php: -------------------------------------------------------------------------------- 1 | setExchange($exchange); 27 | $this->setExchangeOptions($exchangeOptions); 28 | } 29 | 30 | /** 31 | * @param string $message the message to publish 32 | * @param int $flags one or more of AMQP_MANDATORY and AMQP_IMMEDIATE 33 | * @param array $attributes one of content_type, content_encoding, 34 | * message_id, user_id, app_id, delivery_mode, priority, 35 | * timestamp, expiration, type or reply_to 36 | * @param array $routingKeys If set, overrides the Producer 'routing_keys' for this message 37 | * 38 | * @throws \AMQPExchangeException on failure 39 | * @throws \AMQPChannelException if the channel is not open 40 | * @throws \AMQPConnectionException if the connection to the broker was lost 41 | * 42 | * @return bool TRUE on success or throws on failure 43 | */ 44 | public function publishMessage(string $message, int $flags = AMQP_NOPARAM, array $attributes = [], array $routingKeys = []): bool 45 | { 46 | // Merge attributes 47 | $attributes = empty($attributes) ? $this->exchangeOptions['publish_attributes'] : 48 | (empty($this->exchangeOptions['publish_attributes']) ? $attributes : 49 | array_merge($this->exchangeOptions['publish_attributes'], $attributes)); 50 | 51 | $routingKeys = !empty($routingKeys) ? $routingKeys : $this->exchangeOptions['routing_keys']; 52 | 53 | if ($this->eventDispatcher) { 54 | $prePublishEvent = new PrePublishEvent($message, $routingKeys, $flags, $attributes); 55 | $this->eventDispatcher->dispatch($prePublishEvent, PrePublishEvent::NAME); 56 | 57 | if (!$prePublishEvent->canPublish()) { 58 | return true; 59 | } 60 | 61 | $routingKeys = $prePublishEvent->getRoutingKeys(); 62 | $message = $prePublishEvent->getMessage(); 63 | $flags = $prePublishEvent->getFlags(); 64 | $attributes = $prePublishEvent->getAttributes(); 65 | } 66 | 67 | if (!$routingKeys) { 68 | $this->call($this->exchange, 'publish', [$message, null, $flags, $attributes]); 69 | 70 | return true; 71 | } 72 | 73 | // Publish the message for each routing keys 74 | foreach ($routingKeys as $routingKey) { 75 | $this->call($this->exchange, 'publish', [$message, $routingKey, $flags, $attributes]); 76 | } 77 | 78 | return true; 79 | } 80 | 81 | public function getExchange(): \AMQPExchange 82 | { 83 | return $this->exchange; 84 | } 85 | 86 | public function setExchange(\AMQPExchange $exchange): self 87 | { 88 | $this->exchange = $exchange; 89 | 90 | return $this; 91 | } 92 | 93 | public function getExchangeOptions(): array 94 | { 95 | return $this->exchangeOptions; 96 | } 97 | 98 | public function setExchangeOptions(array $exchangeOptions): self 99 | { 100 | $this->exchangeOptions = $exchangeOptions; 101 | 102 | if (!\array_key_exists('publish_attributes', $this->exchangeOptions)) { 103 | $this->exchangeOptions['publish_attributes'] = []; 104 | } 105 | 106 | if (!\array_key_exists('routing_keys', $this->exchangeOptions)) { 107 | $this->exchangeOptions['routing_keys'] = []; 108 | } 109 | 110 | return $this; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/AmqpBundle/Tests/Units/Factory/ConsumerFactory.php: -------------------------------------------------------------------------------- 1 | if($channelClass = '\AMQPChannel') 18 | ->and($queueClass = '\AMQPQueue') 19 | ->and($exchangeClass = '\AMQPExchange') 20 | ->object($factory = new Base($channelClass, $queueClass, $exchangeClass)) 21 | ->isInstanceOf(Base::class); 22 | 23 | $this 24 | ->if($channelClass = '\DateTime') 25 | ->and($queueClass = '\AMQPQueue') 26 | ->and($exchangeClass = '\AMQPExchange') 27 | ->exception( 28 | function () use ($channelClass, $queueClass, $exchangeClass): void { 29 | $factory = new Base($channelClass, $queueClass, $exchangeClass); 30 | }, 31 | ) 32 | ->isInstanceOf('InvalidArgumentException') 33 | ->hasMessage("channelClass '\DateTime' doesn't exist or not a AMQPChannel"); 34 | 35 | $this 36 | ->if($channelClass = '\AMQPChannel') 37 | ->and($queueClass = '\DateTime') 38 | ->and($exchangeClass = '\AMQPExchange') 39 | ->exception( 40 | function () use ($channelClass, $queueClass, $exchangeClass): void { 41 | $factory = new Base($channelClass, $queueClass, $exchangeClass); 42 | }, 43 | ) 44 | ->isInstanceOf('InvalidArgumentException') 45 | ->hasMessage("exchangeClass '\DateTime' doesn't exist or not a AMQPQueue"); 46 | 47 | $this 48 | ->if($channelClass = '\AMQPChannel') 49 | ->and($queueClass = '\AMQPQueue') 50 | ->and($exchangeClass = '\DateTime') 51 | ->exception( 52 | function () use ($channelClass, $queueClass, $exchangeClass): void { 53 | $factory = new Base($channelClass, $queueClass, $exchangeClass); 54 | }, 55 | ) 56 | ->isInstanceOf('InvalidArgumentException') 57 | ->hasMessage("exchangeClass '\DateTime' doesn't exist or not a AMQPExchange"); 58 | } 59 | 60 | public function testFactory(): void 61 | { 62 | $this->mockGenerator->orphanize('__construct'); 63 | $this->mockGenerator->shuntParentClassCalls(); 64 | 65 | $connexion = new \mock\AMQPConnection(); 66 | 67 | $this 68 | ->if($channelClass = '\mock\M6Web\Bundle\AmqpBundle\Tests\Units\Factory\Mock\MockAMQPChannel') 69 | ->and($queueClass = '\mock\M6Web\Bundle\AmqpBundle\Tests\Units\Factory\Mock\MockAMQPQueue') 70 | ->and($exchangeClass = '\mock\M6Web\Bundle\AmqpBundle\Tests\Units\Factory\Mock\MockAMQPExchange') 71 | ->and($consumerClass = '\mock\M6Web\Bundle\AmqpBundle\Amqp\Consumer') 72 | ->and($exchangeOptions = [ 73 | 'name' => 'myexchange', 74 | 'type' => 'type', 75 | 'passive' => false, 76 | 'durable' => true, 77 | 'auto_delete' => false, 78 | 'routing_keys' => ['key'], 79 | 'arguments' => ['alternate-exchange' => 'my-ae'], 80 | ]) 81 | ->and($queueOptions = [ 82 | 'name' => 'myqueue', 83 | 'arguments' => [], 84 | 'passive' => false, 85 | 'durable' => true, 86 | 'exclusive' => true, 87 | 'auto_delete' => false, 88 | 'routing_keys' => ['key'], 89 | ]) 90 | ->and($qosOptions = [ 91 | 'prefetch_size' => 0, 92 | 'prefetch_count' => 0, 93 | ]) 94 | ->and($factory = new Base($channelClass, $queueClass, $exchangeClass)) 95 | ->object($factory->get($consumerClass, $connexion, $exchangeOptions, $queueOptions, false, $qosOptions)) 96 | ->isInstanceOf($consumerClass); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/AmqpBundle/Amqp/Consumer.php: -------------------------------------------------------------------------------- 1 | call($this->queue, 'get', [$flags]); 32 | 33 | if ($this->eventDispatcher) { 34 | $preRetrieveEvent = new PreRetrieveEvent($envelope); 35 | $this->eventDispatcher->dispatch($preRetrieveEvent, PreRetrieveEvent::NAME); 36 | 37 | return $preRetrieveEvent->getEnvelope(); 38 | } 39 | 40 | return $envelope; 41 | } 42 | 43 | /** 44 | * Acknowledge the receipt of a message. 45 | * 46 | * @param int $deliveryTag delivery tag of last message to ack 47 | * @param int $flags AMQP_MULTIPLE or AMQP_NOPARAM 48 | * 49 | * @throws \AMQPChannelException if the channel is not open 50 | * @throws \AMQPConnectionException if the connection to the broker was lost 51 | */ 52 | public function ackMessage(int $deliveryTag, int $flags = AMQP_NOPARAM): void 53 | { 54 | if ($this->eventDispatcher) { 55 | $ackEvent = new AckEvent($deliveryTag, $flags); 56 | 57 | $this->eventDispatcher->dispatch($ackEvent, AckEvent::NAME); 58 | } 59 | 60 | $this->call($this->queue, 'ack', [$deliveryTag, $flags]); 61 | } 62 | 63 | /** 64 | * Mark a message as explicitly not acknowledged. 65 | * 66 | * @param int $deliveryTag delivery tag of last message to nack 67 | * @param int $flags AMQP_NOPARAM or AMQP_REQUEUE to requeue the message(s) 68 | * 69 | * @throws \AMQPConnectionException if the connection to the broker was lost 70 | * @throws \AMQPChannelException if the channel is not open 71 | */ 72 | public function nackMessage(int $deliveryTag, int $flags = AMQP_NOPARAM): void 73 | { 74 | if ($this->eventDispatcher) { 75 | $nackEvent = new NackEvent($deliveryTag, $flags); 76 | 77 | $this->eventDispatcher->dispatch($nackEvent, NackEvent::NAME); 78 | } 79 | 80 | $this->call($this->queue, 'nack', [$deliveryTag, $flags]); 81 | } 82 | 83 | /** 84 | * Purge the contents of the queue. 85 | * 86 | * @throws \AMQPChannelException if the channel is not open 87 | * @throws \AMQPConnectionException if the connection to the broker was lost 88 | */ 89 | public function purge(): int 90 | { 91 | if ($this->eventDispatcher) { 92 | $purgeEvent = new PurgeEvent($this->queue); 93 | 94 | $this->eventDispatcher->dispatch($purgeEvent, PurgeEvent::NAME); 95 | } 96 | 97 | return $this->call($this->queue, 'purge'); 98 | } 99 | 100 | /** 101 | * Get the current message count. 102 | */ 103 | public function getCurrentMessageCount(): int 104 | { 105 | // Save the current queue flags and setup the queue in passive mode 106 | $flags = $this->queue->getFlags(); 107 | $this->queue->setFlags($flags | AMQP_PASSIVE); 108 | 109 | // Declare the queue again as passive to get the count of messages 110 | $messagesCount = $this->queue->declareQueue(); 111 | 112 | // Restore the queue flags 113 | $this->queue->setFlags($flags); 114 | 115 | return $messagesCount; 116 | } 117 | 118 | public function getQueue(): \AMQPQueue 119 | { 120 | return $this->queue; 121 | } 122 | 123 | public function setQueue(\AMQPQueue $queue): Consumer 124 | { 125 | $this->queue = $queue; 126 | 127 | return $this; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/AmqpBundle/Factory/ConsumerFactory.php: -------------------------------------------------------------------------------- 1 | channelClass = $channelClass; 43 | $this->queueClass = $queueClass; 44 | $this->exchangeClass = $exchangeClass; 45 | } 46 | 47 | /** 48 | * build the consumer class. 49 | * 50 | * @param class-string $class Consumer class name 51 | * @param \AMQPConnection $connexion AMQP connexion 52 | * @param array $exchangeOptions Exchange Options 53 | * @param array $queueOptions Queue Options 54 | * @param bool $lazy Specifies if it should connect 55 | * @param array $qosOptions Qos Options 56 | */ 57 | public function get(string $class, \AMQPConnection $connexion, array $exchangeOptions, array $queueOptions, bool $lazy = false, array $qosOptions = []): Consumer 58 | { 59 | if (!class_exists($class)) { 60 | throw new \InvalidArgumentException( 61 | sprintf("Consumer class '%s' doesn't exist", $class), 62 | ); 63 | } 64 | 65 | if ($lazy) { 66 | if (!$connexion->isConnected()) { 67 | $connexion->connect(); 68 | } 69 | } 70 | 71 | /** @var \AMQPChannel $channel */ 72 | $channel = new $this->channelClass($connexion); 73 | 74 | if (isset($qosOptions['prefetch_size'])) { 75 | $channel->setPrefetchSize($qosOptions['prefetch_size']); 76 | } 77 | if (isset($qosOptions['prefetch_count'])) { 78 | $channel->setPrefetchCount($qosOptions['prefetch_count']); 79 | } 80 | 81 | // ensure that exchange exists 82 | $this->createExchange($this->exchangeClass, $channel, $exchangeOptions); 83 | 84 | /** @var \AMQPQueue $queue */ 85 | $queue = new $this->queueClass($channel); 86 | if (!empty($queueOptions['name'])) { 87 | $queue->setName($queueOptions['name']); 88 | } 89 | $queue->setArguments($queueOptions['arguments']); 90 | $queue->setFlags( 91 | ($queueOptions['passive'] ? AMQP_PASSIVE : AMQP_NOPARAM) | 92 | ($queueOptions['durable'] ? AMQP_DURABLE : AMQP_NOPARAM) | 93 | ($queueOptions['exclusive'] ? AMQP_EXCLUSIVE : AMQP_NOPARAM) | 94 | ($queueOptions['auto_delete'] ? AMQP_AUTODELETE : AMQP_NOPARAM), 95 | ); 96 | 97 | // Declare the queue 98 | $queue->declareQueue(); 99 | 100 | if (\count($queueOptions['routing_keys'])) { 101 | foreach ($queueOptions['routing_keys'] as $routingKey) { 102 | $queue->bind($exchangeOptions['name'], $routingKey); 103 | } 104 | } else { 105 | $queue->bind($exchangeOptions['name']); 106 | } 107 | 108 | $consumer = new $class($queue, $queueOptions); 109 | 110 | if (!$consumer instanceof Consumer) { 111 | throw new \InvalidArgumentException("$class must be an instance of Consumer"); 112 | } 113 | 114 | return $consumer; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/AmqpBundle/Tests/Units/DependencyInjection/M6WebAmqpExtension.php: -------------------------------------------------------------------------------- 1 | true]); 27 | $container = new ContainerBuilder($parameterBag); 28 | 29 | $parser = new Parser(); 30 | $config = $parser->parseFile(__DIR__.'/../../Fixtures/'.$fixtureName.'.yml'); 31 | 32 | $extension->load($config, $container); 33 | 34 | return $container; 35 | } 36 | 37 | public function testQueueArgumentsConfig(): void 38 | { 39 | $container = $this->getContainerForConfiguration('queue-arguments-config'); 40 | 41 | // test producer queue options 42 | $this 43 | ->boolean($container->hasDefinition('m6_web_amqp.producer.producer_2')) 44 | ->isTrue() 45 | ->boolean($container->getDefinition('m6_web_amqp.producer.producer_2')->isShared()) 46 | ->isFalse() 47 | ->array($queueOptions = $container->getDefinition('m6_web_amqp.producer.producer_2')->getArgument(3)) 48 | ->hasSize(6) 49 | ->string($queueOptions['name']) 50 | ->isEqualTo('ha.queue_exchange_2') 51 | ->array($arguments = $queueOptions['arguments']) 52 | ->hasSize(1) 53 | ->hasKey('x-message-ttl') 54 | ->contains(1000) 55 | ->boolean($queueOptions['passive']) 56 | ->isFalse() 57 | ->boolean($queueOptions['durable']) 58 | ->isTrue() 59 | ->boolean($queueOptions['auto_delete']) 60 | ->isFalse() 61 | ->array($queueOptions['routing_keys']) 62 | ->isEmpty() 63 | ; 64 | 65 | // test consumer queue options 66 | $this 67 | ->boolean($container->hasDefinition('m6_web_amqp.consumer.consumer_1')) 68 | ->isTrue() 69 | ->boolean($container->getDefinition('m6_web_amqp.consumer.consumer_1')->isShared()) 70 | ->isFalse() 71 | ->array($queueOptions = $container->getDefinition('m6_web_amqp.consumer.consumer_1')->getArgument(3)) 72 | ->hasSize(7) 73 | ->string($queueOptions['name']) 74 | ->isEqualTo('ha.queue_exchange_1') 75 | ->array($arguments = $queueOptions['arguments']) 76 | ->hasSize(1) 77 | ->hasKey('x-dead-letter-exchange') 78 | ->contains('exchange_2') 79 | ->boolean($queueOptions['passive']) 80 | ->isFalse() 81 | ->boolean($queueOptions['durable']) 82 | ->isTrue() 83 | ->boolean($queueOptions['auto_delete']) 84 | ->isFalse() 85 | ->array($queueOptions['routing_keys']) 86 | ->hasSize(1) 87 | ->contains('super_routing_key') 88 | ; 89 | 90 | // test connection options 91 | $this 92 | ->boolean($container->hasDefinition('m6_web_amqp.connection.with_heartbeat')) 93 | ->isTrue() 94 | ->array($connectionArguments = $container->getDefinition('m6_web_amqp.connection.with_heartbeat')->getArguments()) 95 | ->hasSize(1) 96 | ->integer($connectionArguments[0]['heartbeat']) 97 | ->isEqualTo(1); 98 | } 99 | 100 | public function testSandboxClasses(): void 101 | { 102 | $container = $this->getContainerForConfiguration('queue-arguments-config'); 103 | 104 | $this 105 | ->string($container->getParameter('m6_web_amqp.exchange.class')) 106 | ->isEqualTo(NullExchange::class) 107 | ->string($container->getParameter('m6_web_amqp.queue.class')) 108 | ->isEqualTo(NullQueue::class) 109 | ->string($container->getParameter('m6_web_amqp.connection.class')) 110 | ->isEqualTo(NullConnection::class) 111 | ->string($container->getParameter('m6_web_amqp.channel.class')) 112 | ->isEqualTo(NullChannel::class) 113 | ->string($container->getParameter('m6_web_amqp.envelope.class')) 114 | ->isEqualTo(NullEnvelope::class); 115 | } 116 | 117 | public function testDefaultConfiguration(): void 118 | { 119 | $container = $this->getContainerForConfiguration('queue-defaults'); 120 | 121 | // sandbox is off by default, check indirectly via classes definition 122 | $this 123 | ->string($container->getParameter('m6_web_amqp.exchange.class')) 124 | ->isEqualTo('AMQPExchange') 125 | ->string($container->getParameter('m6_web_amqp.queue.class')) 126 | ->isEqualTo('AMQPQueue') 127 | ->string($container->getParameter('m6_web_amqp.connection.class')) 128 | ->isEqualTo('AMQPConnection') 129 | ->string($container->getParameter('m6_web_amqp.channel.class')) 130 | ->isEqualTo('AMQPChannel') 131 | ->string($container->getParameter('m6_web_amqp.envelope.class')) 132 | ->isEqualTo('AMQPEnvelope'); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/AmqpBundle/Sandbox/NullEnvelope.php: -------------------------------------------------------------------------------- 1 | body; 36 | } 37 | 38 | public function setBody(string $body): void 39 | { 40 | $this->body = $body; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function getRoutingKey(): string 47 | { 48 | return $this->routingKey; 49 | } 50 | 51 | public function setRoutingKey(string $routingKey): void 52 | { 53 | $this->routingKey = $routingKey; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function getDeliveryTag(): int 60 | { 61 | return $this->deliveryTag; 62 | } 63 | 64 | public function setDeliveryTag(int $deliveryTag): void 65 | { 66 | $this->deliveryTag = $deliveryTag; 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function getDeliveryMode(): int 73 | { 74 | return $this->deliveryMode; 75 | } 76 | 77 | public function setDeliveryMode(int $deliveryMode): void 78 | { 79 | $this->deliveryMode = $deliveryMode; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function isRedelivery(): bool 86 | { 87 | return $this->redelivery; 88 | } 89 | 90 | public function setRedelivery(bool $redelivery): void 91 | { 92 | $this->redelivery = $redelivery; 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function getContentType(): string 99 | { 100 | return $this->contentType; 101 | } 102 | 103 | public function setContentType(string $contentType): void 104 | { 105 | $this->contentType = $contentType; 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | public function getContentEncoding(): string 112 | { 113 | return $this->contentEncoding; 114 | } 115 | 116 | public function setContentEncoding(string $contentEncoding): void 117 | { 118 | $this->contentEncoding = $contentEncoding; 119 | } 120 | 121 | /** 122 | * {@inheritdoc} 123 | */ 124 | public function getType(): string 125 | { 126 | return $this->type; 127 | } 128 | 129 | public function setType(string $type): void 130 | { 131 | $this->type = $type; 132 | } 133 | 134 | /** 135 | * {@inheritdoc} 136 | */ 137 | public function getTimestamp(): ?int 138 | { 139 | return $this->timestamp; 140 | } 141 | 142 | public function setTimestamp(string $timestamp): void 143 | { 144 | $this->timestamp = $timestamp; 145 | } 146 | 147 | /** 148 | * {@inheritdoc} 149 | */ 150 | public function getPriority(): int 151 | { 152 | return $this->priority; 153 | } 154 | 155 | public function setPriority(int $priority): void 156 | { 157 | $this->priority = $priority; 158 | } 159 | 160 | /** 161 | * {@inheritdoc} 162 | */ 163 | public function getExpiration(): string 164 | { 165 | return $this->expiration; 166 | } 167 | 168 | public function setExpiration(string $expiration): void 169 | { 170 | $this->expiration = $expiration; 171 | } 172 | 173 | /** 174 | * {@inheritdoc} 175 | */ 176 | public function getUserId(): string 177 | { 178 | return $this->userId; 179 | } 180 | 181 | public function setUserId(string $userId): void 182 | { 183 | $this->userId = $userId; 184 | } 185 | 186 | /** 187 | * {@inheritdoc} 188 | */ 189 | public function getAppId(): string 190 | { 191 | return $this->appId; 192 | } 193 | 194 | public function setAppId(string $appId): void 195 | { 196 | $this->appId = $appId; 197 | } 198 | 199 | /** 200 | * {@inheritdoc} 201 | */ 202 | public function getMessageId(): string 203 | { 204 | return $this->messageId; 205 | } 206 | 207 | public function setMessageId(string $messageId): void 208 | { 209 | $this->messageId = $messageId; 210 | } 211 | 212 | /** 213 | * {@inheritdoc} 214 | */ 215 | public function getReplyTo(): string 216 | { 217 | return $this->replyTo; 218 | } 219 | 220 | public function setReplyTo(string $replyTo): void 221 | { 222 | $this->replyTo = $replyTo; 223 | } 224 | 225 | /** 226 | * {@inheritdoc} 227 | */ 228 | public function getCorrelationId(): string 229 | { 230 | return $this->correlationId; 231 | } 232 | 233 | public function setCorrelationId(string $correlationId): void 234 | { 235 | $this->correlationId = $correlationId; 236 | } 237 | 238 | /** 239 | * {@inheritdoc} 240 | */ 241 | public function getHeaders(): array 242 | { 243 | return $this->headers; 244 | } 245 | 246 | public function setHeaders(array $headers): void 247 | { 248 | $this->headers = $headers; 249 | } 250 | 251 | /** 252 | * {@inheritdoc} 253 | */ 254 | public function getHeader(string $headerName): ?string 255 | { 256 | return $this->headers[$headerName] ?? null; 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/AmqpBundle/DependencyInjection/M6WebAmqpExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 28 | 29 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 30 | $loader->load('services.yml'); 31 | 32 | if ($container->getParameter('kernel.debug')) { 33 | $loader->load('data_collector.yml'); 34 | } 35 | 36 | if ($config['sandbox']['enabled']) { 37 | $loader->load('sandbox_services.yml'); 38 | } 39 | 40 | $this->loadConnections($container, $config); 41 | $this->loadProducers($container, $config); 42 | $this->loadConsumers($container, $config); 43 | } 44 | 45 | protected function loadConnections(ContainerBuilder $container, array $config): void 46 | { 47 | foreach ($config['connections'] as $key => $connection) { 48 | $connectionDefinition = new Definition($connection['class']); 49 | $connectionDefinition->addMethodCall('setHost ', [$connection['host']]) 50 | ->addMethodCall('setPort', [$connection['port']]) 51 | ->addMethodCall('setReadTimeout', [$connection['timeout']]) 52 | ->addMethodCall('setLogin', [$connection['login']]) 53 | ->addMethodCall('setPassword', [$connection['password']]) 54 | ->addMethodCall('setVhost', [$connection['vhost']]); 55 | 56 | $connectionDefinition->setArguments([['heartbeat' => $connection['heartbeat']]]); 57 | 58 | if ($config['prototype']) { 59 | $connectionDefinition->setShared(false); 60 | } 61 | 62 | if (!$connection['lazy']) { 63 | $connectionDefinition->addMethodCall('connect'); 64 | } 65 | 66 | $container->setDefinition( 67 | sprintf('m6_web_amqp.connection.%s', $key), 68 | $connectionDefinition, 69 | ); 70 | } 71 | } 72 | 73 | protected function loadProducers(ContainerBuilder $container, array $config): void 74 | { 75 | foreach ($config['producers'] as $key => $producer) { 76 | $lazy = $config['connections'][$producer['connection']]['lazy']; 77 | 78 | // Create the producer with the factory 79 | $producerDefinition = new Definition( 80 | $producer['class'], 81 | [ 82 | $producer['class'], 83 | new Reference(sprintf('m6_web_amqp.connection.%s', $producer['connection'])), 84 | $producer['exchange_options'], 85 | $producer['queue_options'], 86 | $lazy, 87 | ], 88 | ); 89 | 90 | $this->setEventDispatcher($container, $config['event_dispatcher'], $producerDefinition); 91 | 92 | // Use a factory to build the producer 93 | $producerDefinition->setFactory([ 94 | new Reference('m6_web_amqp.producer_factory'), 95 | 'get', 96 | ]); 97 | 98 | if ($config['prototype']) { 99 | $producerDefinition->setShared(false); 100 | } 101 | 102 | if ($lazy) { 103 | if (!method_exists($producerDefinition, 'setLazy')) { 104 | throw new \InvalidArgumentException('It\'s not possible to declare a service as lazy. Are you using Symfony 2.3?'); 105 | } 106 | 107 | $producerDefinition->setLazy(true); 108 | } 109 | 110 | $producerDefinition->addTag('m6_web_amqp.producers'); 111 | $container->setDefinition( 112 | sprintf('m6_web_amqp.producer.%s', $key), 113 | $producerDefinition, 114 | ); 115 | } 116 | } 117 | 118 | protected function loadConsumers(ContainerBuilder $container, array $config): void 119 | { 120 | foreach ($config['consumers'] as $key => $consumer) { 121 | $lazy = $config['connections'][$consumer['connection']]['lazy']; 122 | 123 | // Create the consumer with the factory 124 | $consumerDefinition = new Definition( 125 | $consumer['class'], 126 | [ 127 | $consumer['class'], 128 | new Reference(sprintf('m6_web_amqp.connection.%s', $consumer['connection'])), 129 | $consumer['exchange_options'], 130 | $consumer['queue_options'], 131 | $lazy, 132 | $consumer['qos_options'], 133 | ], 134 | ); 135 | 136 | $this->setEventDispatcher($container, $config['event_dispatcher'], $consumerDefinition); 137 | 138 | // Use a factory to build the consumer 139 | $consumerDefinition->setFactory([ 140 | new Reference('m6_web_amqp.consumer_factory'), 141 | 'get', 142 | ]); 143 | 144 | if ($config['prototype']) { 145 | $consumerDefinition->setShared(false); 146 | } 147 | 148 | if ($lazy) { 149 | if (!method_exists($consumerDefinition, 'setLazy')) { 150 | throw new \InvalidArgumentException('It\'s not possible to declare a service as lazy. Are you using Symfony 2.3?'); 151 | } 152 | 153 | $consumerDefinition->setLazy(true); 154 | } 155 | 156 | $consumerDefinition->addTag('m6_web_amqp.consumers'); 157 | $container->setDefinition( 158 | sprintf('m6_web_amqp.consumer.%s', $key), 159 | $consumerDefinition, 160 | ); 161 | } 162 | } 163 | 164 | private function setEventDispatcher(ContainerBuilder $container, bool $enableEventDispatcher, Definition $definition): void 165 | { 166 | // Add the Event dispatcher & Command Event 167 | if ($enableEventDispatcher === true) { 168 | $definition->addMethodCall( 169 | 'setEventDispatcher', 170 | [ 171 | new Reference('event_dispatcher'), 172 | $container->getParameter('m6_web_amqp.event.command.class'), 173 | ], 174 | ); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/AmqpBundle/Tests/Units/Amqp/Producer.php: -------------------------------------------------------------------------------- 1 | if($exchange = $this->getExchange()) 18 | ->if($exchangeOptions = ['options' => 'test']) 19 | ->and($producer = new Base($exchange, $exchangeOptions)) 20 | ->object($producer->getExchange()) 21 | ->isIdenticalTo($exchange) 22 | ->array($producer->getExchangeOptions()) 23 | ->hasKey('publish_attributes') 24 | ->hasKey('routing_keys') 25 | ->hasKey('options') 26 | ->contains('test'); 27 | } 28 | 29 | public function testSetOptions(): void 30 | { 31 | $this 32 | ->if($exchange = $this->getExchange()) 33 | ->and($exchangeOptions = ['publish_attributes' => ['test1'], 'routing_keys' => ['test2']]) 34 | ->and($producer = new Base($exchange, $exchangeOptions)) 35 | ->array($producer->getExchangeOptions()) 36 | ->isEqualTo($exchangeOptions); 37 | } 38 | 39 | public function testSendMessagesOk(): void 40 | { 41 | $msgList = []; 42 | 43 | $this 44 | ->if($msgList = []) 45 | ->and($exchange = $this->getExchange($msgList)) 46 | ->and($exchangeOptions = [ 47 | 'publish_attributes' => ['attr_test' => 'value'], 48 | 'routing_keys' => ['routing_test'], 49 | ]) 50 | ->and($producer = new Base($exchange, $exchangeOptions)) 51 | ->boolean($producer->publishMessage('message1')) 52 | ->isTrue() 53 | ->boolean($producer->publishMessage('message2')) 54 | ->isTrue() 55 | ->array($msgList) 56 | ->isEqualTo([ 57 | ['message1', 'routing_test', AMQP_NOPARAM, $exchangeOptions['publish_attributes']], 58 | ['message2', 'routing_test', AMQP_NOPARAM, $exchangeOptions['publish_attributes']], 59 | ]); 60 | } 61 | 62 | public function testSendMessagesError(): void 63 | { 64 | $msgList = []; 65 | 66 | $this 67 | ->if($msgList = []) 68 | ->and($exchange = $this->getExchange($msgList)) 69 | ->and($exchangeOptions = [ 70 | 'publish_attributes' => ['attr_test' => 'value'], 71 | 'routing_keys' => ['routing_test', 'error'], 72 | ]) 73 | ->and($producer = new Base($exchange, $exchangeOptions)) 74 | ->boolean($producer->publishMessage('message1')) 75 | ->isTrue() 76 | ->exception( 77 | function () use ($producer): void { 78 | $producer->publishMessage('error'); 79 | }, 80 | )->isInstanceOf(\AMQPExchangeException::class) 81 | ->array($msgList) 82 | ->isEqualTo([ 83 | ['message1', 'routing_test', AMQP_NOPARAM, $exchangeOptions['publish_attributes']], 84 | ['message1', 'error', AMQP_NOPARAM, $exchangeOptions['publish_attributes']], 85 | ['error', 'routing_test', AMQP_NOPARAM, $exchangeOptions['publish_attributes']], 86 | ]) 87 | ->notContains(['error', 'error', AMQP_NOPARAM, $exchangeOptions['publish_attributes']]); 88 | } 89 | 90 | public function testSendMessagesWithAttributes(): void 91 | { 92 | $msgList = []; 93 | 94 | // To verify merged attributs 95 | $this 96 | ->if($msgList = []) 97 | ->and($exchange = $this->getExchange($msgList)) 98 | ->and($exchangeOptions = [ 99 | 'publish_attributes' => ['attr1' => 'value', 'attr2' => 'value2'], 100 | 'routing_keys' => ['routing_test', 'routing_test2'], 101 | ]) 102 | ->and($msgAttr = ['attr2' => 'overload', 'attr3' => 'value3']) 103 | ->and($msgAttrMerged = ['attr1' => 'value', 'attr2' => 'overload', 'attr3' => 'value3']) 104 | 105 | ->and($producer = new Base($exchange, $exchangeOptions)) 106 | ->boolean($producer->publishMessage('message1')) 107 | ->isTrue() 108 | ->boolean($producer->publishMessage('message2', AMQP_IMMEDIATE, $msgAttr)) 109 | ->isTrue() 110 | ->array($msgList) 111 | ->isEqualTo([ 112 | ['message1', 'routing_test', AMQP_NOPARAM, $exchangeOptions['publish_attributes']], 113 | ['message1', 'routing_test2', AMQP_NOPARAM, $exchangeOptions['publish_attributes']], 114 | ['message2', 'routing_test', AMQP_IMMEDIATE, $msgAttrMerged], 115 | ['message2', 'routing_test2', AMQP_IMMEDIATE, $msgAttrMerged], 116 | ]); 117 | } 118 | 119 | public function testSendMessageOk(): void 120 | { 121 | $this 122 | ->if($exchange = $this->getExchange()) 123 | ->and($exchangeOptions = ['publish_attributes' => ['attr_test'], 'routing_keys' => ['routing_test']]) 124 | ->and($producer = new Base($exchange, $exchangeOptions)) 125 | ->boolean($producer->publishMessage('message1')) 126 | ->isTrue() 127 | ->boolean($producer->publishMessage('message2')) 128 | ->isTrue(); 129 | } 130 | 131 | public function testSendMessageWithOverridedRoutingKey(): void 132 | { 133 | $msgList = []; 134 | 135 | // To verify merged attributs 136 | $this 137 | ->if($msgList = []) 138 | ->and($exchange = $this->getExchange($msgList)) 139 | ->and($exchangeOptions = [ 140 | 'routing_keys' => ['routing_test', 'routing_test2'], 141 | ]) 142 | 143 | ->and($producer = new Base($exchange, $exchangeOptions)) 144 | ->boolean($producer->publishMessage('message1')) 145 | ->isTrue() 146 | ->boolean($producer->publishMessage('message2', AMQP_IMMEDIATE, [], ['routing_override1', 'routing_override2'])) 147 | ->isTrue() 148 | ->array($msgList) 149 | ->isEqualTo([ 150 | ['message1', 'routing_test', AMQP_NOPARAM, []], 151 | ['message1', 'routing_test2', AMQP_NOPARAM, []], 152 | ['message2', 'routing_override1', AMQP_IMMEDIATE, []], 153 | ['message2', 'routing_override2', AMQP_IMMEDIATE, []], 154 | ]); 155 | } 156 | 157 | public function testSendMessagesWithoutRoutingKey(): void 158 | { 159 | $msgList = []; 160 | 161 | $this 162 | ->if($msgList = []) 163 | ->and($exchange = $this->getExchange($msgList)) 164 | ->and($exchangeOptions = [ 165 | 'publish_attributes' => ['attr_test' => 'value'], 166 | 'routing_keys' => [], 167 | ]) 168 | ->and($producer = new Base($exchange, $exchangeOptions)) 169 | ->boolean($producer->publishMessage('message1')) 170 | ->isTrue() 171 | ->boolean($producer->publishMessage('message2')) 172 | ->isTrue() 173 | ->array($msgList) 174 | ->isEqualTo([ 175 | ['message1', null, AMQP_NOPARAM, $exchangeOptions['publish_attributes']], 176 | ['message2', null, AMQP_NOPARAM, $exchangeOptions['publish_attributes']], 177 | ]); 178 | } 179 | 180 | protected function getExchange(&$msgList = []) 181 | { 182 | $this->mockGenerator->orphanize('__construct'); 183 | $this->mockGenerator->shuntParentClassCalls(); 184 | 185 | $exchange = new \mock\AMQPExchange(); 186 | 187 | $exchange->getMockController()->publish = function ($message, $routing_key, $flags = AMQP_NOPARAM, array $attributes = []) use (&$msgList) { 188 | if (($message == 'error') && ($routing_key == 'error')) { 189 | throw new \AMQPExchangeException(); 190 | } 191 | 192 | $msgList[] = [$message, $routing_key, $flags, $attributes]; 193 | 194 | return true; 195 | }; 196 | 197 | return $exchange; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/AmqpBundle/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 28 | 29 | $rootNode 30 | ->children() 31 | ->booleanNode('debug')->defaultValue('%kernel.debug%')->end() 32 | ->booleanNode('event_dispatcher')->defaultTrue()->end() 33 | ->booleanNode('prototype')->defaultFalse()->end() 34 | ->arrayNode('sandbox') 35 | ->addDefaultsIfNotSet() 36 | ->children() 37 | ->booleanNode('enabled') 38 | ->defaultFalse() 39 | ->info('Sandbox mode do not establish connections but mocks them via in-memory queue. Useful for tests') 40 | ->end() 41 | ->end() 42 | ->end() 43 | ->end(); 44 | 45 | $this->addConnections($rootNode); 46 | $this->addProducers($rootNode); 47 | $this->addConsumers($rootNode); 48 | 49 | return $treeBuilder; 50 | } 51 | 52 | protected function addConnections(ArrayNodeDefinition $node): void 53 | { 54 | $node 55 | ->children() 56 | ->arrayNode('connections') 57 | ->useAttributeAsKey('key') 58 | ->canBeUnset() 59 | ->arrayPrototype() 60 | ->children() 61 | ->scalarNode('class')->defaultValue('%m6_web_amqp.connection.class%')->end() 62 | ->scalarNode('host')->defaultValue('localhost')->end() 63 | ->scalarNode('port')->defaultValue(5672)->end() 64 | ->scalarNode('timeout')->defaultValue(10)->end() 65 | ->scalarNode('heartbeat') 66 | ->defaultValue(5) 67 | ->info('Send heartbeat every N seconds. RabbitMQ recommend to use timeout / 2. Supported from 1.6.0beta4 version of amqp extension') 68 | ->end() 69 | ->scalarNode('login')->defaultValue('guest')->end() 70 | ->scalarNode('password')->defaultValue('guest')->end() 71 | ->scalarNode('vhost')->defaultValue('/')->end() 72 | ->booleanNode('lazy')->defaultFalse()->end() 73 | ->end() 74 | ->end() 75 | ->end() 76 | ->end(); 77 | } 78 | 79 | protected function addProducers(ArrayNodeDefinition $node): void 80 | { 81 | $node 82 | ->children() 83 | ->arrayNode('producers') 84 | ->canBeUnset() 85 | ->useAttributeAsKey('key') 86 | ->arrayPrototype() 87 | ->children() 88 | ->scalarNode('class')->defaultValue('%m6_web_amqp.producer.class%')->end() 89 | ->scalarNode('connection')->defaultValue('default')->end() 90 | ->append($this->exchangeOptions()) 91 | ->arrayNode('queue_options') 92 | ->addDefaultsIfNotSet() 93 | ->children() 94 | // base info 95 | ->scalarNode('name')->end() 96 | 97 | // flags 98 | ->booleanNode('passive')->defaultFalse()->end() 99 | ->booleanNode('durable')->defaultTrue()->end() 100 | ->booleanNode('auto_delete')->defaultFalse()->end() 101 | 102 | // args 103 | ->arrayNode('arguments') 104 | ->prototype('scalar')->end() 105 | ->defaultValue([]) 106 | ->normalizeKeys(false) 107 | ->end() 108 | 109 | // binding 110 | ->arrayNode('routing_keys') 111 | ->prototype('scalar')->end() 112 | ->defaultValue([]) 113 | ->end() 114 | ->end() 115 | ->end() 116 | ->end() 117 | ->end() 118 | ->end() 119 | ->end(); 120 | } 121 | 122 | protected function addConsumers(ArrayNodeDefinition $node): void 123 | { 124 | $node 125 | ->children() 126 | ->arrayNode('consumers') 127 | ->canBeUnset() 128 | ->useAttributeAsKey('key') 129 | ->arrayPrototype() 130 | ->children() 131 | ->scalarNode('class')->defaultValue('%m6_web_amqp.consumer.class%')->end() 132 | ->scalarNode('connection')->defaultValue('default')->end() 133 | ->append($this->exchangeOptions()) 134 | ->arrayNode('queue_options') 135 | ->children() 136 | // base 137 | ->scalarNode('name')->isRequired()->end() 138 | 139 | // flags 140 | ->booleanNode('passive')->defaultFalse()->end() 141 | ->booleanNode('durable')->defaultTrue()->end() 142 | ->booleanNode('exclusive')->defaultFalse()->end() 143 | ->booleanNode('auto_delete')->defaultFalse()->end() 144 | 145 | // args 146 | ->arrayNode('arguments') 147 | ->prototype('scalar')->end() 148 | ->defaultValue([]) 149 | ->normalizeKeys(false) 150 | ->end() 151 | 152 | // binding 153 | ->arrayNode('routing_keys') 154 | ->prototype('scalar')->end() 155 | ->defaultValue([]) 156 | ->end() 157 | ->end() 158 | ->end() 159 | 160 | ->arrayNode('qos_options') 161 | ->addDefaultsIfNotSet() 162 | ->children() 163 | ->integerNode('prefetch_size')->defaultValue(0)->end() 164 | ->integerNode('prefetch_count')->defaultValue(0)->end() 165 | ->end() 166 | ->end() 167 | ->end() 168 | ->end() 169 | ->end() 170 | ->end(); 171 | } 172 | 173 | /** 174 | * Exchange options for both consumer and producer. 175 | * 176 | * @return NodeDefinition 177 | */ 178 | private function exchangeOptions() 179 | { 180 | $builder = new NodeBuilder(); 181 | 182 | return $builder 183 | ->arrayNode('exchange_options') 184 | ->children() 185 | // base info 186 | ->scalarNode('name')->isRequired()->end() 187 | ->scalarNode('type') 188 | ->info('Set the type of the exchange. If exchange already exist - you can skip it. Otherwise behavior is unpredictable') 189 | ->end() 190 | 191 | // flags 192 | ->booleanNode('passive')->defaultFalse()->end() 193 | ->booleanNode('durable')->defaultTrue()->end() 194 | ->booleanNode('auto_delete')->defaultFalse()->end() 195 | 196 | // args 197 | ->arrayNode('arguments') 198 | ->prototype('scalar')->end() 199 | ->defaultValue([]) 200 | ->normalizeKeys(false) 201 | ->end() 202 | 203 | // binding 204 | ->arrayNode('routing_keys') 205 | ->prototype('scalar')->end() 206 | ->defaultValue([]) 207 | ->end() 208 | 209 | // default message attributes 210 | ->arrayNode('publish_attributes') 211 | ->prototype('scalar')->end() 212 | ->defaultValue([]) 213 | ->end() 214 | ->end(); 215 | // last end is missed here intentionally because arrayNode doesn't have an actual parent 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AmQPBundle 2 | 3 | [![Build Status](https://github.com/BedrockStreaming/AmqpBundle/actions/workflows/ci.yml/badge.svg)](https://github.com/BedrockStreaming/AmqpBundle/actions/workflows/ci.yml) [![Total Downloads](https://poser.pugx.org/m6web/amqp-bundle/downloads.svg)](https://packagist.org/packages/m6web/amqp-bundle) [![License](http://poser.pugx.org/m6web/amqp-bundle/license)](https://packagist.org/packages/m6web/amqp-bundle) [![PHP Version Require](http://poser.pugx.org/m6web/amqp-bundle/require/php)](https://packagist.org/packages/m6web/amqp-bundle) 4 | 5 | The configuration and documentation are inspired from [videlalvaro/RabbitMqBundle](https://github.com/videlalvaro/RabbitMqBundle). 6 | 7 | #### Amqp client as a Symfony Service 8 | 9 | The AmqpBundle incorporates messaging in your application using the [php-amqp extension](http://pecl.php.net/package/amqp). 10 | It can communicate with any AMQP spec 0-9-1 compatible server, such as RabbitMQ, OpenAMQP and Qpid, 11 | giving you the ability to publish to any exchange and consume from any queue. 12 | 13 | Publishing messages to AMQP Server from a Symfony controller is as easy as: 14 | 15 | ```php 16 | $msg = ["key" => "value"]; 17 | $this->myProducer->publishMessage(serialize($msg)); // where "myProducer" refers to a configured producer (see producers documentation below) 18 | ``` 19 | 20 | When you want to consume a message out of a queue: 21 | 22 | ```php 23 | $msg = $this->myConsumer->getMessage(); // where "myConsumer" refers to a configured consumer (see consumers documentation below) 24 | ``` 25 | 26 | The AmqpBundle does not provide a daemon mode to run AMQP consumers and will not. You can do it with the [M6Web/DaemonBundle](https://github.com/M6Web/DaemonBundle). 27 | 28 | ## Installation 29 | 30 | Use composer: 31 | 32 | ```shell 33 | composer require m6web/amqp-bundle 34 | ``` 35 | 36 | Then make sure the bundle is registered in your application: 37 | 38 | ```php 39 | // config/bundles.php 40 | 41 | return [ 42 | M6Web\Bundle\AmqpBundle\M6WebAmqpBundle::class => ['all' => true], 43 | ]; 44 | ``` 45 | 46 | ## Usage 47 | 48 | Add the `m6_web_amqp` section in your configuration file. 49 | 50 | By default, the Symfony event dispatcher will throw an event on each command (the event contains the AMQP command and the time used to execute it). To disable this feature, as well as other events' dispatching: 51 | 52 | ```yaml 53 | m6_web_amqp: 54 | event_dispatcher: false 55 | ``` 56 | 57 | Here a configuration example: 58 | 59 | ```yaml 60 | m6_web_amqp: 61 | sandbox: 62 | enabled: false #optional - default false 63 | connections: 64 | default: 65 | host: 'localhost' # optional - default 'localhost' 66 | port: 5672 # optional - default 5672 67 | timeout: 10 # optional - default 10 - in seconds 68 | login: 'guest' # optional - default 'guest' 69 | password: 'guest' # optional - default 'guest' 70 | vhost: '/' # optional - default '/' 71 | lazy: false # optional - default false 72 | producers: 73 | myproducer: 74 | class: "My\\Provider\\Class" # optional - to overload the default provider class 75 | connection: myconnection # require 76 | queue_options: 77 | name: 'my-queue' # optional 78 | passive: bool # optional - defaut false 79 | durable: bool # optional - defaut true 80 | auto_delete: bool # optional - defaut false 81 | exchange_options: 82 | name: 'myexchange' # require 83 | type: direct/fanout/headers/topic # require 84 | passive: bool # optional - defaut false 85 | durable: bool # optional - defaut true 86 | auto_delete: bool # optional - defaut false 87 | arguments: { key: value } # optional - default { } - Please refer to the documentation of your broker for information about the arguments. 88 | routing_keys: ['routingkey', 'routingkey2'] # optional - default { } 89 | publish_attributes: { key: value } # optional - default { } - possible keys: content_type, content_encoding, message_id, user_id, app_id, delivery_mode, 90 | # priority, timestamp, expiration, type, reply_to, headers. 91 | 92 | consumers: 93 | myconsumer: 94 | class: "My\\Provider\\Class" # optional - to overload the default consumer class 95 | connection: default 96 | exchange_options: 97 | name: 'myexchange' # require 98 | queue_options: 99 | name: 'myqueue' # require 100 | passive: bool # optional - defaut false 101 | durable: bool # optional - defaut true 102 | exclusive: bool # optional - defaut false 103 | auto_delete: bool # optional - defaut false 104 | arguments: { key: value } # optional - default { } - Please refer to the documentation of your broker for information about the arguments. 105 | # RabbitMQ ex: {'x-ha-policy': 'all', 'x-dead-letter-routing-key': 'async.dead', 106 | # 'x-dead-letter-exchange': 'async_dead', 'x-message-ttl': 20000} 107 | routing_keys: ['routingkey', 'routingkey2'] # optional - default { } 108 | qos_options: 109 | prefetch_size: integer # optional - default 0 110 | prefetch_count: integer # optional - default 0 111 | ``` 112 | 113 | Here we configure the connection service and the message endpoints that our application will have. 114 | 115 | Producer and Consumer services are retrievable using `m6_web_amqp.locator` using getConsumer and getProducer. 116 | 117 | In this example your service container will contain the services `m6_web_amqp.producer.myproducer` and `m6_web_amqp.consumer.myconsumer`. 118 | 119 | ### Producer 120 | 121 | A producer will be used to send messages to the server. 122 | 123 | Let's say that you want to publish a message and you've already configured a producer named `myproducer` (just like above), then you'll just have to inject the `m6_web_amqp.producer.myproducer` service wherever you wan't to use it: 124 | 125 | ```yaml 126 | App\TheClassWhereIWantToInjectMyProducer: 127 | arguments: ['@m6_web_amqp.producer.myproducer'] 128 | ``` 129 | 130 | ```php 131 | private $myProducer; 132 | 133 | public function __construct(\M6Web\Bundle\AmqpBundle\Amqp\Producer $myProducer) { 134 | $this->myProducer = $myProducer; 135 | } 136 | 137 | public function myFunction() { 138 | $msg = ["key" => "value"]; 139 | $this->myProducer->publishMessage(serialize($msg)); 140 | } 141 | ``` 142 | 143 | **Otherwise, you could use `m6_web_amqp.locator`** 144 | 145 | ```yaml 146 | App\TheClassWhereIWantToRetriveMyConsumer: 147 | arguments: ['@m6_web_amqp.locator'] 148 | ``` 149 | 150 | ```php 151 | private $myProducer; 152 | 153 | public function __construct(\M6Web\Bundle\AmqpBundle\Amqp\Locator $locator) { 154 | $this->locator = $locator; 155 | } 156 | 157 | public function myFunction() { 158 | $this->locator->getProducer('m6_web_amqp.produer.myproducer'); 159 | } 160 | ``` 161 | In the AMQP Model, messages are sent to an __exchange__, this means that in the configuration for a producer 162 | you will have to specify the connection options along with the `exchange_options`. 163 | 164 | If you need to add default publishing attributes for each message, `publish_attributes` options can be something like this: 165 | 166 | ```yaml 167 | publish_attributes: { 'content_type': 'application/json', 'delivery_mode': 'persistent', 'priority': 8, 'expiration': '3200'} 168 | ``` 169 | 170 | If you don't want to use the configuration to define the __routing key__ (for instance, if it should be computed for each message), you can define it during the call to `publishMessage()`: 171 | 172 | ```php 173 | $routingKey = $this->computeRoutingKey($message); 174 | $this->get('m6_web_amqp.producer.myproducer')->publishMessage($message, AMQP_NOPARAM, [], [$routingKey]); 175 | ``` 176 | 177 | ### Consumer 178 | 179 | A consumer will be used to get a message from the queue. 180 | 181 | Let's say that you want to consume a message and you've already configured a consumer named `myconsumer` (just like above), then you'll just have to inject the `m6_web_amqp.consumer.myconsumer` service wherever you wan't to use it: 182 | 183 | ```yaml 184 | App\TheClassWhereIWantToInjectMyConsumer: 185 | arguments: ['@m6_web_amqp.consumer.myconsumer'] 186 | ``` 187 | 188 | ```php 189 | private $myConsumer; 190 | 191 | public function __construct(\M6Web\Bundle\AmqpBundle\Amqp\Consumer $myConsumer) { 192 | $this->myConsumer = $myConsumer; 193 | } 194 | 195 | public function myFunction() { 196 | $this->myConsumer->getMessage(); 197 | } 198 | ``` 199 | 200 | **Otherwise, you could use `m6_web_amqp.locator`** 201 | 202 | ```yaml 203 | App\TheClassWhereIWantToRetriveMyConsumer: 204 | arguments: ['@m6_web_amqp.locator'] 205 | ``` 206 | 207 | ```php 208 | private $myConsumer; 209 | 210 | public function __construct(\M6Web\Bundle\AmqpBundle\Amqp\Locator $locator) { 211 | $this->locator = $locator; 212 | } 213 | 214 | public function myFunction() { 215 | $this->locator->getConsumer('m6_web_amqp.consumer.myconsumer'); 216 | } 217 | ``` 218 | 219 | The consumer does not wait for a message: getMessage will return null immediately if no message is available or return a AMQPEnvelope object if a message can be consumed. 220 | The "flags" argument of getMessage accepts AMQP_AUTOACK (auto acknowledge by default) or AMQP_NOPARAM (manual acknowledge). 221 | 222 | To manually acknowledge a message, use the consumer's ackMessage/nackMessage methods with a delivery_tag argument's value from the AMQPEnvelope object. 223 | If you choose to not acknowledge the message, the second parameter of nackMessage accepts AMQP_REQUEUE to requeue the message or AMQP_NOPARAM to forget it. 224 | 225 | Be careful with qos parameters, you should know that it can hurt your performances. Please [read this](http://www.rabbitmq.com/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/). 226 | Also be aware that currently there is no `global` parameter available within PHP `amqp` extension. 227 | 228 | ### Lazy connections 229 | 230 | It's highly recommended to set all connections to ```lazy: true``` in the configuration file. It'll prevent the bundle from connecting to RabbitMQ on each request. 231 | 232 | If you want lazy connections, you have to add ```"ocramius/proxy-manager": "~1.0"``` to your composer.json file, and (as said before) add ```lazy: true``` to your connections. 233 | 234 | ### DataCollector 235 | 236 | DataCollector is enabled by default if `kernel.debug` is `true`. Typically in the dev environment. 237 | 238 | ## Docker 239 | 240 | If you have a multi-containers apps, we provide a Dockerfile for a container with rabbitmq-server. 241 | This container is for testing only. 242 | 243 | Example of docker-compose.yml: 244 | 245 | ``` 246 | web: 247 | build: . 248 | volumes: 249 | - .:/var/www 250 | links: 251 | - rabbitmq:rabbitmq.local 252 | 253 | rabbitmq: 254 | build: vendor/m6web/amqp-bundle/ 255 | ports: 256 | - "15672:15672" 257 | - "5672:5672" 258 | ``` 259 | 260 | ## Testing 261 | 262 | If you use this library in your application tests you will need rabbitmq instance running. If you don't want to test rabbitmq producers and consumers you can enable sandbox mode: 263 | 264 | ```yaml 265 | m6_web_amqp: 266 | sandbox: 267 | enabled: true 268 | ``` 269 | 270 | In this mode there will be no connection established to rabbitmq server. Producers silently accept message, consumers silently assume there are no messages available. 271 | -------------------------------------------------------------------------------- /src/AmqpBundle/Resources/views/Collector/amqp.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block toolbar %} 4 | {% set icon %} 5 | Ladybug 6 | {% endset %} 7 | {% set text %} 8 | {{ collector.commands | length }} commands (avg : {{ collector.avgexecutiontime|number_format(4) }} - total : {{ collector.totalexecutiontime|number_format(4) }}) 9 | {% endset %} 10 | {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': true } %} 11 | {% endblock %} 12 | 13 | {% block head %} 14 | {{ parent() }} 15 | {% endblock %} 16 | 17 | {% block menu %} 18 | 19 | Dependency Injection Container 20 | Amqp 21 | 22 | {{ collector.commands|length }} 23 | 24 | 25 | {% endblock %} 26 | 27 | {% block panel %} 28 |

Amqp

29 | 30 |

avg : {{ collector.avgexecutiontime|number_format(4) }} - total : {{ collector.totalexecutiontime|number_format(4) }}

31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% for command in collector.commands %} 40 | 41 | {% set type = command.command %} 42 | 43 | {% set time = command.executiontime %} 44 | 45 | 48 | 49 | {% endfor %} 50 |
Amqp CommandTime (s)Arguments
{{ type }}{{ time }} 46 | {{ command.arguments|join(', ') }} 47 |
51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /src/AmqpBundle/Tests/Units/Amqp/Consumer.php: -------------------------------------------------------------------------------- 1 | if($queue = $this->getQueue()) 22 | ->if($queueOptions = []) 23 | ->and($consumer = new Base($queue, $queueOptions)) 24 | ->object($consumer->getQueue()) 25 | ->isIdenticalTo($queue); 26 | } 27 | 28 | public function testGetMessageAutoAck(): void 29 | { 30 | $msgList = ['wait' => [1 => 'message1', '2' => 'message2']]; 31 | $this 32 | ->if($msgList) 33 | ->and($queue = $this->getQueue($msgList)) 34 | ->and($consumer = new Base($queue, [])) 35 | // First message : auto ack 36 | ->object($message = $consumer->getMessage()) 37 | ->isInstanceOf('\AMQPEnvelope') 38 | ->string($message->getBody()) 39 | ->isEqualTo('message1') 40 | ->integer($message->getDeliveryTag()) 41 | ->isEqualTo(1) 42 | ->array($msgList['wait']) 43 | ->isEqualTo([2 => 'message2']) 44 | ->array($msgList['ack']) 45 | ->isEqualTo([1 => 'message1']) 46 | ->array($msgList['unack']) 47 | ->isEmpty() 48 | 49 | // Second message : auto ack 50 | ->object($message = $consumer->getMessage()) 51 | ->isInstanceOf('\AMQPEnvelope') 52 | ->string($message->getBody()) 53 | ->isEqualTo('message2') 54 | ->integer($message->getDeliveryTag()) 55 | ->isEqualTo(2) 56 | ->array($msgList['wait']) 57 | ->isEmpty() 58 | ->array($msgList['ack']) 59 | ->isEqualTo([1 => 'message1', 2 => 'message2']) 60 | ->array($msgList['unack']) 61 | ->isEmpty() 62 | 63 | // Queue empty 64 | ->variable($message = $consumer->getMessage()) 65 | ->isNull() 66 | 67 | ->mock($queue) 68 | ->call('get') 69 | ->withArguments(AMQP_AUTOACK) 70 | ->exactly(3); 71 | } 72 | 73 | public function testGetMessageManualAck(): void 74 | { 75 | $this 76 | ->if($msgList = ['wait' => [1 => 'message1']]) 77 | ->and($queue = $this->getQueue($msgList)) 78 | ->and($consumer = new Base($queue, [])) 79 | // First message : manual ack 80 | ->object($message = $consumer->getMessage(AMQP_NOPARAM)) 81 | ->isInstanceOf('\AMQPEnvelope') 82 | ->string($message->getBody()) 83 | ->isEqualTo('message1') 84 | ->integer($message->getDeliveryTag()) 85 | ->isEqualTo(1) 86 | 87 | // Check data 88 | ->array($msgList['wait']) 89 | ->isEmpty() 90 | ->array($msgList['unack']) 91 | ->isEqualTo([1 => 'message1']) 92 | ->array($msgList['ack']) 93 | ->isEmpty() 94 | 95 | // Ack (unknown delivery id) 96 | ->and($message = $consumer->ackMessage(12345)) 97 | ->variable($message) 98 | ->isNull() 99 | ->mock($queue) 100 | ->call('ack') 101 | ->withArguments(12345) 102 | ->once() 103 | 104 | // Check data 105 | ->array($msgList['wait']) 106 | ->isEmpty() 107 | ->array($msgList['unack']) 108 | ->isEqualTo([1 => 'message1']) 109 | ->array($msgList['ack']) 110 | ->isEmpty() 111 | 112 | // Ack (known delivery id) 113 | ->and($message = $consumer->ackMessage(1)) 114 | ->variable($message) 115 | ->isNull() 116 | ->mock($queue) 117 | ->call('ack') 118 | ->withArguments(1) 119 | ->once() 120 | 121 | // Check data 122 | ->array($msgList['wait']) 123 | ->isEmpty() 124 | ->array($msgList['ack']) 125 | ->isEqualTo([1 => 'message1']) 126 | ->array($msgList['unack']) 127 | ->isEmpty(); 128 | } 129 | 130 | public function testGetMessageManualNack(): void 131 | { 132 | $this 133 | ->if($msgList = ['wait' => [1 => 'message1']]) 134 | ->and($queue = $this->getQueue($msgList)) 135 | ->and($consumer = new Base($queue, [])) 136 | // First message : manual ack 137 | ->object($message = $consumer->getMessage(AMQP_NOPARAM)) 138 | ->isInstanceOf('\AMQPEnvelope') 139 | ->string($message->getBody()) 140 | ->isEqualTo('message1') 141 | ->integer($message->getDeliveryTag()) 142 | ->isEqualTo(1) 143 | 144 | // Check data 145 | ->array($msgList['wait']) 146 | ->isEmpty() 147 | ->array($msgList['unack']) 148 | ->isEqualTo([1 => 'message1']) 149 | ->array($msgList['ack']) 150 | ->isEmpty() 151 | 152 | // Nack (unknown delivery id) 153 | ->and($message = $consumer->nackMessage(12345)) 154 | ->variable($message) 155 | ->isNull() 156 | ->mock($queue) 157 | ->call('nack') 158 | ->withArguments(12345) 159 | ->once() 160 | 161 | // Check data 162 | ->array($msgList['wait']) 163 | ->isEmpty() 164 | ->array($msgList['unack']) 165 | ->isEqualTo([1 => 'message1']) 166 | ->array($msgList['ack']) 167 | ->isEmpty() 168 | 169 | // Nack and requeue (known delivery id) 170 | ->and($message = $consumer->nackMessage(1, AMQP_REQUEUE)) 171 | ->variable($message) 172 | ->isNull() 173 | ->mock($queue) 174 | ->call('nack') 175 | ->withArguments(1) 176 | ->once() 177 | 178 | // Check data 179 | ->array($msgList['wait']) 180 | ->isEqualTo([1 => 'message1']) 181 | ->array($msgList['ack']) 182 | ->isEmpty() 183 | ->array($msgList['unack']) 184 | ->isEmpty() 185 | 186 | // message : manual ack 187 | ->object($message = $consumer->getMessage(AMQP_NOPARAM)) 188 | ->isInstanceOf('\AMQPEnvelope') 189 | ->string($message->getBody()) 190 | ->isEqualTo('message1') 191 | ->integer($message->getDeliveryTag()) 192 | ->isEqualTo(1) 193 | 194 | // Check data 195 | ->array($msgList['wait']) 196 | ->isEmpty() 197 | ->array($msgList['unack']) 198 | ->isEqualTo([1 => 'message1']) 199 | ->array($msgList['ack']) 200 | ->isEmpty() 201 | 202 | // Nack and forget (known delivery id) 203 | ->and($message = $consumer->nackMessage(1)) 204 | ->variable($message) 205 | ->isNull() 206 | 207 | // Check data 208 | ->array($msgList['wait']) 209 | ->isEmpty() 210 | ->array($msgList['ack']) 211 | ->isEmpty() 212 | ->array($msgList['unack']) 213 | ->isEmpty() 214 | 215 | // Nack (old delivery id) 216 | ->variable($message = $consumer->nackMessage(1)) 217 | ->isEqualTo(null); 218 | } 219 | 220 | public function testPurge(): void 221 | { 222 | $this 223 | ->if($msgList = ['wait' => [1 => 'message1']]) 224 | ->and($queue = $this->getQueue($msgList)) 225 | ->and($consumer = new Base($queue, [])) 226 | // Purge the queue 227 | ->integer($consumer->purge()) 228 | ->isEqualTo(1) 229 | ->mock($queue) 230 | ->call('purge') 231 | ->once() 232 | 233 | // Check data 234 | ->array($msgList['wait']) 235 | ->isEmpty() 236 | ->array($msgList['ack']) 237 | ->isEmpty() 238 | ->array($msgList['unack']) 239 | ->isEmpty() 240 | 241 | // Queue empty 242 | ->variable($consumer->getMessage()) 243 | ->isNull(); 244 | } 245 | 246 | public function testGetMessageCurrentCount(): void 247 | { 248 | $this 249 | ->if($msgList = [ 250 | 'wait' => [1 => 'message1', 2 => 'message2'], 251 | 'flags' => AMQP_DURABLE | AMQP_EXCLUSIVE | AMQP_AUTODELETE, 252 | ]) 253 | ->and($queue = $this->getQueue($msgList)) 254 | ->and($consumer = new Base($queue, [])) 255 | // Declare queue in passive mode 256 | ->integer($consumer->getCurrentMessageCount()) 257 | ->isEqualTo(\count($msgList['wait'])) 258 | ->mock($queue) 259 | ->call('getFlags') 260 | ->once() 261 | ->call('setFlags') 262 | ->withArguments($msgList['flags'] | AMQP_PASSIVE) 263 | ->once() 264 | ->call('declareQueue') 265 | ->once() 266 | ->call('setFlags') 267 | ->withArguments($msgList['flags']) 268 | ->once() 269 | 270 | ->integer($queue->getFlags()) 271 | ->isEqualTo($msgList['flags']); 272 | } 273 | 274 | public function testConsumerWithNullQueue(): void 275 | { 276 | $this 277 | ->if($connection = new NullConnection()) 278 | ->exception( 279 | static fn() => new NullChannel($connection) 280 | ); 281 | 282 | $this 283 | ->if($queue = new NullQueue()) 284 | ->and($consumer = new Base($queue, [])) 285 | ->then 286 | ->variable($consumer->getMessage())->isNull() 287 | ->integer($consumer->getCurrentMessageCount())->isEqualTo(0) 288 | ; 289 | 290 | $this 291 | ->if($envelope = new NullEnvelope()) 292 | ->and($queue->enqueue($envelope)) 293 | ->then 294 | ->integer($consumer->getCurrentMessageCount())->isEqualTo(1) 295 | ->object($consumer->getMessage())->isEqualTo($envelope) 296 | ; 297 | } 298 | 299 | protected function getQueue(&$msgList = []) 300 | { 301 | if (!isset($msgList['wait'])) { 302 | $msgList['wait'] = []; 303 | } 304 | if (!isset($msgList['ack'])) { 305 | $msgList['ack'] = []; 306 | } 307 | if (!isset($msgList['unack'])) { 308 | $msgList['unack'] = []; 309 | } 310 | if (!isset($msgList['flags'])) { 311 | $msgList['flags'] = AMQP_NOPARAM; 312 | } 313 | 314 | $this->mockGenerator->orphanize('__construct'); 315 | $this->mockGenerator->shuntParentClassCalls(); 316 | 317 | $queue = new \mock\AMQPQueue(); 318 | 319 | $queue->getMockController()->get = function ($flags = AMQP_AUTOACK) use (&$msgList) { 320 | $key = array_key_first($msgList['wait']); 321 | $msg = reset($msgList['wait']); 322 | unset($msgList['wait'][$key]); 323 | 324 | if (!$key) { 325 | return null; 326 | } 327 | 328 | if ($flags & AMQP_AUTOACK) { 329 | $msgList['ack'][$key] = $msg; 330 | } else { 331 | $msgList['unack'][$key] = $msg; 332 | } 333 | 334 | // Message 335 | $message = new \mock\AMQPEnvelope(); 336 | 337 | $message->getMockController()->getBody = fn () => $msg; 338 | 339 | $message->getMockController()->getDeliveryTag = fn () => $key; 340 | 341 | return $message; 342 | }; 343 | 344 | $queue->getMockController()->ack = function ($delivery_tag) use (&$msgList) { 345 | if (isset($msgList['unack'][$delivery_tag])) { 346 | $msgList['ack'][$delivery_tag] = $msgList['unack'][$delivery_tag]; 347 | unset($msgList['unack'][$delivery_tag]); 348 | 349 | return true; 350 | } 351 | 352 | return false; 353 | }; 354 | 355 | $queue->getMockController()->nack = function ($delivery_tag, $flags = AMQP_NOPARAM) use (&$msgList) { 356 | if (isset($msgList['unack'][$delivery_tag])) { 357 | if ($flags & AMQP_REQUEUE) { 358 | $msgList['wait'][$delivery_tag] = $msgList['unack'][$delivery_tag]; 359 | } 360 | unset($msgList['unack'][$delivery_tag]); 361 | 362 | return true; 363 | } 364 | 365 | return false; 366 | }; 367 | 368 | $queue->getMockController()->purge = function () use (&$msgList) { 369 | $msgList['wait'] = []; 370 | 371 | return true; 372 | }; 373 | 374 | $queue->getMockController()->setFlags = function ($flags) use (&$msgList) { 375 | $msgList['flags'] = $flags; 376 | 377 | return $this; 378 | }; 379 | 380 | $queue->getMockController()->getFlags = function () use (&$msgList) { 381 | return $msgList['flags']; 382 | }; 383 | 384 | $queue->getMockController()->declareQueue = function () use (&$msgList) { 385 | return \count($msgList['wait']); 386 | }; 387 | 388 | return $queue; 389 | } 390 | } 391 | --------------------------------------------------------------------------------