├── LICENSE ├── composer.json └── src ├── AbstractDataBag.php ├── Client.php ├── Connection ├── Client.php ├── Connection.php ├── ConnectionFactory.php ├── ConnectionsDataBag.php ├── Exception │ ├── ConnectionException.php │ └── ConnectionFactoryException.php └── IConnection.php ├── Console └── Command │ ├── AbstractConsumerCommand.php │ ├── ConsumerCommand.php │ ├── DeclareQueuesAndExchangesCommand.php │ └── StaticConsumerCommand.php ├── Consumer ├── BulkConsumer.php ├── BulkMessage.php ├── Consumer.php ├── ConsumerFactory.php ├── ConsumersDataBag.php ├── Exception │ ├── ConsumerFactoryException.php │ └── UnexpectedConsumerResultTypeException.php └── IConsumer.php ├── DI ├── Helpers │ ├── AbstractHelper.php │ ├── ConnectionsHelper.php │ ├── ConsumersHelper.php │ ├── ExchangesHelper.php │ ├── ProducersHelper.php │ └── QueuesHelper.php ├── RabbitMQExtension.php └── RabbitMQExtension24.php ├── Diagnostics ├── BarPanel.php ├── BarPanel.phtml └── rabbitmq-icon.svg ├── Exchange ├── Exception │ └── ExchangeFactoryException.php ├── Exchange.php ├── ExchangeDeclarator.php ├── ExchangeFactory.php ├── ExchangesDataBag.php ├── IExchange.php └── QueueBinding.php ├── Producer ├── Exception │ └── ProducerFactoryException.php ├── Producer.php ├── ProducerFactory.php └── ProducersDataBag.php └── Queue ├── Exception └── QueueFactoryException.php ├── IQueue.php ├── Queue.php ├── QueueDeclarator.php ├── QueueFactory.php └── QueuesDataBag.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contributte/rabbitmq", 3 | "description": "Nette extension for RabbitMQ (using BunnyPHP)", 4 | "type": "library", 5 | "license": "MIT", 6 | "homepage": "https://github.com/contributte/rabbitmq", 7 | "keywords": [ 8 | "rabbitmq", 9 | "rabbit", 10 | "bunnyphp", 11 | "bunny", 12 | "nette", 13 | "extension", 14 | "php" 15 | ], 16 | "support": { 17 | "issues": "https://github.com/contributte/rabbitmq/issues" 18 | }, 19 | "authors": [ 20 | { 21 | "name": "Pavel Janda" 22 | } 23 | ], 24 | "require": { 25 | "php": ">=8.1", 26 | "bunny/bunny": "^0.5", 27 | "symfony/console": "^6.4.2 || ^7.0.2", 28 | "nette/di": "^3.0.7", 29 | "nette/utils": "^4.0.0" 30 | }, 31 | "require-dev": { 32 | "mockery/mockery": "^1.6.6", 33 | "contributte/qa": "^0.4", 34 | "contributte/tester": "^0.4", 35 | "contributte/phpstan": "^0.1", 36 | "nette/neon": "^3.2.1", 37 | "tracy/tracy": "^2.10.5" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "Contributte\\RabbitMQ\\": "src/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "Tests\\": "tests" 47 | } 48 | }, 49 | "suggest": { 50 | "tracy/tracy": "Allows using tracy bar panel" 51 | }, 52 | "minimum-stability": "dev", 53 | "prefer-stable": true, 54 | "extra": { 55 | "branch-alias": { 56 | "dev-master": "11.0.x-dev" 57 | } 58 | }, 59 | "config": { 60 | "allow-plugins": { 61 | "dealerdirect/phpcodesniffer-composer-installer": true 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/AbstractDataBag.php: -------------------------------------------------------------------------------- 1 | */ 9 | protected array $data = []; 10 | 11 | /** 12 | * @param array $data 13 | */ 14 | public function __construct(array $data) 15 | { 16 | foreach ($data as $queueOrExchangeName => $config) { 17 | $this->data[$queueOrExchangeName] = $config; 18 | } 19 | } 20 | 21 | /** 22 | * @return array 23 | */ 24 | public function getDataBykey(string $key): array 25 | { 26 | if (!isset($this->data[$key])) { 27 | throw new \InvalidArgumentException(sprintf('Data at key [%s] not found', $key)); 28 | } 29 | 30 | return (array) $this->data[$key]; 31 | } 32 | 33 | /** 34 | * @return array 35 | */ 36 | public function getDataKeys(): array 37 | { 38 | return array_keys($this->data); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | producerFactory = $producerFactory; 22 | } 23 | 24 | /** 25 | * @throws ProducerFactoryException 26 | */ 27 | public function getProducer(string $name): Producer 28 | { 29 | return $this->producerFactory->getProducer($name); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Connection/Client.php: -------------------------------------------------------------------------------- 1 | getWriter()->appendFrame(new HeartbeatFrame(), $this->writeBuffer); 20 | $this->flushWriteBuffer(); 21 | } 22 | 23 | public function syncDisconnect(): bool 24 | { 25 | try { 26 | if ($this->state !== ClientStateEnum::CONNECTED) { 27 | return false; 28 | } 29 | 30 | $this->state = ClientStateEnum::DISCONNECTING; 31 | 32 | foreach ($this->channels as $channel) { 33 | $channelId = $channel->getChannelId(); 34 | 35 | $this->channelClose($channelId, 0, '', 0, 0); 36 | $this->removeChannel($channelId); 37 | } 38 | 39 | $this->connectionClose(0, '', 0, 0); 40 | $this->closeStream(); 41 | } catch (ClientException $e) { 42 | // swallow, we do not care we are not connected, we want to close connection anyway 43 | } 44 | 45 | $this->init(); 46 | 47 | return true; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Connection/Connection.php: -------------------------------------------------------------------------------- 1 | */ 17 | private array $connectionParams; 18 | 19 | private int $lastBeat = 0; 20 | 21 | private ?Channel $channel = null; 22 | 23 | /** 24 | * @param array|null $ssl 25 | */ 26 | public function __construct( 27 | string $host, 28 | int $port, 29 | string $user, 30 | string $password, 31 | string $vhost, 32 | float $heartbeat, 33 | float $timeout, 34 | bool $persistent, 35 | string $path, 36 | bool $tcpNoDelay, 37 | bool $lazy = false, 38 | ?array $ssl = null 39 | ) 40 | { 41 | $this->connectionParams = [ 42 | 'host' => $host, 43 | 'port' => $port, 44 | 'user' => $user, 45 | 'password' => $password, 46 | 'vhost' => $vhost, 47 | 'heartbeat' => $heartbeat, 48 | 'timeout' => $timeout, 49 | 'persistent' => $persistent, 50 | 'path' => $path, 51 | 'tcp_nodelay' => $tcpNoDelay, 52 | 'ssl' => $ssl, 53 | ]; 54 | 55 | $this->bunnyClient = $this->createNewConnection(); 56 | 57 | if (!$lazy) { 58 | $this->bunnyClient->connect(); 59 | } 60 | } 61 | 62 | public function __destruct() 63 | { 64 | if ($this->bunnyClient->isConnected()) { 65 | $this->bunnyClient->syncDisconnect(); 66 | } 67 | } 68 | 69 | /** 70 | * @throws ConnectionException 71 | */ 72 | public function getChannel(): Channel 73 | { 74 | if ($this->channel instanceof Channel) { 75 | return $this->channel; 76 | } 77 | 78 | try { 79 | $this->connectIfNeeded(); 80 | $channel = $this->bunnyClient->channel(); 81 | 82 | if (!$channel instanceof Channel) { 83 | throw new \UnexpectedValueException(); 84 | } 85 | 86 | $this->channel = $channel; 87 | } catch (ClientException $e) { 88 | if (!in_array($e->getMessage(), ['Broken pipe or closed connection.', 'Could not write data to socket.'], true)) { 89 | throw new ConnectionException($e->getMessage(), $e->getCode(), $e); 90 | } 91 | 92 | /** 93 | * Try to reconnect 94 | */ 95 | $this->bunnyClient = $this->createNewConnection(); 96 | 97 | $channel = $this->bunnyClient->channel(); 98 | 99 | if (!$channel instanceof Channel) { 100 | throw new \UnexpectedValueException(); 101 | } 102 | 103 | $this->channel = $channel; 104 | } 105 | 106 | return $this->channel; 107 | } 108 | 109 | public function connectIfNeeded(): void 110 | { 111 | if ($this->bunnyClient->isConnected()) { 112 | return; 113 | } 114 | 115 | $this->bunnyClient->connect(); 116 | } 117 | 118 | public function sendHeartbeat(): void 119 | { 120 | $now = time(); 121 | if ($this->lastBeat < ($now - self::HEARTBEAT_INTERVAL) && $this->bunnyClient->isConnected()) { 122 | $this->bunnyClient->sendHeartbeat(); 123 | $this->lastBeat = $now; 124 | } 125 | } 126 | 127 | private function createNewConnection(): Client 128 | { 129 | return new Client($this->connectionParams); 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/Connection/ConnectionFactory.php: -------------------------------------------------------------------------------- 1 | connectionsDataBag = $connectionsDataBag; 18 | } 19 | 20 | /** 21 | * @throws ConnectionFactoryException 22 | */ 23 | public function getConnection(string $name): IConnection 24 | { 25 | if (!isset($this->connections[$name])) { 26 | $this->connections[$name] = $this->create($name); 27 | } 28 | 29 | return $this->connections[$name]; 30 | } 31 | 32 | /** 33 | * @throws ConnectionFactoryException 34 | */ 35 | private function create(string $name): IConnection 36 | { 37 | try { 38 | $connectionData = $this->connectionsDataBag->getDataBykey($name); 39 | 40 | } catch (\InvalidArgumentException $e) { 41 | throw new ConnectionFactoryException(sprintf('Connection [%s] does not exist', $name)); 42 | } 43 | 44 | return new Connection( 45 | $connectionData['host'], 46 | (int) $connectionData['port'], 47 | $connectionData['user'], 48 | $connectionData['password'], 49 | $connectionData['vhost'], 50 | (float) $connectionData['heartbeat'], 51 | (float) $connectionData['timeout'], 52 | (bool) $connectionData['persistent'], 53 | $connectionData['path'], 54 | (bool) $connectionData['tcpNoDelay'], 55 | (bool) $connectionData['lazy'], 56 | $connectionData['ssl'], 57 | ); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Connection/ConnectionsDataBag.php: -------------------------------------------------------------------------------- 1 | consumersDataBag = $consumersDataBag; 22 | $this->consumerFactory = $consumerFactory; 23 | } 24 | 25 | /** 26 | * @throws \InvalidArgumentException 27 | */ 28 | protected function validateConsumerName(string $consumerName): void 29 | { 30 | try { 31 | $this->consumerFactory->getConsumer($consumerName); 32 | 33 | } catch (ConsumerFactoryException $e) { 34 | throw new \InvalidArgumentException( 35 | sprintf( 36 | "%s\n\n Available consumers: %s", 37 | $e->getMessage(), 38 | implode( 39 | '', 40 | array_map(static fn ($s): string => sprintf("\n\t- [%s]", $s), $this->consumersDataBag->getDataKeys()) 41 | ) 42 | ) 43 | ); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Console/Command/ConsumerCommand.php: -------------------------------------------------------------------------------- 1 | setName('rabbitmq:consumer'); 15 | $this->setDescription('Run a RabbitMQ consumer'); 16 | 17 | $this->addArgument('consumerName', InputArgument::REQUIRED, 'Name of the consumer'); 18 | $this->addArgument('secondsToLive', InputArgument::OPTIONAL, 'Max seconds for consumer to run, skip parameter to run indefinitely'); 19 | } 20 | 21 | /** 22 | * @throws \InvalidArgumentException 23 | */ 24 | protected function execute(InputInterface $input, OutputInterface $output): int 25 | { 26 | $consumerName = $input->getArgument('consumerName'); 27 | $secondsToLive = $input->getArgument('secondsToLive'); 28 | 29 | if (!is_string($consumerName)) { 30 | throw new \UnexpectedValueException(); 31 | } 32 | 33 | $this->validateConsumerName($consumerName); 34 | 35 | if ($secondsToLive !== null) { 36 | if (!is_numeric($secondsToLive)) { 37 | throw new \UnexpectedValueException(); 38 | } 39 | 40 | $secondsToLive = (int) $secondsToLive; 41 | $this->validateSecondsToRun($secondsToLive); 42 | } 43 | 44 | $consumer = $this->consumerFactory->getConsumer($consumerName); 45 | $consumer->consume($secondsToLive); 46 | 47 | return 0; 48 | } 49 | 50 | /** 51 | * @throws \InvalidArgumentException 52 | */ 53 | private function validateSecondsToRun(int $secondsToLive): void 54 | { 55 | if ($secondsToLive <= 0) { 56 | throw new \InvalidArgumentException( 57 | 'Parameter [secondsToLive] has to be greater then 0' 58 | ); 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/Console/Command/DeclareQueuesAndExchangesCommand.php: -------------------------------------------------------------------------------- 1 | queuesDataBag = $queuesDataBag; 34 | $this->exchangesDataBag = $exchangesDataBag; 35 | $this->queueDeclarator = $queueDeclarator; 36 | $this->exchangeDeclarator = $exchangeDeclarator; 37 | } 38 | 39 | protected function configure(): void 40 | { 41 | $this->setDescription( 42 | 'Creates all queues and exchanges defined in configs. Intended to run during deploy process' 43 | ); 44 | } 45 | 46 | protected function execute(InputInterface $input, OutputInterface $output): int 47 | { 48 | $output->writeln('Declaring queues:'); 49 | 50 | foreach ($this->queuesDataBag->getDataKeys() as $queueName) { 51 | $output->writeln($queueName); 52 | $this->queueDeclarator->declareQueue($queueName); 53 | } 54 | 55 | $output->writeln(''); 56 | $output->writeln('Declaring exchanges:'); 57 | 58 | foreach ($this->exchangesDataBag->getDataKeys() as $exchangeName) { 59 | $output->writeln($exchangeName); 60 | $this->exchangeDeclarator->declareExchange($exchangeName); 61 | } 62 | 63 | $output->writeln(''); 64 | $output->writeln('Declarations done!'); 65 | 66 | return 0; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/Console/Command/StaticConsumerCommand.php: -------------------------------------------------------------------------------- 1 | setName('rabbitmq:staticConsumer'); 15 | $this->setDescription('Run a RabbitMQ consumer but consume just particular amount of messages'); 16 | 17 | $this->addArgument('consumerName', InputArgument::REQUIRED, 'Name of the consumer'); 18 | $this->addArgument('amountOfMessages', InputArgument::REQUIRED, 'Amount of messages to consume'); 19 | } 20 | 21 | /** 22 | * @throws \InvalidArgumentException 23 | */ 24 | protected function execute(InputInterface $input, OutputInterface $output): int 25 | { 26 | $consumerName = $input->getArgument('consumerName'); 27 | $amountOfMessages = $input->getArgument('amountOfMessages'); 28 | 29 | if (!is_string($consumerName)) { 30 | throw new \UnexpectedValueException(); 31 | } 32 | 33 | if (!is_numeric($amountOfMessages)) { 34 | throw new \UnexpectedValueException(); 35 | } 36 | 37 | $amountOfMessages = (int) $amountOfMessages; 38 | 39 | $this->validateConsumerName($consumerName); 40 | $this->validateAmountOfMessages($amountOfMessages); 41 | 42 | $consumer = $this->consumerFactory->getConsumer($consumerName); 43 | $consumer->consume(null, $amountOfMessages); 44 | 45 | return 0; 46 | } 47 | 48 | /** 49 | * @throws \InvalidArgumentException 50 | */ 51 | private function validateAmountOfMessages(int $amountOfMessages): void 52 | { 53 | if ($amountOfMessages <= 0) { 54 | throw new \InvalidArgumentException( 55 | 'Parameter [amountOfMessages] has to be greater then 0' 56 | ); 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Consumer/BulkConsumer.php: -------------------------------------------------------------------------------- 1 | 0 && $bulkTime > 0) { 37 | $this->bulkSize = $bulkSize; 38 | $this->bulkTime = $bulkTime; 39 | } else { 40 | throw new \InvalidArgumentException('Configuration values bulkSize and bulkTime must have value greater than zero'); 41 | } 42 | } 43 | 44 | public function consume(?int $maxSeconds = null, ?int $maxMessages = null): void 45 | { 46 | $this->maxMessages = $maxMessages; 47 | if ($maxSeconds > 0) { 48 | $this->stopTime = time() + $maxSeconds; 49 | } 50 | 51 | $channel = $this->queue->getConnection()->getChannel(); 52 | 53 | if ($this->prefetchSize !== null || $this->prefetchCount !== null) { 54 | $channel->qos($this->prefetchSize ?? 0, $this->prefetchCount ?? 0); 55 | } 56 | 57 | $this->setupConsume($channel); 58 | $this->startConsumeLoop($channel); 59 | 60 | //process rest of items 61 | $this->processBuffer($channel->getClient()); 62 | } 63 | 64 | private function setupConsume(Channel $channel): void 65 | { 66 | $channel->consume( 67 | function (Message $message, Channel $channel, Client $client): void { 68 | $this->messages++; 69 | $bulkCount = $this->addToBuffer(new BulkMessage($message, $channel)); 70 | if ($bulkCount >= $this->bulkSize || $this->isMaxMessages() || $this->isStopTime()) { 71 | $client->stop(); 72 | } 73 | }, 74 | $this->queue->getName() 75 | ); 76 | } 77 | 78 | private function startConsumeLoop(Channel $channel): void 79 | { 80 | do { 81 | $channel->getClient()->run($this->getTtl()); 82 | $this->processBuffer($channel->getClient()); 83 | } while (!$this->isStopTime() && !$this->isMaxMessages()); 84 | } 85 | 86 | private function addToBuffer(BulkMessage $message): int 87 | { 88 | $this->buffer[] = $message; 89 | 90 | return count($this->buffer); 91 | } 92 | 93 | private function processBuffer(AbstractClient $client): void 94 | { 95 | if (count($this->buffer) === 0) { 96 | return; 97 | } 98 | 99 | $messages = []; 100 | foreach ($this->buffer as $bulkMessage) { 101 | $message = $bulkMessage->getMessage(); 102 | $messages[$message->deliveryTag] = $message; 103 | } 104 | 105 | try { 106 | $result = call_user_func($this->callback, $messages); 107 | } catch (\Throwable $e) { 108 | $result = array_map(static fn () => IConsumer::MESSAGE_NACK, $messages); 109 | } 110 | 111 | if (!is_array($result)) { 112 | $result = array_map(static fn () => IConsumer::MESSAGE_NACK, $messages); 113 | $this->sendResultsBack($client, $result); 114 | 115 | throw new UnexpectedConsumerResultTypeException( 116 | 'Unexpected result from consumer. Expected array(delivery_tag => MESSAGE_STATUS [constant from IConsumer]) but get ' . gettype($result) 117 | ); 118 | } 119 | 120 | $result = array_map('intval', $result); 121 | 122 | $this->sendResultsBack($client, $result); 123 | 124 | $this->buffer = []; 125 | } 126 | 127 | /** 128 | * @param array $result 129 | */ 130 | private function sendResultsBack(AbstractClient $client, array $result): void 131 | { 132 | if ($client instanceof Client) { 133 | foreach ($this->buffer as $bulkMessage) { 134 | $this->sendResponse( 135 | $bulkMessage->getMessage(), 136 | $bulkMessage->getChannel(), 137 | $result[$bulkMessage->getMessage()->deliveryTag] ?? IConsumer::MESSAGE_NACK, 138 | $client 139 | ); 140 | } 141 | } 142 | } 143 | 144 | private function isStopTime(): bool 145 | { 146 | return $this->stopTime !== null && $this->stopTime < time(); 147 | } 148 | 149 | private function getTtl(): int 150 | { 151 | if ($this->stopTime > 0) { 152 | return min($this->bulkTime, $this->stopTime - time()); 153 | } 154 | 155 | return $this->bulkTime; 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /src/Consumer/BulkMessage.php: -------------------------------------------------------------------------------- 1 | message = $message; 21 | $this->channel = $channel; 22 | } 23 | 24 | public function getMessage(): Message 25 | { 26 | return $this->message; 27 | } 28 | 29 | public function getChannel(): Channel 30 | { 31 | return $this->channel; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Consumer/Consumer.php: -------------------------------------------------------------------------------- 1 | name = $name; 37 | $this->queue = $queue; 38 | $this->callback = $callback; 39 | $this->prefetchSize = $prefetchSize; 40 | $this->prefetchCount = $prefetchCount; 41 | } 42 | 43 | public function getQueue(): IQueue 44 | { 45 | return $this->queue; 46 | } 47 | 48 | public function getCallback(): callable 49 | { 50 | return $this->callback; 51 | } 52 | 53 | public function consume(?int $maxSeconds = null, ?int $maxMessages = null): void 54 | { 55 | $this->maxMessages = $maxMessages; 56 | $channel = $this->queue->getConnection()->getChannel(); 57 | 58 | if ($this->prefetchSize !== null || $this->prefetchCount !== null) { 59 | $channel->qos($this->prefetchSize ?? 0, $this->prefetchCount ?? 0); 60 | } 61 | 62 | $channel->consume( 63 | function (Message $message, Channel $channel, Client $client): void { 64 | $this->messages++; 65 | $result = call_user_func($this->callback, $message); 66 | 67 | $this->sendResponse($message, $channel, $result, $client); 68 | 69 | if ($this->isMaxMessages()) { 70 | $client->stop(); 71 | } 72 | }, 73 | $this->queue->getName() 74 | ); 75 | 76 | $channel->getClient()->run($maxSeconds); 77 | } 78 | 79 | protected function sendResponse(Message $message, Channel $channel, int $result, Client $client): void 80 | { 81 | switch ($result) { 82 | case IConsumer::MESSAGE_ACK: 83 | // Acknowledge message 84 | $channel->ack($message); 85 | 86 | break; 87 | 88 | case IConsumer::MESSAGE_NACK: 89 | // Message will be requeued 90 | $channel->nack($message); 91 | 92 | break; 93 | 94 | case IConsumer::MESSAGE_REJECT: 95 | // Message will be discarded 96 | $channel->reject($message, false); 97 | 98 | break; 99 | 100 | case IConsumer::MESSAGE_REJECT_AND_TERMINATE: 101 | // Message will be discarded 102 | $channel->reject($message, false); 103 | $client->stop(); 104 | 105 | break; 106 | 107 | case IConsumer::MESSAGE_ACK_AND_TERMINATE: 108 | // Acknowledge message and terminate 109 | $channel->ack($message); 110 | $client->stop(); 111 | 112 | break; 113 | 114 | default: 115 | throw new \InvalidArgumentException( 116 | sprintf('Unknown return value of consumer [%s] user callback', $this->name) 117 | ); 118 | } 119 | } 120 | 121 | protected function isMaxMessages(): bool 122 | { 123 | return $this->maxMessages !== null && $this->messages >= $this->maxMessages; 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/Consumer/ConsumerFactory.php: -------------------------------------------------------------------------------- 1 | consumersDataBag = $consumersDataBag; 24 | $this->queueFactory = $queueFactory; 25 | } 26 | 27 | /** 28 | * @throws ConsumerFactoryException 29 | */ 30 | public function getConsumer(string $name): Consumer 31 | { 32 | if (!isset($this->consumers[$name])) { 33 | $this->consumers[$name] = $this->create($name); 34 | } 35 | 36 | return $this->consumers[$name]; 37 | } 38 | 39 | /** 40 | * @throws ConsumerFactoryException 41 | */ 42 | private function create(string $name): Consumer 43 | { 44 | try { 45 | $consumerData = $this->consumersDataBag->getDataBykey($name); 46 | 47 | } catch (\InvalidArgumentException $e) { 48 | throw new ConsumerFactoryException(sprintf('Consumer [%s] does not exist', $name)); 49 | } 50 | 51 | $queue = $this->queueFactory->getQueue($consumerData['queue']); 52 | 53 | if (!is_callable($consumerData['callback'])) { 54 | throw new ConsumerFactoryException(sprintf('Consumer [%s] has invalid callback', $name)); 55 | } 56 | 57 | $prefetchSize = null; 58 | $prefetchCount = null; 59 | 60 | if ($consumerData['qos']['prefetchSize'] !== null) { 61 | $prefetchSize = $consumerData['qos']['prefetchSize']; 62 | } 63 | 64 | if ($consumerData['qos']['prefetchCount'] !== null) { 65 | $prefetchCount = $consumerData['qos']['prefetchCount']; 66 | } 67 | 68 | if (is_array($consumerData['bulk']) && $consumerData['bulk']['size']) { 69 | return new BulkConsumer( 70 | $name, 71 | $queue, 72 | $consumerData['callback'], 73 | $prefetchSize, 74 | $prefetchCount, 75 | (int) $consumerData['bulk']['size'], 76 | (int) $consumerData['bulk']['timeout'] 77 | ); 78 | } 79 | 80 | return new Consumer( 81 | $name, 82 | $queue, 83 | $consumerData['callback'], 84 | $prefetchSize, 85 | $prefetchCount 86 | ); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Consumer/ConsumersDataBag.php: -------------------------------------------------------------------------------- 1 | */ 11 | protected array $defaults = []; 12 | 13 | protected CompilerExtension $extension; 14 | 15 | public function __construct(CompilerExtension $extension) 16 | { 17 | $this->extension = $extension; 18 | } 19 | 20 | /** 21 | * @return array 22 | */ 23 | public function getDefaults(): array 24 | { 25 | return $this->defaults; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/DI/Helpers/ConnectionsHelper.php: -------------------------------------------------------------------------------- 1 | */ 14 | protected array $defaults = [ 15 | 'host' => '127.0.0.1', 16 | 'port' => 5672, 17 | 'user' => 'guest', 18 | 'password' => 'guest', 19 | 'vhost' => '/', 20 | 'timeout' => 1, 21 | 'heartbeat' => 60.0, 22 | 'persistent' => false, 23 | 'path' => '/', 24 | 'tcpNoDelay' => false, 25 | 'lazy' => false, 26 | 'ssl' => null, 27 | ]; 28 | 29 | /** 30 | * @param array $config 31 | */ 32 | public function setup(ContainerBuilder $builder, array $config = []): ServiceDefinition 33 | { 34 | $connectionsConfig = []; 35 | 36 | foreach ($config as $connectionName => $connectionData) { 37 | // @phpstan-ignore-next-line 38 | $connectionsConfig[$connectionName] = $this->extension->validateConfig( 39 | $this->getDefaults(), 40 | $connectionData 41 | ); 42 | } 43 | 44 | $connectionsDataBag = $builder->addDefinition($this->extension->prefix('connectionsDataBag')) 45 | ->setFactory(ConnectionsDataBag::class) 46 | ->setArguments([$connectionsConfig]); 47 | 48 | return $builder->addDefinition($this->extension->prefix('connectionFactory')) 49 | ->setFactory(ConnectionFactory::class) 50 | ->setArguments([$connectionsDataBag]); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/DI/Helpers/ConsumersHelper.php: -------------------------------------------------------------------------------- 1 | */ 14 | protected array $defaults = [ 15 | 'queue' => null, 16 | 'callback' => null, 17 | 'idleTimeout' => 30, 18 | 'bulk' => [ 19 | 'size' => null, 20 | 'timeout' => null, 21 | ], 22 | 'qos' => [ 23 | 'prefetchSize' => null, // 0 24 | 'prefetchCount' => null, // 50 25 | ], 26 | ]; 27 | 28 | /** 29 | * @param array $config 30 | */ 31 | public function setup(ContainerBuilder $builder, array $config = []): ServiceDefinition 32 | { 33 | $consumersConfig = []; 34 | 35 | foreach ($config as $consumerName => $consumerData) { 36 | // @phpstan-ignore-next-line 37 | $consumerConfig = $this->extension->validateConfig( 38 | $this->getDefaults(), 39 | $consumerData 40 | ); 41 | 42 | if ($consumerConfig === []) { 43 | throw new \InvalidArgumentException( 44 | 'Each consumer has to have a parameter set' 45 | ); 46 | } 47 | 48 | $consumersConfig[$consumerName] = $consumerConfig; 49 | } 50 | 51 | $consumersDataBag = $builder->addDefinition($this->extension->prefix('consumersDataBag')) 52 | ->setFactory(ConsumersDataBag::class) 53 | ->setArguments([$consumersConfig]); 54 | 55 | return $builder->addDefinition($this->extension->prefix('consumerFactory')) 56 | ->setFactory(ConsumerFactory::class) 57 | ->setArguments([$consumersDataBag]); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/DI/Helpers/ExchangesHelper.php: -------------------------------------------------------------------------------- 1 | */ 17 | protected array $defaults = [ 18 | 'connection' => 'default', 19 | // direct/topic/headers/fanout 20 | 'type' => 'direct', 21 | 'passive' => false, 22 | 'durable' => true, 23 | 'autoDelete' => false, 24 | 'internal' => false, 25 | 'noWait' => false, 26 | 'arguments' => [], 27 | // See self::$queueBindingDefaults 28 | 'queueBindings' => [], 29 | 'autoCreate' => false, 30 | ]; 31 | 32 | /** @var array */ 33 | private array $queueBindingDefaults = [ 34 | 'routingKey' => '', 35 | 'routingKeys' => [], 36 | 'noWait' => false, 37 | 'arguments' => [], 38 | ]; 39 | 40 | /** 41 | * @param array $config 42 | */ 43 | public function setup(ContainerBuilder $builder, array $config = []): ServiceDefinition 44 | { 45 | $exchangesConfig = []; 46 | 47 | foreach ($config as $exchangeName => $exchangeData) { 48 | // @phpstan-ignore-next-line 49 | $exchangeConfig = $this->extension->validateConfig( 50 | $this->getDefaults(), 51 | $exchangeData 52 | ); 53 | 54 | // Validate exchange type 55 | if (!in_array($exchangeConfig['type'], self::EXCHANGE_TYPES, true)) { 56 | throw new \InvalidArgumentException( 57 | sprintf('Unknown exchange type [%s]', $exchangeConfig['type']) 58 | ); 59 | } 60 | 61 | if ($exchangeConfig['queueBindings'] !== []) { 62 | foreach ($exchangeConfig['queueBindings'] as $queueName => $queueBindingData) { 63 | if (isset($queueBindingData['routingKey']) && isset($queueBindingData['routingKeys'])) { 64 | throw new \InvalidArgumentException( 65 | 'Options `routingKey` and `routingKeys` cannot be specified at the same time' 66 | ); 67 | } 68 | 69 | // @phpstan-ignore-next-line 70 | $queueBindingConfig = $this->extension->validateConfig( 71 | $this->queueBindingDefaults, 72 | $queueBindingData 73 | ); 74 | 75 | $queueBindingConfig['routingKey'] = (string) $queueBindingConfig['routingKey']; 76 | $queueBindingConfig['routingKeys'] = array_map('strval', (array) $queueBindingConfig['routingKeys']); 77 | 78 | $exchangeConfig['queueBindings'][$queueName] = $queueBindingConfig; 79 | } 80 | } 81 | 82 | $exchangesConfig[$exchangeName] = $exchangeConfig; 83 | } 84 | 85 | $exchangesDataBag = $builder->addDefinition($this->extension->prefix('exchangesDataBag')) 86 | ->setFactory(ExchangesDataBag::class) 87 | ->setArguments([$exchangesConfig]); 88 | 89 | $builder->addDefinition($this->extension->prefix('exchangesDeclarator')) 90 | ->setFactory(ExchangeDeclarator::class); 91 | 92 | return $builder->addDefinition($this->extension->prefix('exchangeFactory')) 93 | ->setFactory(ExchangeFactory::class) 94 | ->setArguments([$exchangesDataBag]); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/DI/Helpers/ProducersHelper.php: -------------------------------------------------------------------------------- 1 | */ 20 | protected array $defaults = [ 21 | 'exchange' => null, 22 | 'queue' => null, 23 | 'contentType' => 'text/plain', 24 | 'deliveryMode' => Producer::DELIVERY_MODE_PERSISTENT, 25 | ]; 26 | 27 | /** 28 | * @param array $config 29 | */ 30 | public function setup(ContainerBuilder $builder, array $config = []): ServiceDefinition 31 | { 32 | $producersConfig = []; 33 | 34 | foreach ($config as $producerName => $producerData) { 35 | // @phpstan-ignore-next-line 36 | $producerConfig = $this->extension->validateConfig( 37 | $this->getDefaults(), 38 | $producerData 39 | ); 40 | 41 | $producersConfig[$producerName] = $producerConfig; 42 | } 43 | 44 | $producersDataBag = $builder->addDefinition($this->extension->prefix('producersDataBag')) 45 | ->setFactory(ProducersDataBag::class) 46 | ->setArguments([$producersConfig]); 47 | 48 | return $builder->addDefinition($this->extension->prefix('producerFactory')) 49 | ->setFactory(ProducerFactory::class) 50 | ->setArguments([$producersDataBag]); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/DI/Helpers/QueuesHelper.php: -------------------------------------------------------------------------------- 1 | */ 15 | protected array $defaults = [ 16 | 'connection' => 'default', 17 | 'passive' => false, 18 | 'durable' => true, 19 | 'exclusive' => false, 20 | 'autoDelete' => false, 21 | 'noWait' => false, 22 | 'arguments' => [], 23 | 'autoCreate' => false, 24 | ]; 25 | 26 | /** 27 | * @param array $config 28 | */ 29 | public function setup(ContainerBuilder $builder, array $config = []): ServiceDefinition 30 | { 31 | $queuesConfig = []; 32 | 33 | foreach ($config as $queueName => $queueData) { 34 | // @phpstan-ignore-next-line 35 | $queuesConfig[$queueName] = $this->extension->validateConfig( 36 | $this->getDefaults(), 37 | $queueData 38 | ); 39 | } 40 | 41 | $queuesDataBag = $builder->addDefinition($this->extension->prefix('queuesDataBag')) 42 | ->setFactory(QueuesDataBag::class) 43 | ->setArguments([$queuesConfig]); 44 | 45 | $builder->addDefinition($this->extension->prefix('queueDeclarator')) 46 | ->setFactory(QueueDeclarator::class); 47 | 48 | return $builder->addDefinition($this->extension->prefix('queueFactory')) 49 | ->setFactory(QueueFactory::class) 50 | ->setArguments([$queuesDataBag]); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/DI/RabbitMQExtension.php: -------------------------------------------------------------------------------- 1 | */ 20 | private array $defaults = [ 21 | 'connections' => [], 22 | 'queues' => [], 23 | 'exchanges' => [], 24 | 'producers' => [], 25 | 'consumers' => [], 26 | ]; 27 | 28 | private ConnectionsHelper $connectionsHelper; 29 | 30 | private QueuesHelper $queuesHelper; 31 | 32 | private ProducersHelper $producersHelper; 33 | 34 | private ExchangesHelper $exchangesHelper; 35 | 36 | private ConsumersHelper $consumersHelper; 37 | 38 | public function __construct() 39 | { 40 | $this->connectionsHelper = new ConnectionsHelper($this); 41 | $this->queuesHelper = new QueuesHelper($this); 42 | $this->exchangesHelper = new ExchangesHelper($this); 43 | $this->producersHelper = new ProducersHelper($this); 44 | $this->consumersHelper = new ConsumersHelper($this); 45 | } 46 | 47 | public function loadConfiguration(): void 48 | { 49 | $config = $this->validateConfig($this->defaults); // @phpstan-ignore-line 50 | $builder = $this->getContainerBuilder(); 51 | 52 | $this->connectionsHelper->setup($builder, $config['connections']); 53 | $this->queuesHelper->setup($builder, $config['queues']); 54 | $this->exchangesHelper->setup($builder, $config['exchanges']); 55 | $this->producersHelper->setup($builder, $config['producers']); 56 | $this->consumersHelper->setup($builder, $config['consumers']); 57 | 58 | // Register Client class 59 | $builder->addDefinition($this->prefix('client')) 60 | ->setFactory(Client::class); 61 | 62 | $this->setupConsoleCommand(); 63 | } 64 | 65 | public function setupConsoleCommand(): void 66 | { 67 | $builder = $this->getContainerBuilder(); 68 | 69 | $builder->addDefinition($this->prefix('console.consumerCommand')) 70 | ->setFactory(ConsumerCommand::class) 71 | ->setTags(['console.command' => 'rabbitmq:consumer']); 72 | 73 | $builder->addDefinition($this->prefix('console.staticConsumerCommand')) 74 | ->setFactory(StaticConsumerCommand::class) 75 | ->setTags(['console.command' => 'rabbitmq:staticConsumer']); 76 | 77 | $builder->addDefinition($this->prefix('console.declareQueuesExchangesCommand')) 78 | ->setFactory(DeclareQueuesAndExchangesCommand::class) 79 | ->setTags(['console.command' => 'rabbitmq:declareQueuesAndExchanges']); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/DI/RabbitMQExtension24.php: -------------------------------------------------------------------------------- 1 | */ 20 | private array $defaults = [ 21 | 'connections' => [], 22 | 'queues' => [], 23 | 'exchanges' => [], 24 | 'producers' => [], 25 | 'consumers' => [], 26 | ]; 27 | 28 | private ConnectionsHelper $connectionsHelper; 29 | 30 | private QueuesHelper $queuesHelper; 31 | 32 | private ProducersHelper $producersHelper; 33 | 34 | private ExchangesHelper $exchangesHelper; 35 | 36 | private ConsumersHelper $consumersHelper; 37 | 38 | public function __construct() 39 | { 40 | $this->connectionsHelper = new ConnectionsHelper($this); 41 | $this->queuesHelper = new QueuesHelper($this); 42 | $this->exchangesHelper = new ExchangesHelper($this); 43 | $this->producersHelper = new ProducersHelper($this); 44 | $this->consumersHelper = new ConsumersHelper($this); 45 | } 46 | 47 | public function loadConfiguration(): void 48 | { 49 | $config = $this->validateConfig($this->defaults); // @phpstan-ignore-line 50 | $builder = $this->getContainerBuilder(); 51 | 52 | $this->connectionsHelper->setup($builder, $config['connections']); 53 | $this->queuesHelper->setup($builder, $config['queues']); 54 | $this->exchangesHelper->setup($builder, $config['exchanges']); 55 | $this->producersHelper->setup($builder, $config['producers']); 56 | $this->consumersHelper->setup($builder, $config['consumers']); 57 | 58 | // Register Client class 59 | $builder->addDefinition($this->prefix('client')) 60 | ->setFactory(Client::class); 61 | 62 | $this->setupConsoleCommand(); 63 | } 64 | 65 | public function setupConsoleCommand(): void 66 | { 67 | $builder = $this->getContainerBuilder(); 68 | 69 | $builder->addDefinition($this->prefix('console.consumerCommand')) 70 | ->setFactory(ConsumerCommand::class) 71 | ->setTags(['console.command' => 'rabbitmq:consumer']); 72 | 73 | $builder->addDefinition($this->prefix('console.staticConsumerCommand')) 74 | ->setFactory(StaticConsumerCommand::class) 75 | ->setTags(['console.command' => 'rabbitmq:staticConsumer']); 76 | 77 | $builder->addDefinition($this->prefix('console.declareQueuesExchangesCommand')) 78 | ->setFactory(DeclareQueuesAndExchangesCommand::class) 79 | ->setTags(['console.command' => 'rabbitmq:declareQueuesAndExchanges']); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/Diagnostics/BarPanel.php: -------------------------------------------------------------------------------- 1 | */ 21 | private array $sentMessages = []; 22 | 23 | private int $totalMessages = 0; 24 | 25 | public function __construct(ProducerFactory $producerFactory) 26 | { 27 | $this->producerFactory = $producerFactory; 28 | 29 | $this->producerFactory->addOnCreatedCallback( 30 | function (string $name, Producer $producer): void { 31 | $this->sentMessages[$name] = []; 32 | $producer->addOnPublishCallback( 33 | function (string $message) use ($name): void { 34 | if (self::$displayCount === 0 || $this->totalMessages < self::$displayCount) { 35 | $this->sentMessages[$name][] = $message; 36 | } 37 | 38 | $this->totalMessages++; 39 | } 40 | ); 41 | } 42 | ); 43 | } 44 | 45 | public function getTab(): string 46 | { 47 | $img = Html::el('')->addHtml((string) file_get_contents(__DIR__ . '/rabbitmq-icon.svg')); 48 | $tab = Html::el('span')->title('RabbitMq')->addHtml($img); 49 | 50 | if ($this->totalMessages > 0) { 51 | $title = Html::el('span')->class('tracy-label'); 52 | $tab->addHtml($title->setText(' (' . $this->totalMessages . ')')); 53 | } 54 | 55 | return (string) $tab; 56 | } 57 | 58 | public function getPanel(): string 59 | { 60 | ob_start(static function (): void { 61 | }); 62 | 63 | // @codingStandardsIgnoreStart 64 | $sentMessages = $this->sentMessages; 65 | $totalMessages = $this->totalMessages; 66 | $displayCount = self::$displayCount; 67 | // @codingStandardsIgnoreEnd 68 | 69 | try { 70 | require __DIR__ . '/BarPanel.phtml'; 71 | 72 | return (string) ob_get_clean(); 73 | } catch (\Throwable $e) { 74 | ob_get_clean(); 75 | 76 | throw $e; 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/Diagnostics/BarPanel.phtml: -------------------------------------------------------------------------------- 1 |

