├── LICENSE
├── README.md
├── composer.json
├── generated
├── GPBMetadata
│ └── Broadcast.php
└── Spiral
│ └── RoadRunner
│ └── Broadcast
│ └── DTO
│ └── V1
│ ├── Message.php
│ ├── Request.php
│ └── Response.php
├── phpcs.xml
├── psalm.xml
├── resources
├── .phpstorm.meta.php
└── proto
│ └── v1
│ └── broadcast.proto
└── src
├── Broadcast.php
├── BroadcastInterface.php
├── Exception
├── BroadcastException.php
└── InvalidArgumentException.php
├── Topic
├── Group.php
├── SingleTopic.php
└── Topic.php
└── TopicInterface.php
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Spiral Scout
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RoadRunner Broadcast Plugin Bridge
2 |
3 | [](https://packagist.org/packages/spiral/roadrunner-broadcast)
4 | [](https://github.com/spiral/roadrunner-broadcast/actions)
5 | [](https://codecov.io/gh/spiral/roadrunner-broadcast/)
6 |
7 | This repository contains the codebase bridge for broadcast RoadRunner plugin.
8 |
9 | ## Installation
10 |
11 | To install application server and broadcast codebase
12 |
13 | ```bash
14 | $ composer require spiral/roadrunner-broadcast
15 | ```
16 |
17 | You can use the convenient installer to download the latest available compatible
18 | version of RoadRunner assembly:
19 |
20 | ```bash
21 | $ composer require spiral/roadrunner-cli --dev
22 | $ vendor/bin/rr get
23 | ```
24 |
25 | ## Usage
26 |
27 | For example, such a configuration would be quite feasible to run:
28 |
29 | ```yaml
30 | rpc:
31 | listen: tcp://127.0.0.1:6001
32 |
33 | server:
34 | # Don't forget to create a "worker.php" file
35 | command: "php worker.php"
36 | relay: "pipes"
37 |
38 | http:
39 | address: 127.0.0.1:80
40 | # Indicate that HTTP support ws protocol
41 | middleware: [ "websockets" ]
42 |
43 | websockets:
44 | broker: default
45 | path: "/ws"
46 |
47 | broadcast:
48 | default:
49 | driver: memory
50 | config: {}
51 | ```
52 |
53 | > Read more about all available brokers on the
54 | > [documentation](https://roadrunner.dev/docs) page.
55 |
56 | After configuring and starting the RoadRunner server, the corresponding API
57 | will become available to you.
58 |
59 | ```php
60 | publish('channel-1', 'message for channel #1');
73 | ```
74 |
75 | ### Select Specific Topic
76 |
77 | Alternatively, you can also use a specific topic (or set of topics) as a
78 | separate entity and post directly to it.
79 |
80 | ```php
81 | // Now we can select the topic we need to work only with it
82 | $topic = $broadcast->join(['channel-1', 'channel-2']);
83 |
84 | // And send messages there
85 | $topic->publish('message');
86 | $topic->publish(['another message', 'third message']);
87 | ```
88 |
89 | > Read more about all the possibilities in the
90 | > [documentation](https://roadrunner.dev/docs) page.
91 |
92 | ## Client
93 |
94 | In addition to the server (PHP) part, the client part is also present in most
95 | projects. In most cases, this is a browser in which the connection to the server
96 | is made using the [WebSocket](https://en.wikipedia.org/wiki/WebSocket) protocol.
97 |
98 | ```js
99 | const ws = new WebSocket('ws://127.0.0.1/broadcast');
100 |
101 | ws.onopen = e => {
102 | const message = {
103 | command: 'join',
104 | topics: ['channel-1', 'channel-2']
105 | };
106 |
107 | ws.send(JSON.stringify(message));
108 | };
109 |
110 | ws.onmessage = e => {
111 | const message = JSON.parse(e.data);
112 |
113 | console.log(`${message.topic}: ${message.payload}`);
114 | }
115 | ```
116 |
117 |
118 |
119 |
120 |
121 | ## Examples
122 |
123 | Examples are available in the corresponding directory [./example](./example).
124 |
125 | ## License
126 |
127 | The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information.
128 | Maintained by [Spiral Scout](https://spiralscout.com).
129 |
130 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spiral/roadrunner-broadcast",
3 | "type": "library",
4 | "description": "RoadRunner broadcast plugin bridge",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Wolfy-J",
9 | "email": "wolfy.jd@gmail.com"
10 | },
11 | {
12 | "name": "Kirill Nesmeyanov (SerafimArts)",
13 | "email": "kirill.nesmeyanov@spiralscout.com"
14 | },
15 | {
16 | "name": "RoadRunner Community",
17 | "homepage": "https://github.com/spiral/roadrunner/graphs/contributors"
18 | }
19 | ],
20 | "require": {
21 | "php": ">=7.4",
22 | "ext-json": "*",
23 | "symfony/polyfill-php80": "^1.23",
24 | "spiral/roadrunner": "^2.0",
25 | "spiral/goridge": "^3.1",
26 | "google/protobuf": "^3.7"
27 | },
28 | "autoload": {
29 | "psr-4": {
30 | "Spiral\\RoadRunner\\Broadcast\\": ["src", "generated/Spiral/RoadRunner/Broadcast"],
31 | "GPBMetadata\\": "generated/GPBMetadata"
32 | }
33 | },
34 | "require-dev": {
35 | "phpunit/phpunit": "^8.0",
36 | "spiral/code-style": "^1.0",
37 | "vimeo/psalm": ">=4.4",
38 | "symfony/var-dumper": "^5.1",
39 | "roave/security-advisories": "dev-master"
40 | },
41 | "autoload-dev": {
42 | "psr-4": {
43 | "Spiral\\RoadRunner\\Broadcast\\Tests\\": "tests"
44 | }
45 | },
46 | "scripts": {
47 | "build": [
48 | "protoc --proto_path=resources/proto/v1 --php_out=generated resources/proto/v1/broadcast.proto"
49 | ],
50 | "tests": "phpunit",
51 | "cs-fix": "spiral-cs fix src tests",
52 | "analyze": [
53 | "psalm --no-cache",
54 | "spiral-cs check src tests"
55 | ]
56 | },
57 | "extra": {
58 | "branch-alias": {
59 | "dev-master": "2.0.x-dev"
60 | }
61 | },
62 | "config": {
63 | "sort-packages": true
64 | },
65 | "minimum-stability": "dev",
66 | "prefer-stable": true
67 | }
68 |
--------------------------------------------------------------------------------
/generated/GPBMetadata/Broadcast.php:
--------------------------------------------------------------------------------
1 | internalAddGeneratedFile(hex2bin(
18 | "0ad7010a0f62726f6164636173742e70726f746f120c62726f6164636173742e7631223b0a074d657373616765120f0a07636f6d6d616e64180120012809120e0a06746f70696373180220032809120f0a077061796c6f616418032001280c22320a075265717565737412270a086d6573736167657318012003280b32152e62726f6164636173742e76312e4d65737361676522160a08526573706f6e7365120a0a026f6b1801200128084225ca022253706972616c5c526f616452756e6e65725c42726f6164636173745c44544f5c5631620670726f746f33"
19 | ), true);
20 |
21 | static::$is_initialized = true;
22 | }
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/generated/Spiral/RoadRunner/Broadcast/DTO/V1/Message.php:
--------------------------------------------------------------------------------
1 | broadcast.v1.Message
13 | */
14 | class Message extends \Google\Protobuf\Internal\Message
15 | {
16 | /**
17 | * Generated from protobuf field string command = 1;
18 | */
19 | protected $command = '';
20 | /**
21 | * Generated from protobuf field repeated string topics = 2;
22 | */
23 | private $topics;
24 | /**
25 | * Generated from protobuf field bytes payload = 3;
26 | */
27 | protected $payload = '';
28 |
29 | /**
30 | * Constructor.
31 | *
32 | * @param array $data {
33 | * Optional. Data for populating the Message object.
34 | *
35 | * @type string $command
36 | * @type string[]|\Google\Protobuf\Internal\RepeatedField $topics
37 | * @type string $payload
38 | * }
39 | */
40 | public function __construct($data = NULL) {
41 | \GPBMetadata\Broadcast::initOnce();
42 | parent::__construct($data);
43 | }
44 |
45 | /**
46 | * Generated from protobuf field string command = 1;
47 | * @return string
48 | */
49 | public function getCommand()
50 | {
51 | return $this->command;
52 | }
53 |
54 | /**
55 | * Generated from protobuf field string command = 1;
56 | * @param string $var
57 | * @return $this
58 | */
59 | public function setCommand($var)
60 | {
61 | GPBUtil::checkString($var, True);
62 | $this->command = $var;
63 |
64 | return $this;
65 | }
66 |
67 | /**
68 | * Generated from protobuf field repeated string topics = 2;
69 | * @return \Google\Protobuf\Internal\RepeatedField
70 | */
71 | public function getTopics()
72 | {
73 | return $this->topics;
74 | }
75 |
76 | /**
77 | * Generated from protobuf field repeated string topics = 2;
78 | * @param string[]|\Google\Protobuf\Internal\RepeatedField $var
79 | * @return $this
80 | */
81 | public function setTopics($var)
82 | {
83 | $arr = GPBUtil::checkRepeatedField($var, \Google\Protobuf\Internal\GPBType::STRING);
84 | $this->topics = $arr;
85 |
86 | return $this;
87 | }
88 |
89 | /**
90 | * Generated from protobuf field bytes payload = 3;
91 | * @return string
92 | */
93 | public function getPayload()
94 | {
95 | return $this->payload;
96 | }
97 |
98 | /**
99 | * Generated from protobuf field bytes payload = 3;
100 | * @param string $var
101 | * @return $this
102 | */
103 | public function setPayload($var)
104 | {
105 | GPBUtil::checkString($var, False);
106 | $this->payload = $var;
107 |
108 | return $this;
109 | }
110 |
111 | }
112 |
113 |
--------------------------------------------------------------------------------
/generated/Spiral/RoadRunner/Broadcast/DTO/V1/Request.php:
--------------------------------------------------------------------------------
1 | broadcast.v1.Request
15 | */
16 | class Request extends \Google\Protobuf\Internal\Message
17 | {
18 | /**
19 | * Generated from protobuf field repeated .broadcast.v1.Message messages = 1;
20 | */
21 | private $messages;
22 |
23 | /**
24 | * Constructor.
25 | *
26 | * @param array $data {
27 | * Optional. Data for populating the Message object.
28 | *
29 | * @type \Spiral\RoadRunner\Broadcast\DTO\V1\Message[]|\Google\Protobuf\Internal\RepeatedField $messages
30 | * }
31 | */
32 | public function __construct($data = NULL) {
33 | \GPBMetadata\Broadcast::initOnce();
34 | parent::__construct($data);
35 | }
36 |
37 | /**
38 | * Generated from protobuf field repeated .broadcast.v1.Message messages = 1;
39 | * @return \Google\Protobuf\Internal\RepeatedField
40 | */
41 | public function getMessages()
42 | {
43 | return $this->messages;
44 | }
45 |
46 | /**
47 | * Generated from protobuf field repeated .broadcast.v1.Message messages = 1;
48 | * @param \Spiral\RoadRunner\Broadcast\DTO\V1\Message[]|\Google\Protobuf\Internal\RepeatedField $var
49 | * @return $this
50 | */
51 | public function setMessages($var)
52 | {
53 | $arr = GPBUtil::checkRepeatedField($var, \Google\Protobuf\Internal\GPBType::MESSAGE, \Spiral\RoadRunner\Broadcast\DTO\V1\Message::class);
54 | $this->messages = $arr;
55 |
56 | return $this;
57 | }
58 |
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/generated/Spiral/RoadRunner/Broadcast/DTO/V1/Response.php:
--------------------------------------------------------------------------------
1 | broadcast.v1.Response
15 | */
16 | class Response extends \Google\Protobuf\Internal\Message
17 | {
18 | /**
19 | * Generated from protobuf field bool ok = 1;
20 | */
21 | protected $ok = false;
22 |
23 | /**
24 | * Constructor.
25 | *
26 | * @param array $data {
27 | * Optional. Data for populating the Message object.
28 | *
29 | * @type bool $ok
30 | * }
31 | */
32 | public function __construct($data = NULL) {
33 | \GPBMetadata\Broadcast::initOnce();
34 | parent::__construct($data);
35 | }
36 |
37 | /**
38 | * Generated from protobuf field bool ok = 1;
39 | * @return bool
40 | */
41 | public function getOk()
42 | {
43 | return $this->ok;
44 | }
45 |
46 | /**
47 | * Generated from protobuf field bool ok = 1;
48 | * @param bool $var
49 | * @return $this
50 | */
51 | public function setOk($var)
52 | {
53 | GPBUtil::checkBool($var);
54 | $this->ok = $var;
55 |
56 | return $this;
57 | }
58 |
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | ./src
45 | ./tests
46 |
47 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/resources/.phpstorm.meta.php:
--------------------------------------------------------------------------------
1 | rpc = $rpc->withCodec(new ProtobufCodec());
38 | }
39 |
40 | /**
41 | * @deprecated Information about RoadRunner plugins is not available since RoadRunner version 2.2
42 | */
43 | public function isAvailable(): bool
44 | {
45 | throw new \RuntimeException(\sprintf('%s::isAvailable method is deprecated.', self::class));
46 | }
47 |
48 | /**
49 | * @param non-empty-array $topics
50 | * @param string $message
51 | * @return Message
52 | */
53 | private function createMessage(string $message, array $topics): Message
54 | {
55 | return new Message([
56 | 'topics' => $topics,
57 | 'payload' => $message,
58 | ]);
59 | }
60 |
61 | /**
62 | * @param non-empty-list $messages
63 | * @return void
64 | * @throws BroadcastException
65 | */
66 | private function request(iterable $messages): void
67 | {
68 | $request = new Request(['messages' => $this->toArray($messages)]);
69 |
70 | /** @var Response $response */
71 | $response = $this->rpc->call('broadcast.Publish', $request, Response::class);
72 |
73 | if (! $response->getOk()) {
74 | throw new BroadcastException('An error occurred while publishing message');
75 | }
76 | }
77 |
78 | /**
79 | * @template T of mixed
80 | * @param iterable|T $entries
81 | * @return array
82 | */
83 | private function toArray($entries): array
84 | {
85 | switch (true) {
86 | case \is_array($entries):
87 | return $entries;
88 |
89 | case $entries instanceof \Traversable:
90 | return \iterator_to_array($entries, false);
91 |
92 | default:
93 | return [$entries];
94 | }
95 | }
96 |
97 | /**
98 | * {@inheritDoc}
99 | */
100 | public function publish($topics, $messages): void
101 | {
102 | assert(
103 | \is_string($topics) || \is_iterable($topics),
104 | '$topics argument must be a type of iterable|string'
105 | );
106 | assert(
107 | \is_string($messages) || \is_iterable($messages),
108 | '$messages argument must be a type of iterable|string'
109 | );
110 |
111 | /** @var array $topics */
112 | $topics = $this->toArray($topics);
113 |
114 | if ($topics === []) {
115 | throw new InvalidArgumentException('Unable to publish message to 0 topics');
116 | }
117 |
118 | $request = [];
119 |
120 | /** @var string $message */
121 | foreach ($this->toArray($messages) as $message) {
122 | assert(\is_string($message), 'Message argument must be a type of string');
123 |
124 | $request[] = $this->createMessage($message, $topics);
125 | }
126 |
127 | if ($request === []) {
128 | throw new InvalidArgumentException('Unable to publish 0 messages');
129 | }
130 |
131 | $this->request($request);
132 | }
133 |
134 | /**
135 | * {@inheritDoc}
136 | */
137 | public function join($topics): TopicInterface
138 | {
139 | assert(
140 | \is_string($topics) || \is_iterable($topics),
141 | '$topics argument must be type of iterable|string'
142 | );
143 |
144 | /** @var array $topics */
145 | $topics = $this->toArray($topics);
146 |
147 | switch (\count($topics)) {
148 | case 0:
149 | throw new InvalidArgumentException('Unable to connect to 0 topics');
150 |
151 | case 1:
152 | return new SingleTopic($this, \reset($topics));
153 |
154 | default:
155 | return new Group($this, $topics);
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/BroadcastInterface.php:
--------------------------------------------------------------------------------
1 | | string
19 | * @psalm-type MessagesList = non-empty-list | string
20 | */
21 | interface BroadcastInterface
22 | {
23 | /**
24 | * Method to send messages to the required topic (channel).
25 | *
26 | * $broadcast->publish('topic', 'message');
27 | * $broadcast->publish('topic', ['message 1', 'message 2']);
28 | *
29 | * $broadcast->publish(['topic 1', 'topic 2'], 'message');
30 | * $broadcast->publish(['topic 1', 'topic 2'], ['message 1', 'message 2']);
31 | *
32 | *
33 | * Note: In future major releases, the signature of this method will be
34 | * changed to include follow type-hints.
35 | *
36 | *
37 | * public function publish(iterable|string $topics, iterable|string $messages): void;
38 | *
39 | *
40 | * @param TopicsList $topics
41 | * @param MessagesList $messages
42 | * @throws BroadcastException
43 | */
44 | public function publish($topics, $messages): void;
45 |
46 | /**
47 | * Join to concrete topic
48 | *
49 | * @param TopicsList $topics
50 | * @return TopicInterface
51 | * @throws InvalidArgumentException
52 | */
53 | public function join($topics): TopicInterface;
54 | }
55 |
--------------------------------------------------------------------------------
/src/Exception/BroadcastException.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | public function getNames(): array
20 | {
21 | return $this->topics;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Topic/SingleTopic.php:
--------------------------------------------------------------------------------
1 | topics[0];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Topic/Topic.php:
--------------------------------------------------------------------------------
1 |
28 | */
29 | protected array $topics = [];
30 |
31 | /**
32 | * @param BroadcastInterface $broadcast
33 | * @param iterable $topics
34 | * @throws InvalidArgumentException
35 | */
36 | public function __construct(BroadcastInterface $broadcast, iterable $topics)
37 | {
38 | $this->broadcast = $broadcast;
39 |
40 | foreach ($topics as $topic) {
41 | $this->topics[] = $topic;
42 | }
43 |
44 | /** @psalm-suppress TypeDoesNotContainType */
45 | if ($this->topics === []) {
46 | throw new InvalidArgumentException('Unable to create instance for 0 topic names');
47 | }
48 | }
49 |
50 | /**
51 | * {@inheritDoc}
52 | */
53 | public function publish($messages): void
54 | {
55 | $this->broadcast->publish($this->topics, $messages);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/TopicInterface.php:
--------------------------------------------------------------------------------
1 |
25 | * $topic->publish('message');
26 | * $topic->publish(['message 1', 'message 2']);
27 | *
28 | *
29 | * Note: In future major releases, the signature of this method will be
30 | * changed to include follow type-hints.
31 | *
32 | *
33 | * public function publish(iterable|string $messages): void;
34 | *
35 | *
36 | * @param MessagesList $messages
37 | * @throws BroadcastException
38 | */
39 | public function publish($messages): void;
40 | }
41 |
--------------------------------------------------------------------------------