├── .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 | [](https://github.com/BedrockStreaming/AmqpBundle/actions/workflows/ci.yml) [](https://packagist.org/packages/m6web/amqp-bundle) [](https://packagist.org/packages/m6web/amqp-bundle) [](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 |
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 |
20 | Amqp
21 |
22 | {{ collector.commands|length }}
23 |
24 |
25 | {% endblock %}
26 |
27 | {% block panel %}
28 |
avg : {{ collector.avgexecutiontime|number_format(4) }} - total : {{ collector.totalexecutiontime|number_format(4) }}
31 | 32 || Amqp Command | 35 |Time (s) | 36 |Arguments | 37 | 38 |
|---|---|---|
| {{ type }} | 43 | {% set time = command.executiontime %} 44 |{{ time }} | 45 |46 | {{ command.arguments|join(', ') }} 47 | | 48 |