├── Serializer.php ├── SerializerAwareTrait.php ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── JsonSerializer.php ├── RdKafkaTopic.php ├── composer.json ├── README.md ├── RdKafkaConnectionFactory.php ├── RdKafkaProducer.php ├── RdKafkaMessage.php ├── RdKafkaConsumer.php └── RdKafkaContext.php /Serializer.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 17 | } 18 | 19 | /** 20 | * @return Serializer 21 | */ 22 | public function getSerializer() 23 | { 24 | return $this->serializer; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | php: ['8.2', '8.3', '8.4'] 14 | 15 | name: PHP ${{ matrix.php }} tests 16 | 17 | steps: 18 | - uses: actions/checkout@v6 19 | 20 | - uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | coverage: none 24 | 25 | - uses: "ramsey/composer-install@v3" 26 | with: # ext-rdkafka not needed for tests, and a pain to install on CI; 27 | composer-options: "--ignore-platform-req=ext-rdkafka" 28 | 29 | - run: sed -i 's/525568/16777471/' vendor/kwn/php-rdkafka-stubs/stubs/constants.php 30 | 31 | - run: vendor/bin/phpunit --exclude-group=functional 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Kotliar Maksym 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /JsonSerializer.php: -------------------------------------------------------------------------------- 1 | $message->getBody(), 13 | 'properties' => $message->getProperties(), 14 | 'headers' => $message->getHeaders(), 15 | ]); 16 | 17 | if (\JSON_ERROR_NONE !== json_last_error()) { 18 | throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); 19 | } 20 | 21 | return $json; 22 | } 23 | 24 | public function toMessage(string $string): RdKafkaMessage 25 | { 26 | $data = json_decode($string, true); 27 | if (\JSON_ERROR_NONE !== json_last_error()) { 28 | throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); 29 | } 30 | 31 | return new RdKafkaMessage($data['body'], $data['properties'], $data['headers']); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /RdKafkaTopic.php: -------------------------------------------------------------------------------- 1 | name = $name; 36 | } 37 | 38 | public function getTopicName(): string 39 | { 40 | return $this->name; 41 | } 42 | 43 | public function getQueueName(): string 44 | { 45 | return $this->name; 46 | } 47 | 48 | public function getConf(): ?TopicConf 49 | { 50 | return $this->conf; 51 | } 52 | 53 | public function setConf(?TopicConf $conf = null): void 54 | { 55 | $this->conf = $conf; 56 | } 57 | 58 | public function getPartition(): ?int 59 | { 60 | return $this->partition; 61 | } 62 | 63 | public function setPartition(?int $partition = null): void 64 | { 65 | $this->partition = $partition; 66 | } 67 | 68 | public function getKey(): ?string 69 | { 70 | return $this->key; 71 | } 72 | 73 | public function setKey(?string $key = null): void 74 | { 75 | $this->key = $key; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enqueue/rdkafka", 3 | "type": "library", 4 | "description": "Message Queue Kafka Transport", 5 | "keywords": ["messaging", "queue", "kafka"], 6 | "homepage": "https://enqueue.forma-pro.com/", 7 | "license": "MIT", 8 | "require": { 9 | "php": "^8.1", 10 | "ext-rdkafka": "^4.0|^5.0|^6.0", 11 | "queue-interop/queue-interop": "^0.8.1" 12 | }, 13 | "require-dev": { 14 | "phpunit/phpunit": "^9.5", 15 | "enqueue/test": "0.10.x-dev", 16 | "enqueue/null": "0.10.x-dev", 17 | "queue-interop/queue-spec": "^0.6.2", 18 | "kwn/php-rdkafka-stubs": "^2.0.3" 19 | }, 20 | "support": { 21 | "email": "opensource@forma-pro.com", 22 | "issues": "https://github.com/php-enqueue/enqueue-dev/issues", 23 | "forum": "https://gitter.im/php-enqueue/Lobby", 24 | "source": "https://github.com/php-enqueue/enqueue-dev", 25 | "docs": "https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md" 26 | }, 27 | "autoload": { 28 | "psr-4": { "Enqueue\\RdKafka\\": "" }, 29 | "exclude-from-classmap": [ 30 | "/Tests/" 31 | ] 32 | }, 33 | "autoload-dev": { 34 | "files": [ 35 | "Tests/bootstrap.php" 36 | ], 37 | "psr-0": { 38 | "RdKafka": "vendor/kwn/php-rdkafka-stubs/stubs" 39 | }, 40 | "psr-4": { 41 | "RdKafka\\": "vendor/kwn/php-rdkafka-stubs/stubs/RdKafka" 42 | } 43 | }, 44 | "minimum-stability": "dev", 45 | "extra": { 46 | "branch-alias": { 47 | "dev-master": "0.10.x-dev" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Supporting Enqueue

2 | 3 | Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: 4 | 5 | - [Become a sponsor](https://www.patreon.com/makasim) 6 | - [Become our client](http://forma-pro.com/) 7 | 8 | --- 9 | 10 | # RdKafka Transport 11 | 12 | [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) 13 | [![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/rdkafka/ci.yml?branch=master)](https://github.com/php-enqueue/rdkafka/actions?query=workflow%3ACI) 14 | [![Total Downloads](https://poser.pugx.org/enqueue/rdkafka/d/total.png)](https://packagist.org/packages/enqueue/rdkafka) 15 | [![Latest Stable Version](https://poser.pugx.org/enqueue/rdkafka/version.png)](https://packagist.org/packages/enqueue/rdkafka) 16 | 17 | This is an implementation of Queue Interop specification. It allows you to send and consume message via Kafka protocol. 18 | 19 | ## Resources 20 | 21 | * [Site](https://enqueue.forma-pro.com/) 22 | * [Documentation](https://php-enqueue.github.io/transport/kafka/) 23 | * [Questions](https://gitter.im/php-enqueue/Lobby) 24 | * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) 25 | 26 | ## Developed by Forma-Pro 27 | 28 | Forma-Pro is a full stack development company which interests also spread to open source development. 29 | Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. 30 | Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. 31 | 32 | If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com 33 | 34 | ## License 35 | 36 | It is released under the [MIT License](LICENSE). 37 | -------------------------------------------------------------------------------- /RdKafkaConnectionFactory.php: -------------------------------------------------------------------------------- 1 | [ // https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md 22 | * 'metadata.broker.list' => 'localhost:9092', 23 | * ], 24 | * 'topic' => [], 25 | * 'dr_msg_cb' => null, 26 | * 'error_cb' => null, 27 | * 'rebalance_cb' => null, 28 | * 'partitioner' => null, // https://arnaud-lb.github.io/php-rdkafka/phpdoc/rdkafka-topicconf.setpartitioner.html 29 | * 'log_level' => null, 30 | * 'commit_async' => false, 31 | * 'shutdown_timeout' => -1, // https://github.com/arnaud-lb/php-rdkafka#proper-shutdown 32 | * ] 33 | * 34 | * or 35 | * 36 | * kafka://host:port 37 | * 38 | * @param array|string $config 39 | */ 40 | public function __construct($config = 'kafka:') 41 | { 42 | if (version_compare(RdKafkaContext::getLibrdKafkaVersion(), '1.0.0', '<')) { 43 | throw new \RuntimeException('You must install librdkafka:1.0.0 or higher'); 44 | } 45 | 46 | if (empty($config) || 'kafka:' === $config) { 47 | $config = []; 48 | } elseif (is_string($config)) { 49 | $config = $this->parseDsn($config); 50 | } elseif (is_array($config)) { 51 | } else { 52 | throw new \LogicException('The config must be either an array of options, a DSN string or null'); 53 | } 54 | 55 | $this->config = array_replace_recursive($this->defaultConfig(), $config); 56 | } 57 | 58 | /** 59 | * @return RdKafkaContext 60 | */ 61 | public function createContext(): Context 62 | { 63 | return new RdKafkaContext($this->config); 64 | } 65 | 66 | private function parseDsn(string $dsn): array 67 | { 68 | $dsnConfig = parse_url($dsn); 69 | if (false === $dsnConfig) { 70 | throw new \LogicException(sprintf('Failed to parse DSN "%s"', $dsn)); 71 | } 72 | 73 | $dsnConfig = array_replace([ 74 | 'scheme' => null, 75 | 'host' => null, 76 | 'port' => null, 77 | 'user' => null, 78 | 'pass' => null, 79 | 'path' => null, 80 | 'query' => null, 81 | ], $dsnConfig); 82 | 83 | if ('kafka' !== $dsnConfig['scheme']) { 84 | throw new \LogicException(sprintf('The given DSN scheme "%s" is not supported. Could be "kafka" only.', $dsnConfig['scheme'])); 85 | } 86 | 87 | $config = []; 88 | if ($dsnConfig['query']) { 89 | parse_str($dsnConfig['query'], $config); 90 | } 91 | 92 | $broker = $dsnConfig['host']; 93 | if ($dsnConfig['port']) { 94 | $broker .= ':'.$dsnConfig['port']; 95 | } 96 | 97 | $config['global']['metadata.broker.list'] = $broker; 98 | 99 | return array_replace_recursive($this->defaultConfig(), $config); 100 | } 101 | 102 | private function defaultConfig(): array 103 | { 104 | return [ 105 | 'global' => [ 106 | 'group.id' => uniqid('', true), 107 | 'metadata.broker.list' => 'localhost:9092', 108 | ], 109 | ]; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /RdKafkaProducer.php: -------------------------------------------------------------------------------- 1 | producer = $producer; 27 | 28 | $this->setSerializer($serializer); 29 | } 30 | 31 | /** 32 | * @param RdKafkaTopic $destination 33 | * @param RdKafkaMessage $message 34 | */ 35 | public function send(Destination $destination, Message $message): void 36 | { 37 | InvalidDestinationException::assertDestinationInstanceOf($destination, RdKafkaTopic::class); 38 | InvalidMessageException::assertMessageInstanceOf($message, RdKafkaMessage::class); 39 | 40 | $partition = $message->getPartition() ?? $destination->getPartition() ?? \RD_KAFKA_PARTITION_UA; 41 | $payload = $this->serializer->toString($message); 42 | $key = $message->getKey() ?? $destination->getKey() ?? null; 43 | 44 | $topic = $this->producer->newTopic($destination->getTopicName(), $destination->getConf()); 45 | 46 | // Note: Topic::producev method exists in phprdkafka > 3.1.0 47 | // Headers in payload are maintained for backwards compatibility with apps that might run on lower phprdkafka version 48 | if (method_exists($topic, 'producev')) { 49 | // Phprdkafka <= 3.1.0 will fail calling `producev` on librdkafka >= 1.0.0 causing segfault 50 | // Since we are forcing to use at least librdkafka:1.0.0, no need to check the lib version anymore 51 | if (false !== phpversion('rdkafka') 52 | && version_compare(phpversion('rdkafka'), '3.1.0', '<=')) { 53 | trigger_error( 54 | 'Phprdkafka <= 3.1.0 is incompatible with librdkafka 1.0.0 when calling `producev`. '. 55 | 'Falling back to `produce` (without message headers) instead.', 56 | \E_USER_WARNING 57 | ); 58 | } else { 59 | $topic->producev($partition, 0 /* must be 0 */ , $payload, $key, $message->getHeaders()); 60 | $this->producer->poll(0); 61 | 62 | return; 63 | } 64 | } 65 | 66 | $topic->produce($partition, 0 /* must be 0 */ , $payload, $key); 67 | $this->producer->poll(0); 68 | } 69 | 70 | /** 71 | * @return RdKafkaProducer 72 | */ 73 | public function setDeliveryDelay(?int $deliveryDelay = null): Producer 74 | { 75 | if (null === $deliveryDelay) { 76 | return $this; 77 | } 78 | 79 | throw new \LogicException('Not implemented'); 80 | } 81 | 82 | public function getDeliveryDelay(): ?int 83 | { 84 | return null; 85 | } 86 | 87 | /** 88 | * @return RdKafkaProducer 89 | */ 90 | public function setPriority(?int $priority = null): Producer 91 | { 92 | if (null === $priority) { 93 | return $this; 94 | } 95 | 96 | throw PriorityNotSupportedException::providerDoestNotSupportIt(); 97 | } 98 | 99 | public function getPriority(): ?int 100 | { 101 | return null; 102 | } 103 | 104 | public function setTimeToLive(?int $timeToLive = null): Producer 105 | { 106 | if (null === $timeToLive) { 107 | return $this; 108 | } 109 | 110 | throw new \LogicException('Not implemented'); 111 | } 112 | 113 | public function getTimeToLive(): ?int 114 | { 115 | return null; 116 | } 117 | 118 | public function flush(int $timeout): ?int 119 | { 120 | // Flush method is exposed in phprdkafka 4.0 121 | if (method_exists($this->producer, 'flush')) { 122 | return $this->producer->flush($timeout); 123 | } 124 | 125 | return null; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /RdKafkaMessage.php: -------------------------------------------------------------------------------- 1 | body = $body; 50 | $this->properties = $properties; 51 | $this->headers = $headers; 52 | $this->redelivered = false; 53 | } 54 | 55 | public function setBody(string $body): void 56 | { 57 | $this->body = $body; 58 | } 59 | 60 | public function getBody(): string 61 | { 62 | return $this->body; 63 | } 64 | 65 | public function setProperties(array $properties): void 66 | { 67 | $this->properties = $properties; 68 | } 69 | 70 | public function getProperties(): array 71 | { 72 | return $this->properties; 73 | } 74 | 75 | public function setProperty(string $name, $value): void 76 | { 77 | $this->properties[$name] = $value; 78 | } 79 | 80 | public function getProperty(string $name, $default = null) 81 | { 82 | return array_key_exists($name, $this->properties) ? $this->properties[$name] : $default; 83 | } 84 | 85 | public function setHeaders(array $headers): void 86 | { 87 | $this->headers = $headers; 88 | } 89 | 90 | public function getHeaders(): array 91 | { 92 | return $this->headers; 93 | } 94 | 95 | public function setHeader(string $name, $value): void 96 | { 97 | $this->headers[$name] = $value; 98 | } 99 | 100 | public function getHeader(string $name, $default = null) 101 | { 102 | return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; 103 | } 104 | 105 | public function isRedelivered(): bool 106 | { 107 | return $this->redelivered; 108 | } 109 | 110 | public function setRedelivered(bool $redelivered): void 111 | { 112 | $this->redelivered = $redelivered; 113 | } 114 | 115 | public function setCorrelationId(?string $correlationId = null): void 116 | { 117 | $this->setHeader('correlation_id', (string) $correlationId); 118 | } 119 | 120 | public function getCorrelationId(): ?string 121 | { 122 | return $this->getHeader('correlation_id'); 123 | } 124 | 125 | public function setMessageId(?string $messageId = null): void 126 | { 127 | $this->setHeader('message_id', (string) $messageId); 128 | } 129 | 130 | public function getMessageId(): ?string 131 | { 132 | return $this->getHeader('message_id'); 133 | } 134 | 135 | public function getTimestamp(): ?int 136 | { 137 | $value = $this->getHeader('timestamp'); 138 | 139 | return null === $value ? null : (int) $value; 140 | } 141 | 142 | public function setTimestamp(?int $timestamp = null): void 143 | { 144 | $this->setHeader('timestamp', $timestamp); 145 | } 146 | 147 | public function setReplyTo(?string $replyTo = null): void 148 | { 149 | $this->setHeader('reply_to', $replyTo); 150 | } 151 | 152 | public function getReplyTo(): ?string 153 | { 154 | return $this->getHeader('reply_to'); 155 | } 156 | 157 | public function getPartition(): ?int 158 | { 159 | return $this->partition; 160 | } 161 | 162 | public function setPartition(?int $partition = null): void 163 | { 164 | $this->partition = $partition; 165 | } 166 | 167 | public function getKey(): ?string 168 | { 169 | return $this->key; 170 | } 171 | 172 | public function setKey(?string $key = null): void 173 | { 174 | $this->key = $key; 175 | } 176 | 177 | public function getKafkaMessage(): ?VendorMessage 178 | { 179 | return $this->kafkaMessage; 180 | } 181 | 182 | public function setKafkaMessage(?VendorMessage $message = null): void 183 | { 184 | $this->kafkaMessage = $message; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /RdKafkaConsumer.php: -------------------------------------------------------------------------------- 1 | consumer = $consumer; 51 | $this->context = $context; 52 | $this->topic = $topic; 53 | $this->subscribed = false; 54 | $this->commitAsync = true; 55 | 56 | $this->setSerializer($serializer); 57 | } 58 | 59 | public function isCommitAsync(): bool 60 | { 61 | return $this->commitAsync; 62 | } 63 | 64 | public function setCommitAsync(bool $async): void 65 | { 66 | $this->commitAsync = $async; 67 | } 68 | 69 | public function getOffset(): ?int 70 | { 71 | return $this->offset; 72 | } 73 | 74 | public function setOffset(?int $offset = null): void 75 | { 76 | if ($this->subscribed) { 77 | throw new \LogicException('The consumer has already subscribed.'); 78 | } 79 | 80 | $this->offset = $offset; 81 | } 82 | 83 | /** 84 | * @return RdKafkaTopic 85 | */ 86 | public function getQueue(): Queue 87 | { 88 | return $this->topic; 89 | } 90 | 91 | /** 92 | * @return RdKafkaMessage 93 | */ 94 | public function receive(int $timeout = 0): ?Message 95 | { 96 | if (false === $this->subscribed) { 97 | if (null === $this->offset) { 98 | $this->consumer->subscribe([$this->getQueue()->getQueueName()]); 99 | } else { 100 | $this->consumer->assign([new TopicPartition( 101 | $this->getQueue()->getQueueName(), 102 | $this->getQueue()->getPartition(), 103 | $this->offset 104 | )]); 105 | } 106 | 107 | $this->subscribed = true; 108 | } 109 | 110 | if ($timeout > 0) { 111 | return $this->doReceive($timeout); 112 | } 113 | 114 | while (true) { 115 | if ($message = $this->doReceive(500)) { 116 | return $message; 117 | } 118 | } 119 | 120 | return null; 121 | } 122 | 123 | /** 124 | * @return RdKafkaMessage 125 | */ 126 | public function receiveNoWait(): ?Message 127 | { 128 | throw new \LogicException('Not implemented'); 129 | } 130 | 131 | /** 132 | * @param RdKafkaMessage $message 133 | */ 134 | public function acknowledge(Message $message): void 135 | { 136 | InvalidMessageException::assertMessageInstanceOf($message, RdKafkaMessage::class); 137 | 138 | if (false == $message->getKafkaMessage()) { 139 | throw new \LogicException('The message could not be acknowledged because it does not have kafka message set.'); 140 | } 141 | 142 | if ($this->isCommitAsync()) { 143 | $this->consumer->commitAsync($message->getKafkaMessage()); 144 | } else { 145 | $this->consumer->commit($message->getKafkaMessage()); 146 | } 147 | } 148 | 149 | /** 150 | * @param RdKafkaMessage $message 151 | */ 152 | public function reject(Message $message, bool $requeue = false): void 153 | { 154 | $this->acknowledge($message); 155 | 156 | if ($requeue) { 157 | $this->context->createProducer()->send($this->topic, $message); 158 | } 159 | } 160 | 161 | private function doReceive(int $timeout): ?RdKafkaMessage 162 | { 163 | $kafkaMessage = $this->consumer->consume($timeout); 164 | 165 | if (null === $kafkaMessage) { 166 | return null; 167 | } 168 | 169 | switch ($kafkaMessage->err) { 170 | case \RD_KAFKA_RESP_ERR__PARTITION_EOF: 171 | case \RD_KAFKA_RESP_ERR__TIMED_OUT: 172 | case \RD_KAFKA_RESP_ERR__TRANSPORT: 173 | return null; 174 | case \RD_KAFKA_RESP_ERR_NO_ERROR: 175 | $message = $this->serializer->toMessage($kafkaMessage->payload); 176 | $message->setKey($kafkaMessage->key); 177 | $message->setPartition($kafkaMessage->partition); 178 | $message->setKafkaMessage($kafkaMessage); 179 | 180 | // Merge headers passed from Kafka with possible earlier serialized payload headers. Prefer Kafka's. 181 | // Note: Requires phprdkafka >= 3.1.0 182 | if (isset($kafkaMessage->headers)) { 183 | $message->setHeaders(array_merge($message->getHeaders(), $kafkaMessage->headers)); 184 | } 185 | 186 | return $message; 187 | default: 188 | throw new \LogicException($kafkaMessage->errstr(), $kafkaMessage->err); 189 | break; 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /RdKafkaContext.php: -------------------------------------------------------------------------------- 1 | config = $config; 55 | $this->kafkaConsumers = []; 56 | $this->rdKafkaConsumers = []; 57 | 58 | $this->setSerializer(new JsonSerializer()); 59 | } 60 | 61 | /** 62 | * @return RdKafkaMessage 63 | */ 64 | public function createMessage(string $body = '', array $properties = [], array $headers = []): Message 65 | { 66 | return new RdKafkaMessage($body, $properties, $headers); 67 | } 68 | 69 | /** 70 | * @return RdKafkaTopic 71 | */ 72 | public function createTopic(string $topicName): Topic 73 | { 74 | return new RdKafkaTopic($topicName); 75 | } 76 | 77 | /** 78 | * @return RdKafkaTopic 79 | */ 80 | public function createQueue(string $queueName): Queue 81 | { 82 | return new RdKafkaTopic($queueName); 83 | } 84 | 85 | public function createTemporaryQueue(): Queue 86 | { 87 | throw TemporaryQueueNotSupportedException::providerDoestNotSupportIt(); 88 | } 89 | 90 | /** 91 | * @return RdKafkaProducer 92 | */ 93 | public function createProducer(): Producer 94 | { 95 | if (!isset($this->producer)) { 96 | $producer = new VendorProducer($this->getConf()); 97 | 98 | if (isset($this->config['log_level'])) { 99 | $producer->setLogLevel($this->config['log_level']); 100 | } 101 | 102 | $this->producer = new RdKafkaProducer($producer, $this->getSerializer()); 103 | 104 | // Once created RdKafkaProducer can store messages internally that need to be delivered before PHP shuts 105 | // down. Otherwise, we are bound to lose messages in transit. 106 | // Note that it is generally preferable to call "close" method explicitly before shutdown starts, since 107 | // otherwise we might not have access to some objects, like database connections. 108 | register_shutdown_function([$this->producer, 'flush'], $this->config['shutdown_timeout'] ?? -1); 109 | } 110 | 111 | return $this->producer; 112 | } 113 | 114 | /** 115 | * @param RdKafkaTopic $destination 116 | * 117 | * @return RdKafkaConsumer 118 | */ 119 | public function createConsumer(Destination $destination): Consumer 120 | { 121 | InvalidDestinationException::assertDestinationInstanceOf($destination, RdKafkaTopic::class); 122 | 123 | $queueName = $destination->getQueueName(); 124 | 125 | if (!isset($this->rdKafkaConsumers[$queueName])) { 126 | $this->kafkaConsumers[] = $kafkaConsumer = new KafkaConsumer($this->getConf()); 127 | 128 | $consumer = new RdKafkaConsumer( 129 | $kafkaConsumer, 130 | $this, 131 | $destination, 132 | $this->getSerializer() 133 | ); 134 | 135 | if (isset($this->config['commit_async'])) { 136 | $consumer->setCommitAsync($this->config['commit_async']); 137 | } 138 | 139 | $this->rdKafkaConsumers[$queueName] = $consumer; 140 | } 141 | 142 | return $this->rdKafkaConsumers[$queueName]; 143 | } 144 | 145 | public function close(): void 146 | { 147 | $kafkaConsumers = $this->kafkaConsumers; 148 | $this->kafkaConsumers = []; 149 | $this->rdKafkaConsumers = []; 150 | 151 | foreach ($kafkaConsumers as $kafkaConsumer) { 152 | $kafkaConsumer->unsubscribe(); 153 | } 154 | 155 | // Compatibility with phprdkafka 4.0. 156 | if (isset($this->producer)) { 157 | $this->producer->flush($this->config['shutdown_timeout'] ?? -1); 158 | } 159 | } 160 | 161 | public function createSubscriptionConsumer(): SubscriptionConsumer 162 | { 163 | throw SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt(); 164 | } 165 | 166 | public function purgeQueue(Queue $queue): void 167 | { 168 | throw PurgeQueueNotSupportedException::providerDoestNotSupportIt(); 169 | } 170 | 171 | public static function getLibrdKafkaVersion(): string 172 | { 173 | if (!defined('RD_KAFKA_VERSION')) { 174 | throw new \RuntimeException('RD_KAFKA_VERSION constant is not defined. Phprdkafka is probably not installed'); 175 | } 176 | $major = (\RD_KAFKA_VERSION & 0xFF000000) >> 24; 177 | $minor = (\RD_KAFKA_VERSION & 0x00FF0000) >> 16; 178 | $patch = (\RD_KAFKA_VERSION & 0x0000FF00) >> 8; 179 | 180 | return "$major.$minor.$patch"; 181 | } 182 | 183 | private function getConf(): Conf 184 | { 185 | if (null === $this->conf) { 186 | $this->conf = new Conf(); 187 | 188 | if (isset($this->config['topic']) && is_array($this->config['topic'])) { 189 | foreach ($this->config['topic'] as $key => $value) { 190 | $this->conf->set($key, $value); 191 | } 192 | } 193 | 194 | if (isset($this->config['partitioner'])) { 195 | $this->conf->set('partitioner', $this->config['partitioner']); 196 | } 197 | 198 | if (isset($this->config['global']) && is_array($this->config['global'])) { 199 | foreach ($this->config['global'] as $key => $value) { 200 | $this->conf->set($key, $value); 201 | } 202 | } 203 | 204 | if (isset($this->config['dr_msg_cb'])) { 205 | $this->conf->setDrMsgCb($this->config['dr_msg_cb']); 206 | } 207 | 208 | if (isset($this->config['error_cb'])) { 209 | $this->conf->setErrorCb($this->config['error_cb']); 210 | } 211 | 212 | if (isset($this->config['rebalance_cb'])) { 213 | $this->conf->setRebalanceCb($this->config['rebalance_cb']); 214 | } 215 | 216 | if (isset($this->config['stats_cb'])) { 217 | $this->conf->setStatsCb($this->config['stats_cb']); 218 | } 219 | } 220 | 221 | return $this->conf; 222 | } 223 | } 224 | --------------------------------------------------------------------------------