├── .gitignore ├── .php_cs.dist ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Consumer.php └── Producer.php └── tests ├── ConsumerTest.php └── ProducerTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | .phpunit.result.cache 4 | /phpunit.xml 5 | .php_cs.cache 6 | /.php_cs 7 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | setRules([ 5 | '@Symfony' => true, 6 | '@Symfony:risky' => true, 7 | '@PHPUnit75Migration:risky' => true, 8 | 'array_syntax' => ['syntax' => 'short'], 9 | 'blank_line_after_opening_tag' => false, 10 | 'declare_strict_types' => true, 11 | 'fopen_flags' => false, 12 | 'ordered_imports' => true, 13 | 'protected_to_private' => true, 14 | ]) 15 | ->setRiskyAllowed(true) 16 | ->setFinder( 17 | PhpCsFixer\Finder::create() 18 | ->notPath('vendor') 19 | ->in(__DIR__.'/src') 20 | ->in(__DIR__.'/tests') 21 | ) 22 | ; 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | dist: xenial 4 | 5 | addons: 6 | apt_packages: 7 | - rabbitmq-server 8 | 9 | env: 10 | global: 11 | - COMPOSER_FLAGS="--prefer-stable" 12 | 13 | matrix: 14 | include: 15 | - php: 7.2 16 | # Build ensuring minimum dependencies are valid 17 | - php: 7.2 18 | env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" 19 | - php: 7.3 20 | - php: 7.4snapshot 21 | - php: nightly 22 | allow_failures: 23 | - php: 7.4snapshot 24 | - php: nightly 25 | 26 | cache: 27 | directories: 28 | - $HOME/.composer/cache 29 | 30 | before_install: 31 | - phpenv config-rm xdebug.ini || true 32 | - travis_retry composer self-update 33 | - sudo apt update 34 | - sudo apt install -y librabbitmq-dev 35 | - yes '' | pecl install -f amqp 36 | 37 | before_script: 38 | - composer update $COMPOSER_FLAGS 39 | 40 | script: 41 | - vendor/bin/phpunit 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2015 Johann Saunier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactAMQP 2 | 3 | [![Build Status](https://travis-ci.org/GeniusesOfSymfony/ReactAMQP.svg?branch=master)](https://travis-ci.org/GeniusesOfSymfony/ReactAMQP) 4 | 5 | Basic AMQP bindings for [React PHP](https://github.com/reactphp). 6 | 7 | ## Install 8 | This library requires PHP >=7.1 and the [PECL AMQP extension](http://pecl.php.net/package/amqp). The best way to install this library is [through composer](http://getcomposer.org). 9 | > Please, checkout 1.x version for PHP-5.x. 10 | 11 | ```cmd 12 | composer require gos/react-amqp 13 | ``` 14 | 15 | ## Usage 16 | This library provides two classes, an AMQP Consumer and Producer. Both classes work with a periodic timer and you supply the timer interval as an argument to the constructor. 17 | 18 | ### Consumer 19 | The consumer class allows you to receive messages from an AMQP broker and to dispatch a callback whenever one is received. You can also supply a number of messages to consume in one go, making sure that your event loop isn't perpetually stuck consuming messages from a broker. The callback you supply must accept an AMQPEnvelope as the first argument and an optional AMQPQueue as the second. 20 | 21 | ```php 22 | connect(); 26 | 27 | // Create a channel 28 | $ch = new AMQPChannel($cnn); 29 | 30 | // Create a new queue 31 | $queue = new AMQPQueue($ch); 32 | $queue->setName('queue1'); 33 | $queue->declare(); 34 | 35 | // Create an event loop 36 | $loop = React\EventLoop\Factory::create(); 37 | 38 | // Create a consumer that will check for messages every half a second and consume up to 10 at a time. 39 | $consumer = new Gos\Component\ReactAMQP\Consumer($queue, $loop, 0.5, 10); 40 | $consumer->on('consume', function(AMQPEnvelope $envelope, AMQPQueue $queue){ 41 | //Process the message here 42 | }); 43 | $loop->run(); 44 | ``` 45 | 46 | ### Producer 47 | The producer class allows you to send messages to an AMQP exchange. The producer has a publish method that has exactly the same method signature as the AMQPExchange's publish method. Messages are stored in the producer class and sent based on the timer interval passed to the constructor. When the producer object is invoked to send any queued messages the AMQPExchange objects publish method is used. This method is blocking, which may be a performance concern for your application. When a message is successfully sent a 'produce' event is emitted that you can bind a callback to. This is passed an array containing all of the message parameters sent. If an AMQPExchangeException is thrown, meaning the message could not be sent, an 'error' event is emitted that you can bind a callback to. This will be passed the AMQPExchangeException object for you to handle. 48 | 49 | ```php 50 | connect(); 54 | 55 | // Create a channel 56 | $ch = new AMQPChannel($cnn); 57 | 58 | // Declare a new exchange 59 | $ex = new AMQPExchange($ch); 60 | $ex->setName('exchange1'); 61 | $ex->declare(); 62 | 63 | // Create an event loop 64 | $loop = React\EventLoop\Factory::create(); 65 | 66 | // Create a producer that will send any waiting messages every half a second. 67 | $producer = new Gos\Component\ReactAMQP\Producer($ex, $loop, 0.5); 68 | 69 | // Add a callback that's called every time a message is successfully sent. 70 | $producer->on('produce', function(array $message) { 71 | // $message is an array containing keys 'message', 'routingKey', 'flags' and 'attributes' 72 | }); 73 | 74 | $producer->on('error', function(AMQPExchangeException $e) { 75 | // Handle any exceptions here. 76 | }); 77 | 78 | $i = 0; 79 | 80 | $loop->addPeriodicTimer(1, function() use(&$i, $producer) { 81 | $i++; 82 | echo "Sending $i\n"; 83 | $producer->publish($i, 'routing.key'); 84 | }); 85 | 86 | $loop->run(); 87 | ``` 88 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gos/react-amqp", 3 | "description": "AMQP bindings for ReactPHP", 4 | "keywords": ["amqp", "rabbitmq"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Jeremy Cook", 9 | "email": "jeremycook0@gmail.com" 10 | }, 11 | { 12 | "name": "Johann Saunier", 13 | "email": "johann_27@hotmail.fr" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=7.2", 18 | "ext-amqp": "*", 19 | "evenement/evenement": "^3.0", 20 | "react/event-loop": "^1.0" 21 | }, 22 | "require-dev": { 23 | "friendsofphp/php-cs-fixer": "^2.15.1", 24 | "phpunit/phpunit": "^8.1" 25 | }, 26 | "autoload": { 27 | "psr-4": { "Gos\\Component\\ReactAMQP\\": "src/" } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { "Gos\\Component\\ReactAMQP\\Tests\\": "tests/" } 31 | }, 32 | "extra": { 33 | "branch-alias": { 34 | "dev-master": "0.3.x-dev" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/ 7 | 8 | 9 | 10 | 11 | 12 | ./src/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Consumer.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class Consumer extends EventEmitter 15 | { 16 | /** 17 | * @var \AMQPQueue 18 | */ 19 | private $queue; 20 | 21 | /** 22 | * @var LoopInterface 23 | */ 24 | private $loop; 25 | 26 | /** 27 | * @var bool 28 | */ 29 | private $closed = false; 30 | 31 | /** 32 | * Max number of messages to consume in a 'batch'. 33 | * 34 | * Should stop the event loop stopping on this class for protracted lengths of time. 35 | * 36 | * @var int 37 | */ 38 | private $max; 39 | 40 | /** 41 | * @var TimerInterface 42 | */ 43 | private $timer; 44 | 45 | public function __construct(\AMQPQueue $queue, LoopInterface $loop, float $interval, ?int $max = null) 46 | { 47 | $this->queue = $queue; 48 | $this->loop = $loop; 49 | $this->max = $max; 50 | $this->timer = $this->loop->addPeriodicTimer($interval, $this); 51 | 52 | $this->on('close_amqp_consumer', [$this, 'close']); 53 | } 54 | 55 | /** 56 | * Handles receiving an incoming message. 57 | * 58 | * @throws \AMQPChannelException 59 | * @throws \AMQPConnectionException 60 | * @throws \BadMethodCallException if the consumer connection has been closed 61 | */ 62 | public function __invoke(): void 63 | { 64 | if ($this->closed) { 65 | throw new \BadMethodCallException('This consumer object is closed and cannot receive any more messages.'); 66 | } 67 | 68 | $counter = 0; 69 | 70 | while ($envelope = $this->queue->get()) { 71 | $this->emit('consume', [$envelope, $this->queue]); 72 | 73 | if ($this->max && ++$counter >= $this->max) { 74 | return; 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * Allows calls to unknown methods to be passed through to the queue store. 81 | * 82 | * @return mixed 83 | */ 84 | public function __call(string $method, $args) 85 | { 86 | return \call_user_func_array([$this->queue, $method], $args); 87 | } 88 | 89 | public function close(): void 90 | { 91 | if ($this->closed) { 92 | return; 93 | } 94 | 95 | $this->emit('end', [$this]); 96 | $this->loop->cancelTimer($this->timer); 97 | $this->removeAllListeners(); 98 | $this->queue = null; 99 | $this->closed = true; 100 | } 101 | 102 | public function isClosed(): bool 103 | { 104 | return true === $this->closed; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Producer.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class Producer extends EventEmitter implements \Countable, \IteratorAggregate 15 | { 16 | /** 17 | * @var \AMQPExchange 18 | */ 19 | private $exchange; 20 | 21 | /** 22 | * @var LoopInterface 23 | */ 24 | private $loop; 25 | 26 | /** 27 | * @var bool 28 | */ 29 | private $closed = false; 30 | 31 | /** 32 | * @var array 33 | */ 34 | private $messages = []; 35 | 36 | /** 37 | * @var TimerInterface 38 | */ 39 | private $timer; 40 | 41 | public function __construct(\AMQPExchange $exchange, LoopInterface $loop, float $interval) 42 | { 43 | $this->exchange = $exchange; 44 | $this->loop = $loop; 45 | $this->timer = $this->loop->addPeriodicTimer($interval, $this); 46 | } 47 | 48 | public function count(): int 49 | { 50 | return \count($this->messages); 51 | } 52 | 53 | public function getIterator(): array 54 | { 55 | return $this->messages; 56 | } 57 | 58 | /** 59 | * Publishes a message to an AMQP exchange. 60 | * 61 | * Has the same method signature as the exchange object's publish method. 62 | * 63 | * @throws \BadMethodCallException if the producer connection has been closed 64 | */ 65 | public function publish(string $message, string $routingKey, int $flags = 0 /* AMQP_NOPARAM */, array $attributes = []): void 66 | { 67 | if ($this->closed) { 68 | throw new \BadMethodCallException('This Producer object is closed and cannot send any more messages.'); 69 | } 70 | 71 | $this->messages[] = [ 72 | 'message' => $message, 73 | 'routingKey' => $routingKey, 74 | 'flags' => $flags, 75 | 'attributes' => $attributes, 76 | ]; 77 | } 78 | 79 | /** 80 | * Handles publishing outgoing messages. 81 | * 82 | * @throws \AMQPChannelException 83 | * @throws \AMQPConnectionException 84 | * @throws \BadMethodCallException if the consumer connection has been closed 85 | */ 86 | public function __invoke(): void 87 | { 88 | if ($this->closed) { 89 | throw new \BadMethodCallException('This Producer object is closed and cannot send any more messages.'); 90 | } 91 | 92 | foreach ($this->messages as $key => $message) { 93 | try { 94 | $this->exchange->publish($message['message'], $message['routingKey'], $message['flags'], $message['attributes']); 95 | unset($this->messages[$key]); 96 | $this->emit('produce', array_values($message)); 97 | } catch (\AMQPExchangeException $e) { 98 | $this->emit('error', [$e]); 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * Allows calls to unknown methods to be passed through to the exchange store. 105 | * 106 | * @return mixed 107 | */ 108 | public function __call($method, $args) 109 | { 110 | return \call_user_func_array([$this->exchange, $method], $args); 111 | } 112 | 113 | public function close(): void 114 | { 115 | if ($this->closed) { 116 | return; 117 | } 118 | 119 | $this->emit('end', [$this]); 120 | $this->loop->cancelTimer($this->timer); 121 | $this->removeAllListeners(); 122 | $this->exchange = null; 123 | $this->closed = true; 124 | } 125 | 126 | public function isClosed(): bool 127 | { 128 | return true === $this->closed; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/ConsumerTest.php: -------------------------------------------------------------------------------- 1 | 13 | * 14 | * @requires extension amqp 15 | */ 16 | final class ConsumerTest extends TestCase 17 | { 18 | /** 19 | * @var \AMQPQueue|MockObject 20 | */ 21 | private $queue; 22 | 23 | /** 24 | * @var LoopInterface|MockObject 25 | */ 26 | private $loop; 27 | 28 | /** 29 | * Counter to test the number of invokations of an observed object. 30 | * 31 | * @var int 32 | */ 33 | private $counter = 0; 34 | 35 | /** 36 | * @var TimerInterface 37 | */ 38 | private $timer; 39 | 40 | protected function setUp(): void 41 | { 42 | $this->queue = $this->createMock(\AMQPQueue::class); 43 | $this->loop = $this->createMock(LoopInterface::class); 44 | $this->timer = $this->createMock(TimerInterface::class); 45 | 46 | $this->loop->expects($this->any()) 47 | ->method('addPeriodicTimer') 48 | ->willReturn($this->timer); 49 | } 50 | 51 | protected function tearDown(): void 52 | { 53 | $this->counter = 0; 54 | } 55 | 56 | /** 57 | * Allows the test class to be used as a callback by the consumer. 58 | * 59 | * Simply counts the number of times the invoke method is called. 60 | */ 61 | public function __invoke(): void 62 | { 63 | ++$this->counter; 64 | } 65 | 66 | /** 67 | * @dataProvider intervalMaxSupplier 68 | */ 69 | public function testConsumerIsInstantiatedAndRegisteredToTheLoop(float $interval, ?int $max): void 70 | { 71 | $this->loop->expects($this->once()) 72 | ->method('addPeriodicTimer') 73 | ->with($this->identicalTo($interval), $this->isInstanceOf(Consumer::class)); 74 | 75 | new Consumer($this->queue, $this->loop, $interval, $max); 76 | } 77 | 78 | /** 79 | * Basic test case that asserts that messages can be consumed from the queue. 80 | */ 81 | public function testConsumingMessages(): void 82 | { 83 | $this->queue->expects($this->exactly(4)) 84 | ->method('get') 85 | ->willReturnOnConsecutiveCalls('foo', 'bar', 'baz', false); 86 | 87 | $consumer = new Consumer($this->queue, $this->loop, 1.0); 88 | $consumer->on('consume', $this); 89 | $consumer(); 90 | 91 | $this->assertSame(3, $this->counter); 92 | } 93 | 94 | /** 95 | * Asserts that supplying a value for the max number of messages to consume results in the Consumer returning. 96 | * 97 | * @dataProvider maxSupplier 98 | */ 99 | public function testConsumingMessagesWithMaxCount(int $max): void 100 | { 101 | $this->queue->expects($this->exactly($max)) 102 | ->method('get') 103 | ->willReturn('foobar'); 104 | 105 | $consumer = new Consumer($this->queue, $this->loop, 1.0, $max); 106 | $consumer->on('consume', $this); 107 | $consumer(); 108 | 109 | $this->assertSame($max, $this->counter); 110 | } 111 | 112 | /** 113 | * @dataProvider callSupplier 114 | */ 115 | public function testTheConsumerProxiesMethodCalls($method, ...$args): void 116 | { 117 | $this->queue->expects($this->once()) 118 | ->method($method) 119 | ->with($this->identicalTo(...$args)); 120 | 121 | $consumer = new Consumer($this->queue, $this->loop, 1.0); 122 | $consumer->$method(...$args); 123 | } 124 | 125 | public function testClose(): void 126 | { 127 | $consumer = new Consumer($this->queue, $this->loop, 1.0); 128 | $consumer->on('end', $this); 129 | 130 | $this->loop->expects($this->once()) 131 | ->method('cancelTimer') 132 | ->with($this->equalTo($this->timer)); 133 | 134 | $consumer->close(); 135 | 136 | $this->assertTrue($consumer->isClosed()); 137 | $this->assertSame(1, $this->counter); 138 | } 139 | 140 | public function testInvokingConsumerAfterClosingIsNotAllowed(): void 141 | { 142 | $this->expectException(\BadMethodCallException::class); 143 | 144 | $consumer = new Consumer($this->queue, $this->loop, 1.0); 145 | $consumer->close(); 146 | $consumer(); 147 | } 148 | 149 | public function intervalMaxSupplier(): \Generator 150 | { 151 | yield [1, null]; 152 | yield [1, 1]; 153 | yield [0.05, 10]; 154 | } 155 | 156 | public function maxSupplier(): \Generator 157 | { 158 | yield [1]; 159 | yield [10]; 160 | yield [45]; 161 | } 162 | 163 | public function callSupplier(): \Generator 164 | { 165 | yield ['getArgument', 'foo']; 166 | yield ['nack', 'bar']; 167 | yield ['cancel', 'baz']; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /tests/ProducerTest.php: -------------------------------------------------------------------------------- 1 | 13 | * 14 | * @requires extension amqp 15 | */ 16 | final class ProducerTest extends TestCase 17 | { 18 | /** 19 | * @var \AMQPExchange|MockObject 20 | */ 21 | private $exchange; 22 | 23 | /** 24 | * @var LoopInterface 25 | */ 26 | private $loop; 27 | 28 | /** 29 | * Counter to keep track of the number of times this object is called as a callback. 30 | * 31 | * @var int 32 | */ 33 | private $counter = 0; 34 | 35 | /** 36 | * @var TimerInterface 37 | */ 38 | private $timer; 39 | 40 | protected function setUp(): void 41 | { 42 | $this->exchange = $this->createMock(\AMQPExchange::class); 43 | $this->loop = $this->createMock(LoopInterface::class); 44 | $this->timer = $this->createMock(TimerInterface::class); 45 | 46 | $this->loop->expects($this->any()) 47 | ->method('addPeriodicTimer') 48 | ->willReturn($this->timer); 49 | } 50 | 51 | protected function tearDown(): void 52 | { 53 | $this->counter = 0; 54 | } 55 | 56 | /** 57 | * Allows the test class to be used as a callback by the producer. 58 | * 59 | * Simply counts the number of times the invoke method is called. 60 | */ 61 | public function __invoke(): void 62 | { 63 | ++$this->counter; 64 | } 65 | 66 | /** 67 | * @dataProvider intervalSupplier 68 | */ 69 | public function testProducerIsInstantiatedAndRegisteredToTheLoop(float $interval): void 70 | { 71 | $this->loop->expects($this->once()) 72 | ->method('addPeriodicTimer') 73 | ->with($this->identicalTo($interval), $this->isInstanceOf(Producer::class)); 74 | 75 | new Producer($this->exchange, $this->loop, $interval); 76 | } 77 | 78 | /** 79 | * Tests the publish method of the producer. 80 | * 81 | * @dataProvider messageProvider 82 | */ 83 | public function testPublish(string $message, string $routingKey, int $flags, array $attributes): void 84 | { 85 | $producer = new Producer($this->exchange, $this->loop, 1); 86 | 87 | $this->assertCount(0, $producer); 88 | 89 | $producer->publish($message, $routingKey, $flags, $attributes); 90 | 91 | $this->assertCount(1, $producer); 92 | } 93 | 94 | /** 95 | * Asserts that messages stored in the object can be sent. 96 | * 97 | * @depends testPublish 98 | * @dataProvider messagesProvider 99 | */ 100 | public function testSendingMessages(array $messages): void 101 | { 102 | $producer = new Producer($this->exchange, $this->loop, 1); 103 | $producer->on('produce', $this); 104 | 105 | foreach ($messages as $message) { 106 | \call_user_func_array([$producer, 'publish'], $message); 107 | } 108 | 109 | $this->exchange->expects($this->exactly(\count($messages))) 110 | ->method('publish'); 111 | 112 | $this->assertCount(\count($messages), $producer); 113 | 114 | $producer(); 115 | 116 | $this->assertSame(\count($messages), $this->counter); 117 | $this->assertCount(0, $producer); 118 | } 119 | 120 | /** 121 | * Tests the behaviour of the producer when an exception is raised by the exchange. 122 | * 123 | * @depends testPublish 124 | * @dataProvider messagesProvider 125 | */ 126 | public function testSendingMessagesWithError(array $messages): void 127 | { 128 | $producer = new Producer($this->exchange, $this->loop, 1); 129 | $producer->on('error', $this); 130 | 131 | foreach ($messages as $message) { 132 | \call_user_func_array([$producer, 'publish'], $message); 133 | } 134 | 135 | $this->exchange->expects($this->exactly(\count($messages))) 136 | ->method('publish') 137 | ->will($this->throwException(new \AMQPExchangeException())); 138 | 139 | $producer(); 140 | 141 | $this->assertSame(\count($messages), $this->counter); 142 | $this->assertCount(\count($messages), $producer); 143 | } 144 | 145 | /** 146 | * @dataProvider callProvider 147 | */ 148 | public function testTheProducerProxiesMethodCalls($method, $arg): void 149 | { 150 | $this->exchange->expects($this->once()) 151 | ->method($method) 152 | ->with($arg); 153 | 154 | $producer = new Producer($this->exchange, $this->loop, 1); 155 | $producer->$method($arg); 156 | } 157 | 158 | public function testClose(): void 159 | { 160 | $producer = new Producer($this->exchange, $this->loop, 1); 161 | $producer->on('end', $this); 162 | 163 | $this->loop->expects($this->once()) 164 | ->method('cancelTimer') 165 | ->with($this->timer); 166 | 167 | $producer->close(); 168 | 169 | $this->assertTrue($producer->isClosed()); 170 | $this->assertSame(1, $this->counter); 171 | } 172 | 173 | /** 174 | * @depends testPublish 175 | * @dataProvider messagesProvider 176 | */ 177 | public function testCount(array $messages): void 178 | { 179 | $producer = new Producer($this->exchange, $this->loop, 1); 180 | $this->assertCount(0, $producer); 181 | 182 | foreach ($messages as $message) { 183 | \call_user_func_array([$producer, 'publish'], $message); 184 | } 185 | 186 | $this->assertSame(\count($messages), \count($producer)); 187 | } 188 | 189 | /** 190 | * @depends testPublish 191 | * @dataProvider messagesProvider 192 | */ 193 | public function testGetIterator(array $messages): void 194 | { 195 | $producer = new Producer($this->exchange, $this->loop, 1); 196 | $ret = $producer->getIterator(); 197 | 198 | $this->assertTrue(\is_array($ret)); 199 | $this->assertEmpty($ret); 200 | 201 | foreach ($messages as $message) { 202 | \call_user_func_array([$producer, 'publish'], $message); 203 | } 204 | 205 | $ret = $producer->getIterator(); 206 | $this->assertTrue(\is_array($ret)); 207 | $this->assertNotEmpty($ret); 208 | } 209 | 210 | /** 211 | * @depends testClose 212 | */ 213 | public function testPublishingAfterClosingIsNotAllowed(): void 214 | { 215 | $this->expectException(\BadMethodCallException::class); 216 | 217 | $producer = new Producer($this->exchange, $this->loop, 1); 218 | $producer->close(); 219 | $producer->publish('foo', 'bar'); 220 | } 221 | 222 | /** 223 | * @depends testClose 224 | */ 225 | public function testInvokingProducerAfterClosingIsNotAllowed(): void 226 | { 227 | $this->expectException(\BadMethodCallException::class); 228 | 229 | $producer = new Producer($this->exchange, $this->loop, 1); 230 | $producer->close(); 231 | $producer(); 232 | } 233 | 234 | public function intervalSupplier(): \Generator 235 | { 236 | yield [1]; 237 | yield [2.4]; 238 | yield [0.05]; 239 | } 240 | 241 | public function messageProvider(): \Generator 242 | { 243 | yield ['foo', 'bar', 1 & 1, []]; 244 | yield ['bar', 'baz', 1 & 0, ['foo' => 'bar']]; 245 | } 246 | 247 | public function messagesProvider(): \Generator 248 | { 249 | yield [ 250 | [ 251 | ['foo', 'bar', 1 & 1, []], 252 | ['bar', 'baz', 1 & 0, ['foo' => 'bar']], 253 | ] 254 | ]; 255 | } 256 | 257 | public function callProvider(): \Generator 258 | { 259 | yield ['setName', 'foo']; 260 | yield ['setType', 'bar']; 261 | } 262 | } 263 | --------------------------------------------------------------------------------