├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── examples ├── mqtt.php ├── publish.php └── subscribe.php └── src ├── Client.php ├── ConnectionException.php ├── ConnectionOptions.php ├── Packets ├── Connect.php ├── ConnectionAck.php ├── ControlPacket.php ├── ControlPacketType.php ├── Disconnect.php ├── PingRequest.php ├── PingResponse.php ├── Publish.php ├── PublishAck.php ├── PublishComplete.php ├── PublishReceived.php ├── PublishRelease.php ├── QoS │ └── Levels.php ├── Subscribe.php ├── SubscribeAck.php ├── Unsubscribe.php └── UnsubscribeAck.php ├── Protocols ├── Version4.php ├── VersionInterface.php └── VersionViolation.php └── Utils └── PacketFactory.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .idea -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactPHP MQTT Client 2 | 3 | **react-mqtt** is an MQTT client library for PHP. 4 | 5 | Its based on the reactPHP socket-client and added the MQTT protocol 6 | specific functions. 7 | Also based on https://github.com/oliverlorenz/phpMqttClient 8 | 9 | ## Goal 10 | 11 | Goal of this project is easy to use MQTT client for PHP in a modern architecture without using any php modules. 12 | Currently, only protocol version 4 (mqtt 3.1.1) is implemented. 13 | * Protocol specifications: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/csprd02/mqtt-v3.1.1-csprd02.html 14 | 15 | ## Example library initial 16 | ```php 17 | // mqtt.php 18 | 19 | use Morbo\React\Mqtt\Client; 20 | use Morbo\React\Mqtt\ConnectionOptions; 21 | use Morbo\React\Mqtt\Protocols\Version4; 22 | 23 | require_once __DIR__ . '/vendor/autoload.php'; 24 | 25 | // Creating Event Loop 26 | $loop = React\EventLoop\Factory::create(); 27 | 28 | // Connection configuration 29 | $config = [ 30 | 'host' => 'localhost', 31 | 'port' => 1883, 32 | // 'options' => new ConnectionOptions([ 33 | // 'username' => 'auth_user', 34 | // 'password' => 'auth_password', 35 | // 'clientId' => 'react_client', // default is 'react-'.uniqid() 36 | // 'cleanSession' => true, // default is true 37 | // 'cleanSession' => true, // default is true 38 | // . 'willTopic' => '', 39 | // . 'willMessage' => '', 40 | // . 'willQos' => '', 41 | // . 'willRetain' => '', 42 | // . 'keepAlive' => 60, // default is 60 43 | // ]) 44 | ]; 45 | 46 | $mqtt = new Client($loop, new Version4()); 47 | 48 | ``` 49 | 50 | 51 | ## Example publish 52 | 53 | ```php 54 | use React\Socket\ConnectionInterface; 55 | 56 | require 'mqtt.php'; 57 | 58 | $connection = $mqtt->connect($config['host'], $config['port'], $config['options']); 59 | 60 | $connection->then(function (ConnectionInterface $stream) use ($mqtt, $loop) { 61 | /** 62 | * Stop loop, when client disconnected from mqtt server 63 | */ 64 | $stream->on('end', function () use ($loop) { 65 | $loop->stop(); 66 | }); 67 | 68 | $data = [ 69 | 'foo' => 'bar', 70 | 'bar' => 'baz', 71 | 'time' => time(), 72 | ]; 73 | 74 | $qos = Morbo\React\Mqtt\Packets\QoS\Levels::AT_MOST_ONCE_DELIVERY; // 0 75 | 76 | $mqtt->publish($stream, 'foo/bar', json_encode($data), $qos)->then(function (ConnectionInterface $stream) use ($mqtt) { 77 | /** 78 | * Disconnect when published 79 | */ 80 | $mqtt->disconnect($stream); 81 | }); 82 | }); 83 | 84 | 85 | $loop->run(); 86 | ``` 87 | 88 | ## Example subscribe 89 | 90 | 91 | ```php 92 | use Morbo\React\Mqtt\Packets; 93 | use React\Socket\ConnectionInterface; 94 | 95 | require 'mqtt.php'; 96 | 97 | $connection = $mqtt->connect($config['host'], $config['port'], $config['options']); 98 | 99 | $connection->then(function (ConnectionInterface $stream) use ($mqtt) { 100 | $qos = Morbo\React\Mqtt\Packets\QoS\Levels::AT_MOST_ONCE_DELIVERY; // 0 101 | $mqtt->subscribe($stream, 'foo/bar', $qos)->then(function (ConnectionInterface $stream) use ($qos) { 102 | // Success subscription 103 | $stream->on(Packets\Publish::EVENT, function(Packets\Publish $publish) { 104 | var_dump($publish); 105 | }); 106 | }, function ($error) { 107 | // Subscription error 108 | }); 109 | }); 110 | 111 | $loop->run(); 112 | ``` 113 | 114 | ## Avaiable methods 115 | Currently works: 116 | * connect (clean session, will options, keepalive, connection authorization) 117 | * disconnect 118 | * publish 119 | * subscribe -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexmorbo/react-mqtt", 3 | "description": "Async MQTT client in reactphp", 4 | "keywords": ["mqtt", "IoT"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "AlexMorbo", 9 | "email": "alex@morbo.ru" 10 | }, 11 | { 12 | "name": "Oliver Lorenz", 13 | "email": "mail@oliverlorenz.com" 14 | } 15 | ], 16 | "require": { 17 | "react/socket": "^1.11.0", 18 | "psr/log": "^3.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Morbo\\React\\Mqtt\\": "src/" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "9526a30a14565e14535440529ed47a97", 8 | "packages": [ 9 | { 10 | "name": "evenement/evenement", 11 | "version": "v3.0.1", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/igorw/evenement.git", 15 | "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", 20 | "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=7.0" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^6.0" 28 | }, 29 | "type": "library", 30 | "autoload": { 31 | "psr-0": { 32 | "Evenement": "src" 33 | } 34 | }, 35 | "notification-url": "https://packagist.org/downloads/", 36 | "license": [ 37 | "MIT" 38 | ], 39 | "authors": [ 40 | { 41 | "name": "Igor Wiedler", 42 | "email": "igor@wiedler.ch" 43 | } 44 | ], 45 | "description": "Événement is a very simple event dispatching library for PHP", 46 | "keywords": [ 47 | "event-dispatcher", 48 | "event-emitter" 49 | ], 50 | "support": { 51 | "issues": "https://github.com/igorw/evenement/issues", 52 | "source": "https://github.com/igorw/evenement/tree/master" 53 | }, 54 | "time": "2017-07-23T21:35:13+00:00" 55 | }, 56 | { 57 | "name": "psr/log", 58 | "version": "3.0.0", 59 | "source": { 60 | "type": "git", 61 | "url": "https://github.com/php-fig/log.git", 62 | "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" 63 | }, 64 | "dist": { 65 | "type": "zip", 66 | "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", 67 | "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", 68 | "shasum": "" 69 | }, 70 | "require": { 71 | "php": ">=8.0.0" 72 | }, 73 | "type": "library", 74 | "extra": { 75 | "branch-alias": { 76 | "dev-master": "3.x-dev" 77 | } 78 | }, 79 | "autoload": { 80 | "psr-4": { 81 | "Psr\\Log\\": "src" 82 | } 83 | }, 84 | "notification-url": "https://packagist.org/downloads/", 85 | "license": [ 86 | "MIT" 87 | ], 88 | "authors": [ 89 | { 90 | "name": "PHP-FIG", 91 | "homepage": "https://www.php-fig.org/" 92 | } 93 | ], 94 | "description": "Common interface for logging libraries", 95 | "homepage": "https://github.com/php-fig/log", 96 | "keywords": [ 97 | "log", 98 | "psr", 99 | "psr-3" 100 | ], 101 | "support": { 102 | "source": "https://github.com/php-fig/log/tree/3.0.0" 103 | }, 104 | "time": "2021-07-14T16:46:02+00:00" 105 | }, 106 | { 107 | "name": "react/cache", 108 | "version": "v1.1.1", 109 | "source": { 110 | "type": "git", 111 | "url": "https://github.com/reactphp/cache.git", 112 | "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e" 113 | }, 114 | "dist": { 115 | "type": "zip", 116 | "url": "https://api.github.com/repos/reactphp/cache/zipball/4bf736a2cccec7298bdf745db77585966fc2ca7e", 117 | "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e", 118 | "shasum": "" 119 | }, 120 | "require": { 121 | "php": ">=5.3.0", 122 | "react/promise": "^3.0 || ^2.0 || ^1.1" 123 | }, 124 | "require-dev": { 125 | "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" 126 | }, 127 | "type": "library", 128 | "autoload": { 129 | "psr-4": { 130 | "React\\Cache\\": "src/" 131 | } 132 | }, 133 | "notification-url": "https://packagist.org/downloads/", 134 | "license": [ 135 | "MIT" 136 | ], 137 | "authors": [ 138 | { 139 | "name": "Christian Lück", 140 | "email": "christian@clue.engineering", 141 | "homepage": "https://clue.engineering/" 142 | }, 143 | { 144 | "name": "Cees-Jan Kiewiet", 145 | "email": "reactphp@ceesjankiewiet.nl", 146 | "homepage": "https://wyrihaximus.net/" 147 | }, 148 | { 149 | "name": "Jan Sorgalla", 150 | "email": "jsorgalla@gmail.com", 151 | "homepage": "https://sorgalla.com/" 152 | }, 153 | { 154 | "name": "Chris Boden", 155 | "email": "cboden@gmail.com", 156 | "homepage": "https://cboden.dev/" 157 | } 158 | ], 159 | "description": "Async, Promise-based cache interface for ReactPHP", 160 | "keywords": [ 161 | "cache", 162 | "caching", 163 | "promise", 164 | "reactphp" 165 | ], 166 | "support": { 167 | "issues": "https://github.com/reactphp/cache/issues", 168 | "source": "https://github.com/reactphp/cache/tree/v1.1.1" 169 | }, 170 | "funding": [ 171 | { 172 | "url": "https://github.com/WyriHaximus", 173 | "type": "github" 174 | }, 175 | { 176 | "url": "https://github.com/clue", 177 | "type": "github" 178 | } 179 | ], 180 | "time": "2021-02-02T06:47:52+00:00" 181 | }, 182 | { 183 | "name": "react/dns", 184 | "version": "v1.9.0", 185 | "source": { 186 | "type": "git", 187 | "url": "https://github.com/reactphp/dns.git", 188 | "reference": "6d38296756fa644e6cb1bfe95eff0f9a4ed6edcb" 189 | }, 190 | "dist": { 191 | "type": "zip", 192 | "url": "https://api.github.com/repos/reactphp/dns/zipball/6d38296756fa644e6cb1bfe95eff0f9a4ed6edcb", 193 | "reference": "6d38296756fa644e6cb1bfe95eff0f9a4ed6edcb", 194 | "shasum": "" 195 | }, 196 | "require": { 197 | "php": ">=5.3.0", 198 | "react/cache": "^1.0 || ^0.6 || ^0.5", 199 | "react/event-loop": "^1.2", 200 | "react/promise": "^3.0 || ^2.7 || ^1.2.1", 201 | "react/promise-timer": "^1.8" 202 | }, 203 | "require-dev": { 204 | "clue/block-react": "^1.2", 205 | "phpunit/phpunit": "^9.3 || ^4.8.35" 206 | }, 207 | "type": "library", 208 | "autoload": { 209 | "psr-4": { 210 | "React\\Dns\\": "src" 211 | } 212 | }, 213 | "notification-url": "https://packagist.org/downloads/", 214 | "license": [ 215 | "MIT" 216 | ], 217 | "authors": [ 218 | { 219 | "name": "Christian Lück", 220 | "email": "christian@clue.engineering", 221 | "homepage": "https://clue.engineering/" 222 | }, 223 | { 224 | "name": "Cees-Jan Kiewiet", 225 | "email": "reactphp@ceesjankiewiet.nl", 226 | "homepage": "https://wyrihaximus.net/" 227 | }, 228 | { 229 | "name": "Jan Sorgalla", 230 | "email": "jsorgalla@gmail.com", 231 | "homepage": "https://sorgalla.com/" 232 | }, 233 | { 234 | "name": "Chris Boden", 235 | "email": "cboden@gmail.com", 236 | "homepage": "https://cboden.dev/" 237 | } 238 | ], 239 | "description": "Async DNS resolver for ReactPHP", 240 | "keywords": [ 241 | "async", 242 | "dns", 243 | "dns-resolver", 244 | "reactphp" 245 | ], 246 | "support": { 247 | "issues": "https://github.com/reactphp/dns/issues", 248 | "source": "https://github.com/reactphp/dns/tree/v1.9.0" 249 | }, 250 | "funding": [ 251 | { 252 | "url": "https://github.com/WyriHaximus", 253 | "type": "github" 254 | }, 255 | { 256 | "url": "https://github.com/clue", 257 | "type": "github" 258 | } 259 | ], 260 | "time": "2021-12-20T08:46:54+00:00" 261 | }, 262 | { 263 | "name": "react/event-loop", 264 | "version": "v1.2.0", 265 | "source": { 266 | "type": "git", 267 | "url": "https://github.com/reactphp/event-loop.git", 268 | "reference": "be6dee480fc4692cec0504e65eb486e3be1aa6f2" 269 | }, 270 | "dist": { 271 | "type": "zip", 272 | "url": "https://api.github.com/repos/reactphp/event-loop/zipball/be6dee480fc4692cec0504e65eb486e3be1aa6f2", 273 | "reference": "be6dee480fc4692cec0504e65eb486e3be1aa6f2", 274 | "shasum": "" 275 | }, 276 | "require": { 277 | "php": ">=5.3.0" 278 | }, 279 | "require-dev": { 280 | "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" 281 | }, 282 | "suggest": { 283 | "ext-event": "~1.0 for ExtEventLoop", 284 | "ext-pcntl": "For signal handling support when using the StreamSelectLoop", 285 | "ext-uv": "* for ExtUvLoop" 286 | }, 287 | "type": "library", 288 | "autoload": { 289 | "psr-4": { 290 | "React\\EventLoop\\": "src" 291 | } 292 | }, 293 | "notification-url": "https://packagist.org/downloads/", 294 | "license": [ 295 | "MIT" 296 | ], 297 | "authors": [ 298 | { 299 | "name": "Christian Lück", 300 | "email": "christian@clue.engineering", 301 | "homepage": "https://clue.engineering/" 302 | }, 303 | { 304 | "name": "Cees-Jan Kiewiet", 305 | "email": "reactphp@ceesjankiewiet.nl", 306 | "homepage": "https://wyrihaximus.net/" 307 | }, 308 | { 309 | "name": "Jan Sorgalla", 310 | "email": "jsorgalla@gmail.com", 311 | "homepage": "https://sorgalla.com/" 312 | }, 313 | { 314 | "name": "Chris Boden", 315 | "email": "cboden@gmail.com", 316 | "homepage": "https://cboden.dev/" 317 | } 318 | ], 319 | "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", 320 | "keywords": [ 321 | "asynchronous", 322 | "event-loop" 323 | ], 324 | "support": { 325 | "issues": "https://github.com/reactphp/event-loop/issues", 326 | "source": "https://github.com/reactphp/event-loop/tree/v1.2.0" 327 | }, 328 | "funding": [ 329 | { 330 | "url": "https://github.com/WyriHaximus", 331 | "type": "github" 332 | }, 333 | { 334 | "url": "https://github.com/clue", 335 | "type": "github" 336 | } 337 | ], 338 | "time": "2021-07-11T12:31:24+00:00" 339 | }, 340 | { 341 | "name": "react/promise", 342 | "version": "v2.9.0", 343 | "source": { 344 | "type": "git", 345 | "url": "https://github.com/reactphp/promise.git", 346 | "reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910" 347 | }, 348 | "dist": { 349 | "type": "zip", 350 | "url": "https://api.github.com/repos/reactphp/promise/zipball/234f8fd1023c9158e2314fa9d7d0e6a83db42910", 351 | "reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910", 352 | "shasum": "" 353 | }, 354 | "require": { 355 | "php": ">=5.4.0" 356 | }, 357 | "require-dev": { 358 | "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" 359 | }, 360 | "type": "library", 361 | "autoload": { 362 | "files": [ 363 | "src/functions_include.php" 364 | ], 365 | "psr-4": { 366 | "React\\Promise\\": "src/" 367 | } 368 | }, 369 | "notification-url": "https://packagist.org/downloads/", 370 | "license": [ 371 | "MIT" 372 | ], 373 | "authors": [ 374 | { 375 | "name": "Jan Sorgalla", 376 | "email": "jsorgalla@gmail.com", 377 | "homepage": "https://sorgalla.com/" 378 | }, 379 | { 380 | "name": "Christian Lück", 381 | "email": "christian@clue.engineering", 382 | "homepage": "https://clue.engineering/" 383 | }, 384 | { 385 | "name": "Cees-Jan Kiewiet", 386 | "email": "reactphp@ceesjankiewiet.nl", 387 | "homepage": "https://wyrihaximus.net/" 388 | }, 389 | { 390 | "name": "Chris Boden", 391 | "email": "cboden@gmail.com", 392 | "homepage": "https://cboden.dev/" 393 | } 394 | ], 395 | "description": "A lightweight implementation of CommonJS Promises/A for PHP", 396 | "keywords": [ 397 | "promise", 398 | "promises" 399 | ], 400 | "support": { 401 | "issues": "https://github.com/reactphp/promise/issues", 402 | "source": "https://github.com/reactphp/promise/tree/v2.9.0" 403 | }, 404 | "funding": [ 405 | { 406 | "url": "https://github.com/WyriHaximus", 407 | "type": "github" 408 | }, 409 | { 410 | "url": "https://github.com/clue", 411 | "type": "github" 412 | } 413 | ], 414 | "time": "2022-02-11T10:27:51+00:00" 415 | }, 416 | { 417 | "name": "react/promise-timer", 418 | "version": "v1.8.0", 419 | "source": { 420 | "type": "git", 421 | "url": "https://github.com/reactphp/promise-timer.git", 422 | "reference": "0bbbcc79589e5bfdddba68a287f1cb805581a479" 423 | }, 424 | "dist": { 425 | "type": "zip", 426 | "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/0bbbcc79589e5bfdddba68a287f1cb805581a479", 427 | "reference": "0bbbcc79589e5bfdddba68a287f1cb805581a479", 428 | "shasum": "" 429 | }, 430 | "require": { 431 | "php": ">=5.3", 432 | "react/event-loop": "^1.2", 433 | "react/promise": "^3.0 || ^2.7.0 || ^1.2.1" 434 | }, 435 | "require-dev": { 436 | "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" 437 | }, 438 | "type": "library", 439 | "autoload": { 440 | "files": [ 441 | "src/functions_include.php" 442 | ], 443 | "psr-4": { 444 | "React\\Promise\\Timer\\": "src/" 445 | } 446 | }, 447 | "notification-url": "https://packagist.org/downloads/", 448 | "license": [ 449 | "MIT" 450 | ], 451 | "authors": [ 452 | { 453 | "name": "Christian Lück", 454 | "email": "christian@clue.engineering", 455 | "homepage": "https://clue.engineering/" 456 | }, 457 | { 458 | "name": "Cees-Jan Kiewiet", 459 | "email": "reactphp@ceesjankiewiet.nl", 460 | "homepage": "https://wyrihaximus.net/" 461 | }, 462 | { 463 | "name": "Jan Sorgalla", 464 | "email": "jsorgalla@gmail.com", 465 | "homepage": "https://sorgalla.com/" 466 | }, 467 | { 468 | "name": "Chris Boden", 469 | "email": "cboden@gmail.com", 470 | "homepage": "https://cboden.dev/" 471 | } 472 | ], 473 | "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", 474 | "homepage": "https://github.com/reactphp/promise-timer", 475 | "keywords": [ 476 | "async", 477 | "event-loop", 478 | "promise", 479 | "reactphp", 480 | "timeout", 481 | "timer" 482 | ], 483 | "support": { 484 | "issues": "https://github.com/reactphp/promise-timer/issues", 485 | "source": "https://github.com/reactphp/promise-timer/tree/v1.8.0" 486 | }, 487 | "funding": [ 488 | { 489 | "url": "https://github.com/WyriHaximus", 490 | "type": "github" 491 | }, 492 | { 493 | "url": "https://github.com/clue", 494 | "type": "github" 495 | } 496 | ], 497 | "time": "2021-12-06T11:08:48+00:00" 498 | }, 499 | { 500 | "name": "react/socket", 501 | "version": "v1.11.0", 502 | "source": { 503 | "type": "git", 504 | "url": "https://github.com/reactphp/socket.git", 505 | "reference": "f474156aaab4f09041144fa8b57c7d70aed32a1c" 506 | }, 507 | "dist": { 508 | "type": "zip", 509 | "url": "https://api.github.com/repos/reactphp/socket/zipball/f474156aaab4f09041144fa8b57c7d70aed32a1c", 510 | "reference": "f474156aaab4f09041144fa8b57c7d70aed32a1c", 511 | "shasum": "" 512 | }, 513 | "require": { 514 | "evenement/evenement": "^3.0 || ^2.0 || ^1.0", 515 | "php": ">=5.3.0", 516 | "react/dns": "^1.8", 517 | "react/event-loop": "^1.2", 518 | "react/promise": "^2.6.0 || ^1.2.1", 519 | "react/promise-timer": "^1.8", 520 | "react/stream": "^1.2" 521 | }, 522 | "require-dev": { 523 | "clue/block-react": "^1.5", 524 | "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", 525 | "react/promise-stream": "^1.2" 526 | }, 527 | "type": "library", 528 | "autoload": { 529 | "psr-4": { 530 | "React\\Socket\\": "src" 531 | } 532 | }, 533 | "notification-url": "https://packagist.org/downloads/", 534 | "license": [ 535 | "MIT" 536 | ], 537 | "authors": [ 538 | { 539 | "name": "Christian Lück", 540 | "email": "christian@clue.engineering", 541 | "homepage": "https://clue.engineering/" 542 | }, 543 | { 544 | "name": "Cees-Jan Kiewiet", 545 | "email": "reactphp@ceesjankiewiet.nl", 546 | "homepage": "https://wyrihaximus.net/" 547 | }, 548 | { 549 | "name": "Jan Sorgalla", 550 | "email": "jsorgalla@gmail.com", 551 | "homepage": "https://sorgalla.com/" 552 | }, 553 | { 554 | "name": "Chris Boden", 555 | "email": "cboden@gmail.com", 556 | "homepage": "https://cboden.dev/" 557 | } 558 | ], 559 | "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", 560 | "keywords": [ 561 | "Connection", 562 | "Socket", 563 | "async", 564 | "reactphp", 565 | "stream" 566 | ], 567 | "support": { 568 | "issues": "https://github.com/reactphp/socket/issues", 569 | "source": "https://github.com/reactphp/socket/tree/v1.11.0" 570 | }, 571 | "funding": [ 572 | { 573 | "url": "https://github.com/WyriHaximus", 574 | "type": "github" 575 | }, 576 | { 577 | "url": "https://github.com/clue", 578 | "type": "github" 579 | } 580 | ], 581 | "time": "2022-01-14T10:14:32+00:00" 582 | }, 583 | { 584 | "name": "react/stream", 585 | "version": "v1.2.0", 586 | "source": { 587 | "type": "git", 588 | "url": "https://github.com/reactphp/stream.git", 589 | "reference": "7a423506ee1903e89f1e08ec5f0ed430ff784ae9" 590 | }, 591 | "dist": { 592 | "type": "zip", 593 | "url": "https://api.github.com/repos/reactphp/stream/zipball/7a423506ee1903e89f1e08ec5f0ed430ff784ae9", 594 | "reference": "7a423506ee1903e89f1e08ec5f0ed430ff784ae9", 595 | "shasum": "" 596 | }, 597 | "require": { 598 | "evenement/evenement": "^3.0 || ^2.0 || ^1.0", 599 | "php": ">=5.3.8", 600 | "react/event-loop": "^1.2" 601 | }, 602 | "require-dev": { 603 | "clue/stream-filter": "~1.2", 604 | "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" 605 | }, 606 | "type": "library", 607 | "autoload": { 608 | "psr-4": { 609 | "React\\Stream\\": "src" 610 | } 611 | }, 612 | "notification-url": "https://packagist.org/downloads/", 613 | "license": [ 614 | "MIT" 615 | ], 616 | "authors": [ 617 | { 618 | "name": "Christian Lück", 619 | "email": "christian@clue.engineering", 620 | "homepage": "https://clue.engineering/" 621 | }, 622 | { 623 | "name": "Cees-Jan Kiewiet", 624 | "email": "reactphp@ceesjankiewiet.nl", 625 | "homepage": "https://wyrihaximus.net/" 626 | }, 627 | { 628 | "name": "Jan Sorgalla", 629 | "email": "jsorgalla@gmail.com", 630 | "homepage": "https://sorgalla.com/" 631 | }, 632 | { 633 | "name": "Chris Boden", 634 | "email": "cboden@gmail.com", 635 | "homepage": "https://cboden.dev/" 636 | } 637 | ], 638 | "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", 639 | "keywords": [ 640 | "event-driven", 641 | "io", 642 | "non-blocking", 643 | "pipe", 644 | "reactphp", 645 | "readable", 646 | "stream", 647 | "writable" 648 | ], 649 | "support": { 650 | "issues": "https://github.com/reactphp/stream/issues", 651 | "source": "https://github.com/reactphp/stream/tree/v1.2.0" 652 | }, 653 | "funding": [ 654 | { 655 | "url": "https://github.com/WyriHaximus", 656 | "type": "github" 657 | }, 658 | { 659 | "url": "https://github.com/clue", 660 | "type": "github" 661 | } 662 | ], 663 | "time": "2021-07-11T12:37:55+00:00" 664 | } 665 | ], 666 | "packages-dev": [], 667 | "aliases": [], 668 | "minimum-stability": "stable", 669 | "stability-flags": [], 670 | "prefer-stable": false, 671 | "prefer-lowest": false, 672 | "platform": [], 673 | "platform-dev": [], 674 | "plugin-api-version": "2.2.0" 675 | } 676 | -------------------------------------------------------------------------------- /examples/mqtt.php: -------------------------------------------------------------------------------- 1 | 'localhost', 12 | 'port' => 1883, 13 | ]; 14 | 15 | $mqtt = new Client($loop, new Version4()); 16 | -------------------------------------------------------------------------------- /examples/publish.php: -------------------------------------------------------------------------------- 1 | connect($config['host'], $config['port'], $config['options']); 8 | 9 | $connection->then(function (ConnectionInterface $stream) use ($mqtt, $loop) { 10 | /** 11 | * Stop loop, when client disconnected from mqtt server 12 | */ 13 | $stream->on('end', function () use ($loop) { 14 | $loop->stop(); 15 | }); 16 | 17 | $data = [ 18 | 'foo' => 'bar', 19 | 'bar' => 'baz', 20 | 'time' => time(), 21 | ]; 22 | 23 | $qos = Morbo\React\Mqtt\Packets\QoS\Levels::AT_MOST_ONCE_DELIVERY; // 0 24 | 25 | $mqtt->publish($stream, 'foo/bar', json_encode($data), $qos)->then(function (ConnectionInterface $stream) use ($mqtt) { 26 | /** 27 | * Disconnect when published 28 | */ 29 | $mqtt->disconnect($stream); 30 | }); 31 | }); 32 | 33 | 34 | $loop->run(); -------------------------------------------------------------------------------- /examples/subscribe.php: -------------------------------------------------------------------------------- 1 | connect($config['host'], $config['port'], $config['options']); 9 | 10 | $connection->then(function (ConnectionInterface $stream) use ($mqtt) { 11 | $qos = Morbo\React\Mqtt\Packets\QoS\Levels::AT_MOST_ONCE_DELIVERY; // 0 12 | $mqtt->subscribe($stream, 'foo/bar', $qos)->then(function (ConnectionInterface $stream) use ($qos) { 13 | // Success subscription 14 | $stream->on(Packets\Publish::EVENT, function(Packets\Publish $publish) { 15 | var_dump($publish); 16 | }); 17 | }, function ($error) { 18 | // Subscription error 19 | }); 20 | }); 21 | 22 | $loop->run(); -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 61 | $this->version = $version; 62 | $this->logger = $logger; 63 | $this->connector = new Socket\TcpConnector($loop); 64 | $this->messageCounter = 1; 65 | $this->state = self::STATE_INITIATED; 66 | 67 | if (!$this->logger) { 68 | $this->logger = new Log\NullLogger(); 69 | } 70 | } 71 | 72 | public function connect(string $host, int $port, ConnectionOptions $options = null) 73 | { 74 | $this->logger->debug(sprintf('Initiate connection to %s:%d', $host, $port)); 75 | $this->state = self::STATE_CONNECTING; 76 | 77 | // Set default connection options, if none provided 78 | if ($options == null) { 79 | $options = $this->getDefaultConnectionOptions(); 80 | } 81 | 82 | $promise = $this->connector->connect(sprintf('%s:%d', $host, $port)); 83 | 84 | $promise->then(function (Socket\ConnectionInterface $stream) { 85 | $this->listenPackets($stream); 86 | }); 87 | 88 | $connection = $promise 89 | ->then(function (Socket\ConnectionInterface $stream) use ($options) { 90 | return $this->sendConnectPacket($stream, $options); 91 | }) 92 | ->then(function (Socket\ConnectionInterface $stream) use ($options) { 93 | $this->state = self::STATE_CONNECTED; 94 | 95 | return $this->setupKeepAlive($stream, $options->keepAlive); 96 | }) 97 | ->otherwise(function (Exception $e) { 98 | if ($e instanceof ConnectionException) { 99 | $this->logger->critical('Connection error', [$e->getMessage()]); 100 | } 101 | 102 | throw $e; 103 | }); 104 | 105 | return $connection; 106 | } 107 | 108 | protected function listenPackets(Socket\ConnectionInterface $stream) 109 | { 110 | $stream->on('data', function ($raw) use ($stream) { 111 | try { 112 | foreach (Utils\PacketFactory::getNextPacket($this->version, $raw) as $packet) { 113 | $this->logger->debug('Received packet: ' . get_class($packet)); 114 | $stream->emit($packet::EVENT, [$packet]); 115 | } 116 | } catch (VersionViolation $e) { 117 | $stream->emit('INVALID', [$e]); 118 | } 119 | }); 120 | 121 | $stream->on('close', function () { 122 | $this->state = self::STATE_DISCONNECTED; 123 | $this->logger->debug('Stream was closed'); 124 | }); 125 | 126 | $this->logger->debug('Packets listened initiated'); 127 | } 128 | 129 | protected function sendConnectPacket( 130 | Socket\ConnectionInterface $stream, 131 | ConnectionOptions $options): Promise\PromiseInterface 132 | { 133 | $packet = new Packets\Connect( 134 | $this->version, 135 | $options->username, 136 | $options->password, 137 | $options->clientId, 138 | $options->cleanSession, 139 | $options->will, 140 | $options->keepAlive 141 | ); 142 | 143 | $deferred = new Promise\Deferred(); 144 | $stream->on(Packets\ConnectionAck::EVENT, function (Packets\ConnectionAck $ack) use ($stream, $deferred) { 145 | $this->logger->debug('Received ' . Packets\ConnectionAck::EVENT . ' event', ['statusCode' => $ack->getStatusCode()]); 146 | if ($ack->getConnected()) { 147 | $deferred->resolve($stream); 148 | } 149 | $deferred->reject( 150 | new ConnectionException('Unable to establish connection, statusCode is '.$ack->getStatusCode()) 151 | ); 152 | }); 153 | 154 | $this->sendPacketToStream($stream, $packet); 155 | 156 | return $deferred->promise(); 157 | } 158 | 159 | protected function setupKeepAlive(Socket\ConnectionInterface $stream, int $interval) 160 | { 161 | if ($interval > 0) { 162 | $this->logger->debug('KeepAlive interval is ' . $interval); 163 | $this->loop->addPeriodicTimer($interval, function (EventLoop\TimerInterface $timer) use ($stream) { 164 | if ($this->state === self::STATE_CONNECTED) { 165 | $packet = new Packets\PingRequest($this->version); 166 | $this->sendPacketToStream($stream, $packet); 167 | } 168 | $this->keepAliveTimer = $timer; 169 | }); 170 | } 171 | 172 | return new Promise\FulfilledPromise($stream); 173 | } 174 | 175 | public function subscribe(Socket\ConnectionInterface $stream, $topic, $qos = 0): Promise\PromiseInterface 176 | { 177 | if ($this->state !== self::STATE_CONNECTED) { 178 | return new Promise\RejectedPromise('Connection unavailable'); 179 | } 180 | 181 | $subscribePacket = new Packets\Subscribe($this->version); 182 | $subscribePacket->addSubscription($topic, $qos); 183 | $this->sendPacketToStream($stream, $subscribePacket); 184 | $this->logger->debug('Send subscription, packetId: '.$subscribePacket->getPacketId()); 185 | 186 | $deferred = new Promise\Deferred(); 187 | $stream->on(Packets\SubscribeAck::EVENT, function (Packets\SubscribeAck $ackPacket) use ($stream, $deferred, $subscribePacket) { 188 | if ($subscribePacket->getPacketId() === $ackPacket->getPacketId()) { 189 | $this->logger->debug('Subscription successful', [ 190 | 'topic' => $subscribePacket->getTopic(), 191 | 'qos' => $subscribePacket->getQoS() 192 | ]); 193 | $deferred->resolve($stream); 194 | } else { 195 | $deferred->reject('Subscription ack has wrong packetId'); 196 | } 197 | }); 198 | 199 | return $deferred->promise(); 200 | } 201 | 202 | public function unsubscribe(Socket\ConnectionInterface $stream, $topic): Promise\PromiseInterface 203 | { 204 | if ($this->state !== self::STATE_CONNECTED) { 205 | return new Promise\RejectedPromise('Connection unavailable'); 206 | } 207 | 208 | $unsubscribePacket = new Packets\Unsubscribe($this->version); 209 | $unsubscribePacket->removeSubscription($topic); 210 | $this->sendPacketToStream($stream, $unsubscribePacket); 211 | 212 | $deferred = new Promise\Deferred(); 213 | 214 | $stream->on(Packets\UnsubscribeAck::EVENT, function (Packets\UnsubscribeAck $ackPacket) use ($stream, $deferred, $unsubscribePacket) { 215 | if ($unsubscribePacket->getPacketId() === $ackPacket->getPacketId()) { 216 | $this->logger->debug('Unsubscription successful', [ 217 | 'topic' => $unsubscribePacket->getTopic() 218 | ]); 219 | $deferred->resolve($stream); 220 | } else { 221 | $deferred->reject('Subscription ack has wrong packetId'); 222 | } 223 | $deferred->resolve($stream); 224 | }); 225 | 226 | return $deferred->promise(); 227 | } 228 | 229 | public function publish( 230 | Socket\ConnectionInterface $stream, 231 | string $topic, 232 | string $message, 233 | int $qos = 0, 234 | bool $dup = false, 235 | bool $retain = false 236 | ): Promise\PromiseInterface 237 | { 238 | if ($this->state !== self::STATE_CONNECTED) { 239 | return new Promise\RejectedPromise('Connection unavailable'); 240 | } 241 | 242 | $publishPacket = new Packets\Publish($this->version); 243 | $publishPacket->setTopic($topic); 244 | $publishPacket->setQos($qos); 245 | $publishPacket->setDup($dup); 246 | $publishPacket->setRetain($retain); 247 | 248 | $success = $this->sendPacketToStream($stream, $publishPacket, $message); 249 | 250 | $deferred = new Promise\Deferred(); 251 | if ($success) { 252 | if ($qos === Packets\QoS\Levels::AT_LEAST_ONCE_DELIVERY) { 253 | $stream->on(Packets\PublishAck::EVENT, function (Packets\PublishAck $message) use ($deferred, $stream) { 254 | $this->logger->debug('QoS: '.Packets\QoS\Levels::AT_LEAST_ONCE_DELIVERY.', packetId: '.$message->getPacketId()); 255 | $deferred->resolve($stream); 256 | }); 257 | } elseif ($qos === Packets\QoS\Levels::EXACTLY_ONCE_DELIVERY) { 258 | $stream->on(Packets\PublishReceived::EVENT, function (Packets\PublishReceived $receivedPacket) use ($stream, $deferred, $publishPacket) { 259 | if ($publishPacket->getPacketId() === $receivedPacket->getPacketId()) { 260 | $this->logger->debug('QoS: '.Packets\QoS\Levels::AT_LEAST_ONCE_DELIVERY.', packetId: '.$receivedPacket->getPacketId()); 261 | 262 | $releasePacket = new Packets\PublishRelease($this->version); 263 | $releasePacket->setPacketId($receivedPacket->getPacketId()); 264 | $stream->write($releasePacket->get()); 265 | 266 | $deferred->resolve($stream); 267 | } else { 268 | $deferred->reject('PublishReceived ack has wrong packetId'); 269 | } 270 | }); 271 | } else { 272 | $deferred->resolve($stream); 273 | } 274 | } else { 275 | $deferred->reject(); 276 | } 277 | 278 | return $deferred->promise(); 279 | } 280 | 281 | public function disconnect(Socket\ConnectionInterface $stream): Promise\PromiseInterface 282 | { 283 | $packet = new Packets\Disconnect($this->version); 284 | $this->sendPacketToStream($stream, $packet); 285 | 286 | return new Promise\FulfilledPromise($stream); 287 | } 288 | 289 | protected function sendPacketToStream( 290 | Socket\ConnectionInterface $stream, 291 | Packets\ControlPacket $controlPacket, 292 | string $additionalPayload = '' 293 | ): bool 294 | { 295 | $this->logger->debug('Send packet to stream', ['packet' => get_class($controlPacket)]); 296 | $message = $controlPacket->get($additionalPayload); 297 | 298 | return $stream->write($message); 299 | } 300 | 301 | 302 | /** 303 | * Returns default connection options 304 | * 305 | * @return ConnectionOptions 306 | */ 307 | private function getDefaultConnectionOptions(): ConnectionOptions 308 | { 309 | return new ConnectionOptions(); 310 | } 311 | } -------------------------------------------------------------------------------- /src/ConnectionException.php: -------------------------------------------------------------------------------- 1 | populate($options); 122 | } 123 | 124 | /** 125 | * Populate these options from an array 126 | * 127 | * @param array $options [optional] 128 | */ 129 | public function populate(array $options = []) 130 | { 131 | foreach ($options as $key => $value) { 132 | $this->{$key} = $value; 133 | } 134 | } 135 | 136 | public function __get($name) 137 | { 138 | if ($name === 'will') { 139 | $active = $this->willMessage || $this->willTopic || $this->willQos || $this->willRetain; 140 | 141 | return [ 142 | 'active' => $active, 143 | 'message' => $this->willMessage, 144 | 'topic' => $this->willTopic, 145 | 'qos' => $this->willQos, 146 | 'retain' => $this->willRetain, 147 | ]; 148 | } 149 | 150 | return $this->{$name}; 151 | } 152 | } -------------------------------------------------------------------------------- /src/Packets/Connect.php: -------------------------------------------------------------------------------- 1 | false, 41 | 'message' => null, 42 | 'topic' => null, 43 | 'qos' => null, 44 | 'retain' => null, 45 | ]; 46 | 47 | /** 48 | * @var null|string 49 | */ 50 | protected $willTopic; 51 | 52 | /** 53 | * @var null|string 54 | */ 55 | protected $willMessage; 56 | 57 | /** 58 | * @var bool|null 59 | */ 60 | protected $willQos; 61 | 62 | /** 63 | * @var null 64 | */ 65 | protected $willRetain; 66 | 67 | /** 68 | * @var int 69 | */ 70 | private $keepAlive; 71 | 72 | public function __construct( 73 | VersionInterface $version, 74 | $username = null, 75 | $password = null, 76 | $clientId = null, 77 | $cleanSession = true, 78 | $will = [], 79 | $keepAlive = 0 80 | ) 81 | { 82 | parent::__construct($version); 83 | $this->clientId = $clientId; 84 | $this->username = $username; 85 | $this->password = $password; 86 | $this->cleanSession = (bool) $cleanSession; 87 | if ($will) { 88 | $this->will = $will; 89 | } 90 | $this->keepAlive = $keepAlive; 91 | } 92 | 93 | /** 94 | * @return int 95 | */ 96 | public function getControlPacketType() 97 | { 98 | return ControlPacketType::MQTT_CONNECT; 99 | } 100 | 101 | protected function buildPayload() 102 | { 103 | // Byte 1 - MSB 104 | $this->addRawToPayLoad(chr(ControlPacketType::MOST_SIGNIFICANT_BYTE)); 105 | // Byte 2 - LSB length 106 | $this->addRawToPayLoad(chr($this->version->getProtocolVersion())); 107 | // Byte 3,4,5,6 - Identifier 108 | $this->addRawToPayLoad($this->version->getProtocolIdentifierString()); 109 | // Byte 7 - Protocol level 110 | $this->addRawToPayLoad(chr($this->version->getProtocolVersion())); 111 | 112 | $connectFlags = 0; 113 | if ($this->cleanSession) { 114 | $connectFlags += 0x02; 115 | } 116 | if ($this->will['active']) { 117 | $connectFlags += 0x04; 118 | if ($this->will['qos']) { 119 | $connectFlags += ($this->will['active'] << 3); 120 | } 121 | if ($this->will['retain']) { 122 | $connectFlags += 0x20; 123 | } 124 | } 125 | if ($this->username) { 126 | $connectFlags += 0x80; 127 | if ($this->password) { 128 | $connectFlags += 0x40; 129 | } 130 | } 131 | // Connect flags 132 | $this->addRawToPayLoad(chr($connectFlags)); 133 | // Keepalive (MSB) 134 | $this->addRawToPayLoad(chr($this->keepAlive >> 8)); 135 | // Keepalive (LSB) 136 | $this->addRawToPayLoad(chr($this->keepAlive & 0xff)); 137 | if (empty($this->clientId)) { 138 | $this->clientId = 'react-'.uniqid(); 139 | } 140 | if ($this->clientId) { 141 | $this->addRawToPayLoad( 142 | $this->createPayload($this->clientId) 143 | ); 144 | } 145 | if ($this->will['active']) { 146 | $this->addRawToPayLoad( 147 | $this->createPayload($this->will['topic']) 148 | ); 149 | $this->addRawToPayLoad( 150 | $this->createPayload($this->will['message']) 151 | ); 152 | } 153 | if ($this->username) { 154 | $this->addRawToPayLoad( 155 | $this->createPayload($this->username) 156 | ); 157 | 158 | if ($this->password) { 159 | $this->addRawToPayLoad( 160 | $this->createPayload($this->password) 161 | ); 162 | } 163 | } 164 | 165 | return $this->payload; 166 | } 167 | } -------------------------------------------------------------------------------- /src/Packets/ConnectionAck.php: -------------------------------------------------------------------------------- 1 | connected = $status; 42 | } 43 | 44 | public function getConnected(): bool 45 | { 46 | return $this->connected; 47 | } 48 | 49 | public function setStatusCode(int $statusCode) 50 | { 51 | $this->statusCode = $statusCode; 52 | } 53 | 54 | public function getStatusCode(): int 55 | { 56 | return $this->statusCode; 57 | } 58 | 59 | public static function parse(VersionInterface $version, $rawInput) 60 | { 61 | $packet = new static($version); 62 | 63 | $statusCode = ord(substr($rawInput, 3)); 64 | $packet->setStatusCode($statusCode); 65 | if ($statusCode === self::CONNECTION_SUCCESS) { 66 | $packet->setConnected(true); 67 | } else { 68 | $packet->setConnected(false); 69 | } 70 | 71 | return $packet; 72 | } 73 | } -------------------------------------------------------------------------------- /src/Packets/ControlPacket.php: -------------------------------------------------------------------------------- 1 | version = $version; 30 | $this->payload = ''; 31 | } 32 | 33 | /** 34 | * Create MQTT header from command and payload 35 | * 36 | * @return string Header to send 37 | * @var int $command 38 | * 39 | */ 40 | protected function createHeader(int $command, string $additionalPayload = '') 41 | { 42 | $payload = $this->payload; 43 | if ($additionalPayload) { 44 | $payload .= $additionalPayload; 45 | } 46 | 47 | return chr($command) . $this->encodeLength(strlen($payload)); 48 | } 49 | 50 | /** 51 | * Encode length to bytes to send in stream 52 | * 53 | * @param integer $len 54 | * @return string 55 | */ 56 | protected function encodeLength($len) 57 | { 58 | if ($len < 0 || $len >= 128 * 128 * 128 * 128) { 59 | // illegal length 60 | return false; 61 | } 62 | 63 | $output = ''; 64 | 65 | do { 66 | $byte = $len & 0x7f; // keep lowest 7 bits 67 | $len = $len >> 7; // shift away lowest 7 bits 68 | if ($len > 0) { 69 | $byte = $byte | 0x80; // set high bit to indicate continuation 70 | } 71 | $output .= chr($byte); 72 | } while ($len > 0); 73 | 74 | return $output; 75 | } 76 | 77 | /** 78 | * Append payload data 79 | * 80 | * @param string $stringToAdd 81 | */ 82 | public function addRawToPayLoad(string $stringToAdd) 83 | { 84 | $this->payload .= $stringToAdd; 85 | } 86 | 87 | /** 88 | * Add payload length as bytes to begining of string and return 89 | * 90 | * @param string $payload 91 | * @return string 92 | */ 93 | protected function createPayload(string $payload): string 94 | { 95 | $fullLength = strlen($payload); 96 | $retval = chr($fullLength >> 8) . chr($fullLength & 0xff) . $payload; 97 | 98 | return $retval; 99 | } 100 | 101 | protected function buildPayload() 102 | { 103 | throw new \RuntimeException('You must overwrite buildPayload()'); 104 | } 105 | 106 | // /** 107 | // * @param VersionInterface $version 108 | // * @param string $rawInput 109 | // * @return static 110 | // */ 111 | // public static function parse(VersionInterface $version, $rawInput) 112 | // { 113 | // return new static($version); 114 | // } 115 | 116 | /** 117 | * @return int 118 | */ 119 | public function getControlPacketType() 120 | { 121 | throw new \RuntimeException('You must overwrite getControlPacketType()'); 122 | } 123 | 124 | /** 125 | * @return string 126 | */ 127 | public function getPayload(): string 128 | { 129 | return $this->payload; 130 | } 131 | 132 | public function get(string $additionalPayload = '') 133 | { 134 | $this->buildPayload(); 135 | $header = $this->createHeader($this->getControlPacketType(), $additionalPayload); 136 | 137 | $payload = $this->payload; 138 | if ($additionalPayload) { 139 | $payload .= $additionalPayload; 140 | } 141 | 142 | if (strlen($payload)) { 143 | return $header . $payload; 144 | } 145 | 146 | return $header; 147 | } 148 | } -------------------------------------------------------------------------------- /src/Packets/ControlPacketType.php: -------------------------------------------------------------------------------- 1 | payload; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Packets/PingRequest.php: -------------------------------------------------------------------------------- 1 | dup << 3) + ($this->qos << 1) + $this->retain; 46 | } 47 | 48 | public function buildPayload() 49 | { 50 | $this->packetId = $this->version->getNextPacketId(); 51 | 52 | // Topic 53 | $this->addRawToPayLoad( 54 | $this->createPayload($this->topic) 55 | ); 56 | 57 | if ($this->qos >= QoS\Levels::AT_LEAST_ONCE_DELIVERY) { 58 | $this->addRawToPayLoad( 59 | $this->version->getPacketIdPayload($this->packetId) 60 | ); 61 | } 62 | 63 | // $this->addRawToPayLoad( 64 | // $this->message 65 | // ); 66 | 67 | return $this->payload; 68 | } 69 | 70 | public static function parse(VersionInterface $version, $rawInput, $bytesRead) 71 | { 72 | $packet = new static($version); 73 | 74 | $flags = ord($rawInput[0]) & 0x0f; 75 | $packet->setDup($flags == 0x80); 76 | $packet->setRetain($flags == 0x01); 77 | $packet->setQos(($flags >> 1) & 0x03); 78 | 79 | $topicLength = (ord($rawInput[$bytesRead]) << 8) + ord($rawInput[$bytesRead + 1]); 80 | $packet->setTopic(substr($rawInput, 2 + $bytesRead, $topicLength)); 81 | $payload = substr($rawInput, $bytesRead + 2 + $topicLength); 82 | 83 | if ($packet->getQos() == QoS\Levels::AT_MOST_ONCE_DELIVERY) { 84 | // no packet id for QoS 0, the payload is the message 85 | $packet->setPayload($payload); 86 | } else { 87 | if (strlen($payload) >= 2) { 88 | $packet->setPacketId((ord($payload[0]) << 8) + ord($payload[1])); 89 | // skip packet id (2 bytes) for QoS 1 and 2 90 | $packet->setPayload(substr($payload, 2)); 91 | } 92 | } 93 | 94 | return $packet; 95 | } 96 | 97 | /** 98 | * @param $topic 99 | * @return $this 100 | */ 101 | public function setTopic(string $topic) 102 | { 103 | $this->topic = $topic; 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * @param string $payload 110 | * @return $this 111 | */ 112 | public function setPayload(string $payload) 113 | { 114 | $this->payload = $payload; 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * @param int $packetId 121 | */ 122 | public function setPacketId(int $packetId) 123 | { 124 | $this->packetId = $packetId; 125 | } 126 | 127 | /** 128 | * @param int $qos 0,1,2 129 | */ 130 | public function setQos($qos) 131 | { 132 | $this->qos = $qos; 133 | } 134 | 135 | /** 136 | * @param bool $dup 137 | */ 138 | public function setDup($dup) 139 | { 140 | $this->dup = $dup; 141 | } 142 | 143 | /** 144 | * @param bool $retain 145 | */ 146 | public function setRetain($retain) 147 | { 148 | $this->retain = $retain; 149 | 150 | return $this; 151 | } 152 | 153 | /** 154 | * @return string 155 | */ 156 | public function getTopic(): string 157 | { 158 | return $this->topic; 159 | } 160 | 161 | /** 162 | * @return int 163 | */ 164 | public function getQos(): int 165 | { 166 | return $this->qos; 167 | } 168 | 169 | /** 170 | * @return int 171 | */ 172 | public function getPacketId(): int 173 | { 174 | return $this->packetId; 175 | } 176 | } -------------------------------------------------------------------------------- /src/Packets/PublishAck.php: -------------------------------------------------------------------------------- 1 | setPacketId($data[1]); 33 | 34 | return $packet; 35 | } 36 | 37 | /** 38 | * @param $messageId 39 | */ 40 | public function setPacketId($messageId) 41 | { 42 | $this->packetId = $messageId; 43 | } 44 | 45 | /** 46 | * @return int 47 | */ 48 | public function getPacketId(): int 49 | { 50 | return $this->packetId; 51 | } 52 | } -------------------------------------------------------------------------------- /src/Packets/PublishComplete.php: -------------------------------------------------------------------------------- 1 | setPacketId($data[1]); 34 | 35 | return $packet; 36 | } 37 | 38 | /** 39 | * @param $messageId 40 | */ 41 | public function setPacketId($messageId) 42 | { 43 | $this->packetId = $messageId; 44 | } 45 | 46 | /** 47 | * @return int 48 | */ 49 | public function getPacketId(): int 50 | { 51 | return $this->packetId; 52 | } 53 | } -------------------------------------------------------------------------------- /src/Packets/PublishReceived.php: -------------------------------------------------------------------------------- 1 | setPacketId($data[1]); 34 | 35 | return $packet; 36 | } 37 | 38 | /** 39 | * @param $messageId 40 | */ 41 | public function setPacketId($messageId) 42 | { 43 | $this->packetId = $messageId; 44 | } 45 | 46 | /** 47 | * @return int 48 | */ 49 | public function getPacketId(): int 50 | { 51 | return $this->packetId; 52 | } 53 | } -------------------------------------------------------------------------------- /src/Packets/PublishRelease.php: -------------------------------------------------------------------------------- 1 | addRawToPayLoad( 29 | chr(($this->packetId & 0xff00)>>8) . chr($this->packetId & 0xff) 30 | ); 31 | } 32 | 33 | public function setPacketId(int $packetId) 34 | { 35 | $this->packetId = $packetId; 36 | } 37 | } -------------------------------------------------------------------------------- /src/Packets/QoS/Levels.php: -------------------------------------------------------------------------------- 1 | packetId = $this->version->getNextPacketId(); 38 | $this->addRawToPayLoad( 39 | $this->version->getPacketIdPayload($this->packetId) 40 | ); 41 | 42 | // Topic 43 | $this->addRawToPayLoad( 44 | $this->createPayload($this->topic) 45 | ); 46 | 47 | // QoS 48 | $this->addRawToPayLoad( 49 | chr($this->qos) 50 | ); 51 | 52 | return $this->payload; 53 | } 54 | 55 | /** 56 | * @param string $topic 57 | * @param int $qos 58 | */ 59 | public function addSubscription(string $topic, int $qos = 0) 60 | { 61 | $this->topic = $topic; 62 | $this->qos = $qos; 63 | } 64 | 65 | public function getPacketId(): int 66 | { 67 | return $this->packetId; 68 | } 69 | 70 | public function getTopic(): string 71 | { 72 | return $this->topic; 73 | } 74 | 75 | public function getQoS(): int 76 | { 77 | return $this->qos; 78 | } 79 | } -------------------------------------------------------------------------------- /src/Packets/SubscribeAck.php: -------------------------------------------------------------------------------- 1 | setPacketId($data[1]); 44 | $packet->setQos(ord(substr($rawInput, $length+1))); 45 | 46 | return $packet; 47 | } 48 | 49 | public function setQoS(int $qos) 50 | { 51 | $this->qos = $qos; 52 | } 53 | 54 | public function getQos(): int 55 | { 56 | return $this->qos; 57 | } 58 | 59 | public function setPacketId(int $packetId) 60 | { 61 | $this->packetId = $packetId; 62 | } 63 | 64 | public function getPacketId(): int 65 | { 66 | return $this->packetId; 67 | } 68 | } -------------------------------------------------------------------------------- /src/Packets/Unsubscribe.php: -------------------------------------------------------------------------------- 1 | packetId = $this->version->getNextPacketId(); 34 | $this->addRawToPayLoad( 35 | $this->version->getPacketIdPayload($this->packetId) 36 | ); 37 | 38 | // Topic 39 | $this->addRawToPayLoad( 40 | $this->createPayload($this->topic) 41 | ); 42 | 43 | return $this->payload; 44 | } 45 | 46 | /** 47 | * @param string $topic 48 | */ 49 | public function removeSubscription($topic) 50 | { 51 | $this->topic = $topic; 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getTopic(): string 58 | { 59 | return $this->topic; 60 | } 61 | 62 | /** 63 | * @return int 64 | */ 65 | public function getPacketId(): int 66 | { 67 | return $this->packetId; 68 | } 69 | } -------------------------------------------------------------------------------- /src/Packets/UnsubscribeAck.php: -------------------------------------------------------------------------------- 1 | setPacketId($data[1]); 41 | 42 | return $packet; 43 | } 44 | 45 | public function setPacketId(int $packetId) 46 | { 47 | $this->packetId = $packetId; 48 | } 49 | 50 | public function getPacketId(): int 51 | { 52 | return $this->packetId; 53 | } 54 | } -------------------------------------------------------------------------------- /src/Protocols/Version4.php: -------------------------------------------------------------------------------- 1 | packetId = rand(1, 100) * 100; 19 | } 20 | 21 | public function getProtocolIdentifierString(): string 22 | { 23 | return 'MQTT'; 24 | } 25 | 26 | public function getProtocolVersion(): int 27 | { 28 | return 0x04; 29 | } 30 | 31 | public function getNextPacketId(): int 32 | { 33 | return ($this->packetId = ($this->packetId + 1) & 0xffff); 34 | } 35 | 36 | public function getPacketIdPayload(int $packetId) { 37 | return chr(($packetId & 0xff00)>>8) . chr($packetId & 0xff); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Protocols/VersionInterface.php: -------------------------------------------------------------------------------- 1 | 4) { 32 | return false; 33 | } 34 | $str = $remainingData[$bytesRead]; 35 | if ($str === false || strlen($str) != 1) { 36 | return false; 37 | } 38 | $byte = ord($str[0]); 39 | $remainingLength += ($byte & 0x7f) * $multiplier; 40 | $isContinued = ($byte & 0x80); 41 | if ($isContinued) { 42 | $multiplier *= 128; 43 | } 44 | $bytesRead++; 45 | } while ($isContinued); 46 | 47 | $packetLength = 2 + $remainingLength; 48 | $nextPacketData = substr($remainingData, 0); 49 | $remainingData = substr($remainingData, $packetLength); 50 | 51 | yield self::getByMessage($version, $bytesRead, $nextPacketData); 52 | } 53 | } 54 | 55 | private static function getByMessage(VersionInterface $version, $bytesRead, $input) 56 | { 57 | $controlPacketType = ord($input[0]); 58 | 59 | switch ($controlPacketType) { 60 | case ControlPacketType::MQTT_CONNACK: 61 | return ConnectionAck::parse($version, $input); 62 | case ControlPacketType::MQTT_PINGRESP: 63 | return PingResponse::parse($version, $input); 64 | case ControlPacketType::MQTT_SUBACK: 65 | return SubscribeAck::parse($version, $input); 66 | case ControlPacketType::MQTT_UNSUBACK: 67 | return UnsubscribeAck::parse($version, $input); 68 | case ControlPacketType::MQTT_DISCONNECT: 69 | return Disconnect::parse($version, $input); 70 | case ControlPacketType::MQTT_PUBLISH: // QoS - 0 71 | case ControlPacketType::MQTT_PUBLISH + 0x02: // QoS - 1 72 | case ControlPacketType::MQTT_PUBLISH + 0x04: // QoS - 2 73 | return Publish::parse($version, $input, $bytesRead); 74 | case ControlPacketType::MQTT_PUBACK: 75 | return PublishAck::parse($version, $input); 76 | case ControlPacketType::MQTT_PUBREC: 77 | return PublishReceived::parse($version, $input); 78 | case ControlPacketType::MQTT_PUBREL: 79 | return PublishRelease::parse($version, $input); 80 | case ControlPacketType::MQTT_PUBCOMP: 81 | return PublishComplete::parse($version, $input); 82 | 83 | } 84 | 85 | throw new VersionViolation('Unexpected packet type: ' . $controlPacketType); 86 | } 87 | } --------------------------------------------------------------------------------