RabbitMq, total sent

2 | 3 | $displayCount): ?> 4 |

Displayed only first messages

5 | 6 | 7 |
8 | 9 | $producerMessages): ?> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Producer:
18 |
19 | -------------------------------------------------------------------------------- /src/Diagnostics/rabbitmq-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Exchange/Exception/ExchangeFactoryException.php: -------------------------------------------------------------------------------- 1 | */ 13 | private array $queueBindings; 14 | 15 | private IConnection $connection; 16 | 17 | /** 18 | * @param array $queueBindings 19 | */ 20 | public function __construct( 21 | string $name, 22 | array $queueBindings, 23 | IConnection $connection 24 | ) 25 | { 26 | $this->name = $name; 27 | $this->queueBindings = $queueBindings; 28 | $this->connection = $connection; 29 | } 30 | 31 | public function getName(): string 32 | { 33 | return $this->name; 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | public function getQueueBindings(): array 40 | { 41 | return $this->queueBindings; 42 | } 43 | 44 | public function getConnection(): IConnection 45 | { 46 | return $this->connection; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Exchange/ExchangeDeclarator.php: -------------------------------------------------------------------------------- 1 | connectionFactory = $connectionFactory; 25 | $this->exchangesDataBag = $exchangesDataBag; 26 | $this->queueFactory = $queueFactory; 27 | } 28 | 29 | public function declareExchange(string $name): void 30 | { 31 | try { 32 | $exchangeData = $this->exchangesDataBag->getDataBykey($name); 33 | } catch (\InvalidArgumentException $e) { 34 | throw new ExchangeFactoryException(sprintf('Exchange [%s] does not exist', $name)); 35 | } 36 | 37 | $connection = $this->connectionFactory->getConnection($exchangeData['connection']); 38 | 39 | $connection->getChannel()->exchangeDeclare( 40 | $name, 41 | $exchangeData['type'], 42 | $exchangeData['passive'], 43 | $exchangeData['durable'], 44 | $exchangeData['autoDelete'], 45 | $exchangeData['internal'], 46 | $exchangeData['noWait'], 47 | $exchangeData['arguments'] 48 | ); 49 | 50 | if ($exchangeData['queueBindings'] !== []) { 51 | foreach ($exchangeData['queueBindings'] as $queueName => $queueBinding) { 52 | $queue = $this->queueFactory->getQueue($queueName); 53 | 54 | $routingKeysToBind = []; 55 | 56 | if (isset($queueBinding['routingKeys']) 57 | && $queueBinding['routingKeys'] !== [] 58 | ) { 59 | $routingKeysToBind = $queueBinding['routingKeys']; 60 | } elseif (isset($queueBinding['routingKey'])) { 61 | $routingKeysToBind = [$queueBinding['routingKey']]; 62 | } 63 | 64 | foreach ($routingKeysToBind as $routingKey) { 65 | $connection->getChannel()->queueBind( 66 | $queue->getName(), 67 | $name, 68 | $routingKey, 69 | $queueBinding['noWait'], 70 | $queueBinding['arguments'] 71 | ); 72 | } 73 | } 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/Exchange/ExchangeFactory.php: -------------------------------------------------------------------------------- 1 | exchangesDataBag = $exchangesDataBag; 32 | $this->queueFactory = $queueFactory; 33 | $this->connectionFactory = $connectionFactory; 34 | $this->exchangeDeclarator = $exchangeDeclarator; 35 | } 36 | 37 | /** 38 | * @throws ExchangeFactoryException 39 | */ 40 | public function getExchange(string $name): IExchange 41 | { 42 | if (!isset($this->exchanges[$name])) { 43 | $this->exchanges[$name] = $this->create($name); 44 | } 45 | 46 | return $this->exchanges[$name]; 47 | } 48 | 49 | /** 50 | * @throws ExchangeFactoryException 51 | * @throws QueueFactoryException 52 | */ 53 | private function create(string $name): IExchange 54 | { 55 | $queueBindings = []; 56 | 57 | try { 58 | $exchangeData = $this->exchangesDataBag->getDataBykey($name); 59 | 60 | } catch (\InvalidArgumentException $e) { 61 | throw new ExchangeFactoryException(sprintf('Exchange [%s] does not exist', $name)); 62 | } 63 | 64 | $connection = $this->connectionFactory->getConnection($exchangeData['connection']); 65 | 66 | if (isset($exchangeData['autoCreate']) && $exchangeData['autoCreate'] === true) { 67 | $this->exchangeDeclarator->declareExchange($name); 68 | } 69 | 70 | if ($exchangeData['queueBindings'] !== []) { 71 | foreach ($exchangeData['queueBindings'] as $queueName => $queueBinding) { 72 | // (QueueFactoryException) 73 | $queue = $this->queueFactory->getQueue($queueName); 74 | 75 | $queueBindings[] = new QueueBinding( 76 | $queue, 77 | $queueBinding['routingKey'], 78 | ...$queueBinding['routingKeys'] ?? [] 79 | ); 80 | } 81 | } 82 | 83 | return new Exchange( 84 | $name, 85 | $queueBindings, 86 | $connection 87 | ); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/Exchange/ExchangesDataBag.php: -------------------------------------------------------------------------------- 1 | $config 12 | */ 13 | public function addExchangeConfig(string $exchangeName, array $config): void 14 | { 15 | $this->data[$exchangeName] = $config; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/Exchange/IExchange.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function getQueueBindings(): array; 16 | 17 | public function getConnection(): IConnection; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/Exchange/QueueBinding.php: -------------------------------------------------------------------------------- 1 | */ 15 | private array $routingKeys; 16 | 17 | public function __construct( 18 | IQueue $queue, 19 | string $routingKey, 20 | string ...$routingKeys 21 | ) 22 | { 23 | $this->queue = $queue; 24 | $this->routingKey = $routingKey; 25 | $this->routingKeys = $routingKeys; 26 | } 27 | 28 | public function getQueue(): IQueue 29 | { 30 | return $this->queue; 31 | } 32 | 33 | public function getRoutingKey(): string 34 | { 35 | return $this->routingKey; 36 | } 37 | 38 | /** 39 | * @return array 40 | */ 41 | public function getRoutingKeys(): array 42 | { 43 | return $this->routingKeys; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Producer/Exception/ProducerFactoryException.php: -------------------------------------------------------------------------------- 1 | exchange = $exchange; 33 | $this->queue = $queue; 34 | $this->contentType = $contentType; 35 | $this->deliveryMode = $deliveryMode; 36 | } 37 | 38 | /** 39 | * @param array $headers 40 | */ 41 | public function publish(string $message, array $headers = [], ?string $routingKey = null): void 42 | { 43 | $headers = array_merge($this->getBasicHeaders(), $headers); 44 | 45 | if ($this->queue !== null) { 46 | $this->publishToQueue($message, $headers); 47 | } 48 | 49 | if ($this->exchange !== null) { 50 | $this->publishToExchange($message, $headers, $routingKey ?? ''); 51 | } 52 | 53 | foreach ($this->publishCallbacks as $callback) { 54 | ($callback)($message, $headers, $routingKey); 55 | } 56 | } 57 | 58 | public function addOnPublishCallback(callable $callback): void 59 | { 60 | $this->publishCallbacks[] = $callback; 61 | } 62 | 63 | public function sendHeartbeat(): void 64 | { 65 | if ($this->queue !== null) { 66 | $this->queue->getConnection()->sendHeartbeat(); 67 | } 68 | 69 | if ($this->exchange !== null) { 70 | $this->exchange->getConnection()->sendHeartbeat(); 71 | } 72 | } 73 | 74 | /** 75 | * @return array 76 | */ 77 | private function getBasicHeaders(): array 78 | { 79 | return [ 80 | 'content-type' => $this->contentType, 81 | 'delivery-mode' => $this->deliveryMode, 82 | ]; 83 | } 84 | 85 | /** 86 | * @param array $headers 87 | */ 88 | private function publishToQueue(string $message, array $headers = []): void 89 | { 90 | if ($this->queue === null) { 91 | throw new \UnexpectedValueException('Queue is not defined'); 92 | } 93 | 94 | $this->queue->getConnection()->getChannel()->publish( 95 | $message, 96 | $headers, 97 | // Exchange name 98 | '', 99 | // Routing key, in this case the queue's name 100 | $this->queue->getName() 101 | ); 102 | } 103 | 104 | /** 105 | * @param array $headers 106 | */ 107 | private function publishToExchange(string $message, array $headers, string $routingKey): void 108 | { 109 | if ($this->exchange === null) { 110 | throw new \UnexpectedValueException('Exchange is not defined'); 111 | } 112 | 113 | $this->exchange->getConnection()->getChannel()->publish( 114 | $message, 115 | $headers, 116 | $this->exchange->getName(), 117 | $routingKey 118 | ); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/Producer/ProducerFactory.php: -------------------------------------------------------------------------------- 1 | producersDataBag = $producersDataBag; 31 | $this->queueFactory = $queueFactory; 32 | $this->exchangeFactory = $exchangeFactory; 33 | } 34 | 35 | /** 36 | * @throws ProducerFactoryException 37 | */ 38 | public function getProducer(string $name): Producer 39 | { 40 | if (!isset($this->producers[$name])) { 41 | $this->producers[$name] = $this->create($name); 42 | } 43 | 44 | return $this->producers[$name]; 45 | } 46 | 47 | public function addOnCreatedCallback(callable $callback): void 48 | { 49 | $this->createdCallbacks[] = $callback; 50 | } 51 | 52 | /** 53 | * @throws ProducerFactoryException 54 | */ 55 | private function create(string $name): Producer 56 | { 57 | try { 58 | $producerData = $this->producersDataBag->getDataBykey($name); 59 | 60 | } catch (\InvalidArgumentException $e) { 61 | throw new ProducerFactoryException(sprintf('Producer [%s] does not exist', $name)); 62 | } 63 | 64 | $exchange = null; 65 | $queue = null; 66 | 67 | if (isset($producerData['exchange'])) { 68 | $exchange = $this->exchangeFactory->getExchange($producerData['exchange']); 69 | } 70 | 71 | if ($producerData['queue']) { 72 | $queue = $this->queueFactory->getQueue($producerData['queue']); 73 | } 74 | 75 | $producer = new Producer( 76 | $exchange, 77 | $queue, 78 | $producerData['contentType'], 79 | $producerData['deliveryMode'] 80 | ); 81 | 82 | foreach ($this->createdCallbacks as $callback) { 83 | ($callback)($name, $producer); 84 | } 85 | 86 | return $producer; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Producer/ProducersDataBag.php: -------------------------------------------------------------------------------- 1 | $data 13 | * @throws \InvalidArgumentException 14 | */ 15 | public function __construct(array $data) 16 | { 17 | parent::__construct($data); 18 | 19 | foreach ($data as $producerName => $producer) { 20 | $this->addProducerByData($producerName, $producer); 21 | } 22 | } 23 | 24 | /** 25 | * @param array $data 26 | * @throws \InvalidArgumentException 27 | */ 28 | public function addProducerByData(string $producerName, array $data): void 29 | { 30 | $data['deliveryMode'] ??= Producer::DELIVERY_MODE_PERSISTENT; 31 | $data['contentType'] ??= 'text/plain'; 32 | $data['exchange'] ??= null; 33 | $data['queue'] ??= null; 34 | 35 | if (!in_array($data['deliveryMode'], ProducersHelper::DELIVERY_MODES, true)) { 36 | throw new \InvalidArgumentException( 37 | sprintf('Unknown exchange type [%s]', $data['type']) 38 | ); 39 | } 40 | 41 | /** 42 | * 1, Producer has to be subscribed to either a queue or an exchange 43 | * 2, A producer can be subscribed to both a queue and an exchange 44 | */ 45 | if ($data['queue'] === [] && $data['exchange'] === []) { 46 | throw new \InvalidArgumentException( 47 | 'Producer has to be subscribed to either a queue or an exchange' 48 | ); 49 | } 50 | 51 | $this->data[$producerName] = $data; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/Queue/Exception/QueueFactoryException.php: -------------------------------------------------------------------------------- 1 | name = $name; 20 | $this->connection = $connection; 21 | } 22 | 23 | public function getName(): string 24 | { 25 | return $this->name; 26 | } 27 | 28 | public function getConnection(): IConnection 29 | { 30 | return $this->connection; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Queue/QueueDeclarator.php: -------------------------------------------------------------------------------- 1 | queuesDataBag = $queuesDataBag; 18 | $this->connectionFactory = $connectionFactory; 19 | } 20 | 21 | public function declareQueue(string $name): void 22 | { 23 | try { 24 | $queueData = $this->queuesDataBag->getDataBykey($name); 25 | 26 | } catch (\InvalidArgumentException $e) { 27 | throw new QueueFactoryException(sprintf('Queue [%s] does not exist', $name)); 28 | } 29 | 30 | $connection = $this->connectionFactory->getConnection($queueData['connection']); 31 | 32 | $connection->getChannel()->queueDeclare( 33 | $name, 34 | $queueData['passive'], 35 | $queueData['durable'], 36 | $queueData['exclusive'], 37 | $queueData['autoDelete'], 38 | $queueData['noWait'], 39 | $queueData['arguments'] 40 | ); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/Queue/QueueFactory.php: -------------------------------------------------------------------------------- 1 | queuesDataBag = $queuesDataBag; 28 | $this->connectionFactory = $connectionFactory; 29 | $this->queueDeclarator = $queueDeclarator; 30 | } 31 | 32 | /** 33 | * @throws QueueFactoryException 34 | */ 35 | public function getQueue(string $name): IQueue 36 | { 37 | if (!isset($this->queues[$name])) { 38 | $this->queues[$name] = $this->create($name); 39 | } 40 | 41 | return $this->queues[$name]; 42 | } 43 | 44 | /** 45 | * @throws QueueFactoryException 46 | * @throws ConnectionFactoryException 47 | */ 48 | private function create(string $name): IQueue 49 | { 50 | try { 51 | $queueData = $this->queuesDataBag->getDataBykey($name); 52 | 53 | } catch (\InvalidArgumentException $e) { 54 | throw new QueueFactoryException(sprintf('Queue [%s] does not exist', $name)); 55 | } 56 | 57 | $connection = $this->connectionFactory->getConnection($queueData['connection']); 58 | 59 | if (isset($queueData['autoCreate']) && $queueData['autoCreate'] === true) { 60 | $this->queueDeclarator->declareQueue($name); 61 | } 62 | 63 | return new Queue( 64 | $name, 65 | $connection 66 | ); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/Queue/QueuesDataBag.php: -------------------------------------------------------------------------------- 1 | $config 12 | */ 13 | public function addQueueByData(string $queueName, array $config): void 14 | { 15 | $this->data[$queueName] = $config; 16 | } 17 | 18 | } 19 | --------------------------------------------------------------------------------