├── 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 | [](https://gitter.im/php-enqueue/Lobby)
13 | [](https://github.com/php-enqueue/rdkafka/actions?query=workflow%3ACI)
14 | [](https://packagist.org/packages/enqueue/rdkafka)
15 | [](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 |
--------------------------------------------------------------------------------