├── LICENSE.md ├── README.md ├── composer.json └── src ├── ReactFlow.php └── ReactMqttClient.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sebastian Mößler 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 13 | > all 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 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # net-mqtt-client-react 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE.md) 5 | [![Total Downloads][ico-downloads]][link-downloads] 6 | [![Build Status](https://travis-ci.org/binsoul/net-mqtt-client-react.svg?branch=master)](https://travis-ci.org/binsoul/net-mqtt-client-react) 7 | 8 | This package provides an asynchronous MQTT client built on the [React socket](https://github.com/reactphp/socket) library. All client methods return a promise which is fulfilled if the operation succeeded or rejected if the operation failed. Incoming messages of subscribed topics are delivered via the "message" event. 9 | 10 | ## Install 11 | 12 | Via composer: 13 | 14 | ``` bash 15 | $ composer require binsoul/net-mqtt-client-react 16 | ``` 17 | 18 | ## Example 19 | 20 | Connect to a public broker and run forever. 21 | 22 | ``` php 23 | createCached('8.8.8.8', $loop)); 40 | $client = new ReactMqttClient($connector, $loop); 41 | 42 | // Bind to events 43 | $client->on('open', function () use ($client) { 44 | // Network connection established 45 | echo sprintf("Open: %s:%d\n", $client->getHost(), $client->getPort()); 46 | }); 47 | 48 | $client->on('close', function () use ($client, $loop) { 49 | // Network connection closed 50 | echo sprintf("Close: %s:%d\n", $client->getHost(), $client->getPort()); 51 | 52 | $loop->stop(); 53 | }); 54 | 55 | $client->on('connect', function (Connection $connection) { 56 | // Broker connected 57 | echo sprintf("Connect: client=%s\n", $connection->getClientID()); 58 | }); 59 | 60 | $client->on('disconnect', function (Connection $connection) { 61 | // Broker disconnected 62 | echo sprintf("Disconnect: client=%s\n", $connection->getClientID()); 63 | }); 64 | 65 | $client->on('message', function (Message $message) { 66 | // Incoming message 67 | echo 'Message'; 68 | 69 | if ($message->isDuplicate()) { 70 | echo ' (duplicate)'; 71 | } 72 | 73 | if ($message->isRetained()) { 74 | echo ' (retained)'; 75 | } 76 | 77 | echo ': '.$message->getTopic().' => '.mb_strimwidth($message->getPayload(), 0, 50, '...'); 78 | echo "\n"; 79 | }); 80 | 81 | $client->on('warning', function (\Exception $e) { 82 | echo sprintf("Warning: %s\n", $e->getMessage()); 83 | }); 84 | 85 | $client->on('error', function (\Exception $e) use ($loop) { 86 | echo sprintf("Error: %s\n", $e->getMessage()); 87 | 88 | $loop->stop(); 89 | }); 90 | 91 | // Connect to broker 92 | $client->connect('test.mosquitto.org')->then( 93 | function () use ($client) { 94 | // Subscribe to all topics 95 | $client->subscribe(new DefaultSubscription('#')) 96 | ->then(function (Subscription $subscription) { 97 | echo sprintf("Subscribe: %s\n", $subscription->getFilter()); 98 | }) 99 | ->otherwise(function (\Exception $e) { 100 | echo sprintf("Error: %s\n", $e->getMessage()); 101 | }); 102 | 103 | // Publish humidity once 104 | $client->publish(new DefaultMessage('sensors/humidity', '55%')) 105 | ->then(function (Message $message) { 106 | echo sprintf("Publish: %s => %s\n", $message->getTopic(), $message->getPayload()); 107 | }) 108 | ->otherwise(function (\Exception $e) { 109 | echo sprintf("Error: %s\n", $e->getMessage()); 110 | }); 111 | 112 | // Publish a random temperature every 10 seconds 113 | $generator = function () { 114 | return mt_rand(-20, 30); 115 | }; 116 | 117 | $client->publishPeriodically(10, new DefaultMessage('sensors/temperature'), $generator) 118 | ->progress(function (Message $message) { 119 | echo sprintf("Publish: %s => %s\n", $message->getTopic(), $message->getPayload()); 120 | }) 121 | ->otherwise(function (\Exception $e) { 122 | echo sprintf("Error: %s\n", $e->getMessage()); 123 | }); 124 | } 125 | ); 126 | 127 | $loop->run(); 128 | ``` 129 | 130 | ## Testing 131 | 132 | ``` bash 133 | $ composer test 134 | ``` 135 | 136 | ## License 137 | 138 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 139 | 140 | [ico-version]: https://img.shields.io/packagist/v/binsoul/net-mqtt-client-react.svg?style=flat-square 141 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 142 | [ico-downloads]: https://img.shields.io/packagist/dt/binsoul/net-mqtt-client-react.svg?style=flat-square 143 | 144 | [link-packagist]: https://packagist.org/packages/binsoul/net-mqtt-client-react 145 | [link-downloads]: https://packagist.org/packages/binsoul/net-mqtt-client-react 146 | [link-author]: https://github.com/binsoul 147 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "binsoul/net-mqtt-client-react", 3 | "description": "Asynchronous MQTT client built on React", 4 | "keywords": [ 5 | "net", 6 | "mqtt", 7 | "client" 8 | ], 9 | "homepage": "https://github.com/binsoul/net-mqtt-client-react", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Sebastian Mößler", 14 | "email": "code@binsoul.de", 15 | "homepage": "https://github.com/binsoul", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^7.2 || ^8.0", 21 | "binsoul/net-mqtt": "^0.8", 22 | "evenement/evenement": "^3", 23 | "react/event-loop": "^1.1", 24 | "react/promise": "^2.7", 25 | "react/socket": "^1.3" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^8", 29 | "friendsofphp/php-cs-fixer": "^2", 30 | "phpstan/phpstan": "^0.12" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "BinSoul\\Net\\Mqtt\\Client\\React\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "BinSoul\\Test\\Net\\Mqtt\\Client\\React\\": "tests" 40 | }, 41 | "exclude-from-classmap": [ 42 | "/tests/" 43 | ] 44 | }, 45 | "scripts": { 46 | "test": "phpunit", 47 | "fix-style": [ 48 | "php-cs-fixer fix src --rules=@Symfony,-yoda_style", 49 | "php-cs-fixer fix tests --rules=@Symfony,-yoda_style" 50 | ], 51 | "analyze": "phpstan analyse -l 7 src" 52 | }, 53 | "extra": { 54 | "branch-alias": { 55 | "dev-master": "1.0-dev" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ReactFlow.php: -------------------------------------------------------------------------------- 1 | decorated = $decorated; 31 | $this->deferred = $deferred; 32 | $this->packet = $packet; 33 | $this->isSilent = $isSilent; 34 | } 35 | 36 | public function getCode(): string 37 | { 38 | return $this->decorated->getCode(); 39 | } 40 | 41 | public function start(): ?Packet 42 | { 43 | $this->packet = $this->decorated->start(); 44 | 45 | return $this->packet; 46 | } 47 | 48 | public function accept(Packet $packet): bool 49 | { 50 | return $this->decorated->accept($packet); 51 | } 52 | 53 | public function next(Packet $packet): ?Packet 54 | { 55 | $this->packet = $this->decorated->next($packet); 56 | 57 | return $this->packet; 58 | } 59 | 60 | public function isFinished(): bool 61 | { 62 | return $this->decorated->isFinished(); 63 | } 64 | 65 | public function isSuccess(): bool 66 | { 67 | return $this->decorated->isSuccess(); 68 | } 69 | 70 | public function getResult() 71 | { 72 | return $this->decorated->getResult(); 73 | } 74 | 75 | public function getErrorMessage(): string 76 | { 77 | return $this->decorated->getErrorMessage(); 78 | } 79 | 80 | /** 81 | * Returns the associated deferred. 82 | */ 83 | public function getDeferred(): Deferred 84 | { 85 | return $this->deferred; 86 | } 87 | 88 | /** 89 | * Returns the current packet. 90 | */ 91 | public function getPacket(): ?Packet 92 | { 93 | return $this->packet; 94 | } 95 | 96 | /** 97 | * Indicates if the flow should emit events. 98 | */ 99 | public function isSilent(): bool 100 | { 101 | return $this->isSilent; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/ReactMqttClient.php: -------------------------------------------------------------------------------- 1 | connector = $connector; 104 | $this->loop = $loop; 105 | 106 | $this->parser = $parser; 107 | if ($this->parser === null) { 108 | $this->parser = new StreamParser(new DefaultPacketFactory()); 109 | } 110 | 111 | $this->parser->onError(function (Throwable $error) { 112 | $this->emitWarning($error); 113 | }); 114 | 115 | $this->identifierGenerator = $identifierGenerator; 116 | if ($this->identifierGenerator === null) { 117 | $this->identifierGenerator = new DefaultIdentifierGenerator(); 118 | } 119 | 120 | $this->flowFactory = $flowFactory; 121 | if ($this->flowFactory === null) { 122 | $this->flowFactory = new DefaultFlowFactory($this->identifierGenerator, new DefaultIdentifierGenerator(), new DefaultPacketFactory()); 123 | } 124 | } 125 | 126 | /** 127 | * Return the host. 128 | */ 129 | public function getHost(): string 130 | { 131 | return $this->host; 132 | } 133 | 134 | /** 135 | * Return the port. 136 | */ 137 | public function getPort(): int 138 | { 139 | return $this->port; 140 | } 141 | 142 | /** 143 | * Indicates if the client is connected. 144 | */ 145 | public function isConnected(): bool 146 | { 147 | return $this->isConnected; 148 | } 149 | 150 | /** 151 | * Returns the underlying stream or null if the client is not connected. 152 | */ 153 | public function getStream(): ?DuplexStreamInterface 154 | { 155 | return $this->stream; 156 | } 157 | 158 | /** 159 | * Connects to a broker. 160 | */ 161 | public function connect(string $host, int $port = 1883, ?Connection $connection = null, int $timeout = 5): ExtendedPromiseInterface 162 | { 163 | if ($this->isConnected || $this->isConnecting) { 164 | return new RejectedPromise(new LogicException('The client is already connected.')); 165 | } 166 | 167 | $this->isConnecting = true; 168 | $this->isConnected = false; 169 | 170 | $this->host = $host; 171 | $this->port = $port; 172 | 173 | if ($connection === null) { 174 | $connection = new DefaultConnection(); 175 | } 176 | 177 | if ($connection->getClientID() === '') { 178 | $connection = $connection->withClientID($this->identifierGenerator->generateClientIdentifier()); 179 | } 180 | 181 | $deferred = new Deferred(); 182 | 183 | $this->establishConnection($this->host, $this->port, $timeout) 184 | ->then(function (DuplexStreamInterface $stream) use ($connection, $deferred, $timeout) { 185 | $this->stream = $stream; 186 | 187 | $this->emit('open', [$connection, $this]); 188 | 189 | $this->registerClient($connection, $timeout) 190 | ->then(function ($result) use ($connection, $deferred) { 191 | $this->isConnecting = false; 192 | $this->isConnected = true; 193 | $this->connection = $connection; 194 | 195 | $this->emit('connect', [$connection, $this]); 196 | $deferred->resolve($result ?: $connection); 197 | }) 198 | ->otherwise(function (Throwable $reason) use ($connection, $deferred) { 199 | $this->isConnecting = false; 200 | 201 | $this->emitError($reason); 202 | $deferred->reject($reason); 203 | 204 | if ($this->stream !== null) { 205 | $this->stream->close(); 206 | } 207 | 208 | $this->emit('close', [$connection, $this]); 209 | }); 210 | }) 211 | ->otherwise(function (Throwable $reason) use ($deferred) { 212 | $this->isConnecting = false; 213 | 214 | $this->emitError($reason); 215 | $deferred->reject($reason); 216 | }); 217 | 218 | return $deferred->promise(); 219 | } 220 | 221 | /** 222 | * Disconnects from a broker. 223 | */ 224 | public function disconnect(int $timeout = 5): ExtendedPromiseInterface 225 | { 226 | if (!$this->isConnected || $this->isDisconnecting) { 227 | return new RejectedPromise(new LogicException('The client is not connected.')); 228 | } 229 | 230 | $this->isDisconnecting = true; 231 | 232 | $deferred = new Deferred(); 233 | 234 | $isResolved = false; 235 | /** @var mixed $flowResult */ 236 | $flowResult = null; 237 | 238 | $this->onCloseCallback = function ($connection) use ($deferred, &$isResolved, &$flowResult) { 239 | if (!$isResolved) { 240 | $isResolved = true; 241 | 242 | if ($connection) { 243 | $this->emit('disconnect', [$connection, $this]); 244 | } 245 | 246 | $deferred->resolve($flowResult ?: $connection); 247 | } 248 | }; 249 | 250 | $this->startFlow($this->flowFactory->buildOutgoingDisconnectFlow($this->connection), true) 251 | ->then(function ($result) use ($timeout, &$flowResult) { 252 | $flowResult = $result; 253 | 254 | $this->timer[] = $this->loop->addTimer( 255 | $timeout, 256 | function () { 257 | if ($this->stream !== null) { 258 | $this->stream->close(); 259 | } 260 | } 261 | ); 262 | }) 263 | ->otherwise(function ($exception) use ($deferred, &$isResolved) { 264 | if (!$isResolved) { 265 | $isResolved = true; 266 | $this->isDisconnecting = false; 267 | $deferred->reject($exception); 268 | } 269 | }); 270 | 271 | return $deferred->promise(); 272 | } 273 | 274 | /** 275 | * Subscribes to a topic filter. 276 | */ 277 | public function subscribe(Subscription $subscription): ExtendedPromiseInterface 278 | { 279 | if (!$this->isConnected) { 280 | return new RejectedPromise(new LogicException('The client is not connected.')); 281 | } 282 | 283 | return $this->startFlow($this->flowFactory->buildOutgoingSubscribeFlow([$subscription])); 284 | } 285 | 286 | /** 287 | * Unsubscribes from a topic filter. 288 | */ 289 | public function unsubscribe(Subscription $subscription): ExtendedPromiseInterface 290 | { 291 | if (!$this->isConnected) { 292 | return new RejectedPromise(new LogicException('The client is not connected.')); 293 | } 294 | 295 | $deferred = new Deferred(); 296 | 297 | $this->startFlow($this->flowFactory->buildOutgoingUnsubscribeFlow([$subscription])) 298 | ->then(static function (array $subscriptions) use ($deferred) { 299 | $deferred->resolve(array_shift($subscriptions)); 300 | }) 301 | ->otherwise(static function ($exception) use ($deferred) { 302 | $deferred->reject($exception); 303 | }); 304 | 305 | return $deferred->promise(); 306 | } 307 | 308 | /** 309 | * Publishes a message. 310 | */ 311 | public function publish(Message $message): ExtendedPromiseInterface 312 | { 313 | if (!$this->isConnected) { 314 | return new RejectedPromise(new LogicException('The client is not connected.')); 315 | } 316 | 317 | return $this->startFlow($this->flowFactory->buildOutgoingPublishFlow($message)); 318 | } 319 | 320 | /** 321 | * Calls the given generator periodically and publishes the return value. 322 | */ 323 | public function publishPeriodically(int $interval, Message $message, callable $generator): ExtendedPromiseInterface 324 | { 325 | if (!$this->isConnected) { 326 | return new RejectedPromise(new LogicException('The client is not connected.')); 327 | } 328 | 329 | $deferred = new Deferred(); 330 | 331 | $this->timer[] = $this->loop->addPeriodicTimer( 332 | $interval, 333 | function () use ($message, $generator, $deferred) { 334 | $this->publish($message->withPayload((string) $generator($message->getTopic())))->then( 335 | static function ($value) use ($deferred) { 336 | $deferred->notify($value); 337 | }, 338 | static function (Throwable $reason) use ($deferred) { 339 | $deferred->reject($reason); 340 | } 341 | ); 342 | } 343 | ); 344 | 345 | return $deferred->promise(); 346 | } 347 | 348 | /** 349 | * Emits warnings. 350 | */ 351 | private function emitWarning(Throwable $error): void 352 | { 353 | $this->emit('warning', [$error, $this]); 354 | } 355 | 356 | /** 357 | * Emits errors. 358 | */ 359 | private function emitError(Throwable $error): void 360 | { 361 | $this->emit('error', [$error, $this]); 362 | } 363 | 364 | /** 365 | * Establishes a network connection to a server. 366 | */ 367 | private function establishConnection(string $host, int $port, int $timeout): ExtendedPromiseInterface 368 | { 369 | $deferred = new Deferred(); 370 | 371 | $future = null; 372 | $timer = $this->loop->addTimer( 373 | $timeout, 374 | static function () use ($deferred, $timeout, &$future) { 375 | $exception = new RuntimeException(sprintf('Connection timed out after %d seconds.', $timeout)); 376 | $deferred->reject($exception); 377 | if ($future instanceof CancellablePromiseInterface) { 378 | $future->cancel(); 379 | } 380 | $future = null; 381 | } 382 | ); 383 | 384 | $future = $this->connector->connect($host.':'.$port) 385 | ->always(function () use ($timer) { 386 | $this->loop->cancelTimer($timer); 387 | }) 388 | ->then(function (DuplexStreamInterface $stream) use ($deferred) { 389 | $stream->on('data', function ($data) { 390 | $this->handleReceive($data); 391 | }); 392 | 393 | $stream->on('close', function () { 394 | $this->handleClose(); 395 | }); 396 | 397 | $stream->on('error', function (Throwable $error) { 398 | $this->handleError($error); 399 | }); 400 | 401 | $deferred->resolve($stream); 402 | }) 403 | ->otherwise(static function (Throwable $reason) use ($deferred) { 404 | $deferred->reject($reason); 405 | }); 406 | 407 | return $deferred->promise(); 408 | } 409 | 410 | /** 411 | * Registers a new client with the broker. 412 | */ 413 | private function registerClient(Connection $connection, int $timeout): ExtendedPromiseInterface 414 | { 415 | $deferred = new Deferred(); 416 | 417 | $responseTimer = $this->loop->addTimer( 418 | $timeout, 419 | static function () use ($deferred, $timeout) { 420 | $exception = new RuntimeException(sprintf('No response after %d seconds.', $timeout)); 421 | $deferred->reject($exception); 422 | } 423 | ); 424 | 425 | $this->startFlow($this->flowFactory->buildOutgoingConnectFlow($connection), true) 426 | ->always(function () use ($responseTimer) { 427 | $this->loop->cancelTimer($responseTimer); 428 | })->then(function ($result) use ($connection, $deferred) { 429 | $this->timer[] = $this->loop->addPeriodicTimer( 430 | floor($connection->getKeepAlive() * 0.75), 431 | function () { 432 | $this->startFlow($this->flowFactory->buildOutgoingPingFlow()); 433 | } 434 | ); 435 | 436 | $deferred->resolve($result ?: $connection); 437 | })->otherwise(static function (Throwable $reason) use ($deferred) { 438 | $deferred->reject($reason); 439 | }); 440 | 441 | return $deferred->promise(); 442 | } 443 | 444 | /** 445 | * Handles incoming data. 446 | */ 447 | private function handleReceive(string $data): void 448 | { 449 | if (!$this->isConnected && !$this->isConnecting) { 450 | return; 451 | } 452 | 453 | $flowCount = count($this->receivingFlows); 454 | 455 | $packets = $this->parser->push($data); 456 | foreach ($packets as $packet) { 457 | $this->handlePacket($packet); 458 | } 459 | 460 | if ($flowCount > count($this->receivingFlows)) { 461 | $this->receivingFlows = array_values($this->receivingFlows); 462 | } 463 | 464 | $this->handleSend(); 465 | } 466 | 467 | /** 468 | * Handles an incoming packet. 469 | */ 470 | private function handlePacket(Packet $packet): void 471 | { 472 | switch ($packet->getPacketType()) { 473 | case Packet::TYPE_PUBLISH: 474 | if (!($packet instanceof PublishRequestPacket)) { 475 | throw new RuntimeException(sprintf('Expected %s but got %s.', PublishRequestPacket::class, get_class($packet))); 476 | } 477 | 478 | $message = new DefaultMessage( 479 | $packet->getTopic(), 480 | $packet->getPayload(), 481 | $packet->getQosLevel(), 482 | $packet->isRetained(), 483 | $packet->isDuplicate() 484 | ); 485 | 486 | $this->startFlow($this->flowFactory->buildIncomingPublishFlow($message, $packet->getIdentifier())); 487 | break; 488 | case Packet::TYPE_CONNACK: 489 | case Packet::TYPE_PINGRESP: 490 | case Packet::TYPE_SUBACK: 491 | case Packet::TYPE_UNSUBACK: 492 | case Packet::TYPE_PUBREL: 493 | case Packet::TYPE_PUBACK: 494 | case Packet::TYPE_PUBREC: 495 | case Packet::TYPE_PUBCOMP: 496 | $flowFound = false; 497 | foreach ($this->receivingFlows as $index => $flow) { 498 | if ($flow->accept($packet)) { 499 | $flowFound = true; 500 | 501 | unset($this->receivingFlows[$index]); 502 | $this->continueFlow($flow, $packet); 503 | 504 | break; 505 | } 506 | } 507 | 508 | if (!$flowFound) { 509 | $this->emitWarning( 510 | new LogicException(sprintf('Received unexpected packet of type %d.', $packet->getPacketType())) 511 | ); 512 | } 513 | break; 514 | default: 515 | $this->emitWarning( 516 | new LogicException(sprintf('Cannot handle packet of type %d.', $packet->getPacketType())) 517 | ); 518 | } 519 | } 520 | 521 | /** 522 | * Handles outgoing packets. 523 | */ 524 | private function handleSend(): void 525 | { 526 | $flow = null; 527 | if ($this->writtenFlow !== null) { 528 | $flow = $this->writtenFlow; 529 | $this->writtenFlow = null; 530 | } 531 | 532 | if (count($this->sendingFlows) > 0) { 533 | $this->writtenFlow = array_shift($this->sendingFlows); 534 | $this->stream->write($this->writtenFlow->getPacket()); 535 | } 536 | 537 | if ($flow !== null) { 538 | if ($flow->isFinished()) { 539 | $this->loop->futureTick(function () use ($flow) { 540 | $this->finishFlow($flow); 541 | }); 542 | } else { 543 | $this->receivingFlows[] = $flow; 544 | } 545 | } 546 | } 547 | 548 | /** 549 | * Handles closing of the stream. 550 | */ 551 | private function handleClose(): void 552 | { 553 | foreach ($this->timer as $timer) { 554 | $this->loop->cancelTimer($timer); 555 | } 556 | 557 | $connection = $this->connection; 558 | 559 | $this->isConnecting = false; 560 | $this->isDisconnecting = false; 561 | $this->isConnected = false; 562 | $this->connection = null; 563 | $this->stream = null; 564 | 565 | if ($this->onCloseCallback !== null) { 566 | call_user_func($this->onCloseCallback, $connection); 567 | $this->onCloseCallback = null; 568 | } 569 | 570 | if ($connection !== null) { 571 | $this->emit('close', [$connection, $this]); 572 | } 573 | } 574 | 575 | /** 576 | * Handles errors of the stream. 577 | */ 578 | private function handleError(Throwable $error): void 579 | { 580 | $this->emitError($error); 581 | } 582 | 583 | /** 584 | * Starts the given flow. 585 | */ 586 | private function startFlow(Flow $flow, bool $isSilent = false): ExtendedPromiseInterface 587 | { 588 | try { 589 | $packet = $flow->start(); 590 | } catch (Throwable $t) { 591 | $this->emitError($t); 592 | 593 | return new RejectedPromise($t); 594 | } 595 | 596 | $deferred = new Deferred(); 597 | $internalFlow = new ReactFlow($flow, $deferred, $packet, $isSilent); 598 | 599 | if ($packet !== null) { 600 | if ($this->writtenFlow !== null) { 601 | $this->sendingFlows[] = $internalFlow; 602 | } else { 603 | $this->stream->write($packet); 604 | $this->writtenFlow = $internalFlow; 605 | $this->handleSend(); 606 | } 607 | } else { 608 | $this->loop->futureTick(function () use ($internalFlow) { 609 | $this->finishFlow($internalFlow); 610 | }); 611 | } 612 | 613 | return $deferred->promise(); 614 | } 615 | 616 | /** 617 | * Continues the given flow. 618 | */ 619 | private function continueFlow(ReactFlow $flow, Packet $packet): void 620 | { 621 | try { 622 | $response = $flow->next($packet); 623 | } catch (Throwable $t) { 624 | $this->emitError($t); 625 | 626 | return; 627 | } 628 | 629 | if ($response !== null) { 630 | if ($this->writtenFlow !== null) { 631 | $this->sendingFlows[] = $flow; 632 | } else { 633 | $this->stream->write($response); 634 | $this->writtenFlow = $flow; 635 | $this->handleSend(); 636 | } 637 | } elseif ($flow->isFinished()) { 638 | $this->loop->futureTick(function () use ($flow) { 639 | $this->finishFlow($flow); 640 | }); 641 | } 642 | } 643 | 644 | /** 645 | * Finishes the given flow. 646 | */ 647 | private function finishFlow(ReactFlow $flow): void 648 | { 649 | if ($flow->isSuccess()) { 650 | if (!$flow->isSilent()) { 651 | $this->emit($flow->getCode(), [$flow->getResult(), $this]); 652 | } 653 | 654 | $flow->getDeferred()->resolve($flow->getResult()); 655 | } else { 656 | $result = new RuntimeException($flow->getErrorMessage()); 657 | $this->emitWarning($result); 658 | 659 | $flow->getDeferred()->reject($result); 660 | } 661 | } 662 | } 663 | --------------------------------------------------------------------------------