├── 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 | [![Latest Stable Version](https://poser.pugx.org/spiral/roadrunner-broadcast/version)](https://packagist.org/packages/spiral/roadrunner-broadcast) 4 | [![Build Status](https://github.com/spiral/roadrunner-broadcast/workflows/build/badge.svg)](https://github.com/spiral/roadrunner-broadcast/actions) 5 | [![Codecov](https://codecov.io/gh/spiral/roadrunner-broadcast/branch/master/graph/badge.svg)](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 | try Spiral Framework 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 | --------------------------------------------------------------------------------