├── .gitignore
├── src
├── Transport
│ ├── TransportProviderInterface.php
│ ├── AbstractClientTransportProvider.php
│ ├── ClientTransportProviderInterface.php
│ ├── TransportInterface.php
│ └── AbstractTransport.php
├── Peer
│ ├── PeerInterface.php
│ ├── ClientInterface.php
│ └── Client.php
├── Logging
│ ├── ConsoleLogger.php
│ └── Logger.php
├── Role
│ ├── AbstractRole.php
│ ├── Publisher.php
│ ├── Caller.php
│ ├── Subscriber.php
│ └── Callee.php
├── Authentication
│ ├── ClientAuthenticationInterface.php
│ └── ClientWampCraAuthenticator.php
├── Result.php
├── ClientSession.php
├── CallResult.php
├── Connection.php
└── AbstractSession.php
├── test
├── bootstrap.php
├── Transport
│ └── ClientTestTransportProvider.php
└── Peer
│ └── ClientTest.php
├── phpunit.xml.dist
├── composer.json
├── .travis.yml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | .idea
3 | composer.lock
4 | nbproject
--------------------------------------------------------------------------------
/src/Transport/TransportProviderInterface.php:
--------------------------------------------------------------------------------
1 | addPsr4('Thruway\\', __DIR__);
9 | } else {
10 | throw new RuntimeException('Install dependencies to run test suite.');
11 | }
12 |
13 | Logger::set(new NullLogger());
14 |
--------------------------------------------------------------------------------
/test/Transport/ClientTestTransportProvider.php:
--------------------------------------------------------------------------------
1 |
2 |
92 | // * Whether a offset exists
93 | // * @link http://php.net/manual/en/arrayaccess.offsetexists.php
94 | // * @param mixed $offset
95 | // * An offset to check for. 96 | // *
97 | // * @return boolean true on success or false on failure. 98 | // * 99 | // *
100 | // * The return value will be casted to boolean if non-boolean was returned.
101 | // */
102 | // public function offsetExists($offset)
103 | // {
104 | // $args = $this->getArguments();
105 | //
106 | // if ($args === null) return false;
107 | //
108 | // return isset($args[$offset]);
109 | // }
110 | //
111 | // /**
112 | // * (PHP 5 >= 5.0.0)
113 | // * Offset to retrieve
114 | // * @link http://php.net/manual/en/arrayaccess.offsetget.php
115 | // * @param mixed $offset
116 | // * The offset to retrieve. 117 | // *
118 | // * @return mixed Can return all value types. 119 | // */ 120 | // public function offsetGet($offset) 121 | // { 122 | // $args = $this->getArguments(); 123 | // 124 | // return $args[$offset]; 125 | // } 126 | // 127 | // /** 128 | // * (PHP 5 >= 5.0.0)132 | // * The offset to assign the value to. 133 | // *
134 | // * @param mixed $value135 | // * The value to set. 136 | // *
137 | // * @return void 138 | // */ 139 | // public function offsetSet($offset, $value) 140 | // { 141 | // if ($offset === null) { 142 | // $this->getArguments()[] = $value; 143 | // } else { 144 | // $this->getArguments()[$offset] = $value; 145 | // } 146 | // } 147 | // 148 | // /** 149 | // * (PHP 5 >= 5.0.0)153 | // * The offset to unset. 154 | // *
155 | // * @return void 156 | // */ 157 | // public function offsetUnset($offset) 158 | // { 159 | // unset($this->getArguments()[$offset]); 160 | // } 161 | 162 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://travis-ci.org/voryx/Thruway) 2 | 3 | Thruway 4 | =========== 5 | 6 | Thruway Client is an open source client for [Thruway](https://github.com/voryx/Thruway) and the [WAMP (Web Application Messaging Protocol)](http://wamp-proto.org/), for PHP. 7 | 8 | Thruway uses ([reactphp](http://reactphp.org/)); an event-driven, non-blocking I/O model, perfect for modern real-time applications. 9 | 10 | ### Supported WAMP Features 11 | 12 | **Basic Spec** [read more](https://github.com/tavendo/WAMP/blob/master/spec/basic.md) 13 | * Publish and Subscribe 14 | * Remote Procedure Calls 15 | * Websocket Transport 16 | * Internal Transport\* 17 | * JSON serialization 18 | 19 | **Advanced Spec** [read more](https://github.com/tavendo/WAMP/blob/master/spec/advanced.md) 20 | * RawSocket Transport 21 | * Authentication 22 | * WAMP Challenge-Response Authentication 23 | * Custom Authentication Methods 24 | * Publish & Subscribe 25 | * Subscriber Black and Whitelisting 26 | * Publisher Exclusion 27 | * Publisher Identification 28 | * Remote Procedure Calls 29 | * Caller Identification 30 | * Progressive Call Results 31 | * Caller Exclusion 32 | * Canceling Calls 33 | 34 | \* _Thruway specific features_ 35 | 36 | 37 | 38 | Requirements 39 | ------------ 40 | 41 | Thruway Client is only supported on PHP 5.6 and up. 42 | 43 | ### Quick Start with Composer 44 | 45 | The below instructions actually install the Thruway Router and Client for test purposes. 46 | The client can also be installed without the router in your own project. 47 | 48 | Create a directory for the test project 49 | 50 | $ mkdir thruway 51 | 52 | Switch to the new directory 53 | 54 | $ cd thruway 55 | 56 | Download Composer [more info](https://getcomposer.org/doc/00-intro.md#downloading-the-composer-executable) 57 | 58 | $ curl -sS https://getcomposer.org/installer | php 59 | 60 | Download Thruway and dependencies 61 | 62 | $ php composer.phar require voryx/thruway 63 | 64 | Start the WAMP router 65 | 66 | $ php vendor/voryx/thruway/Examples/SimpleWsRouter.php 67 | 68 | Thruway is now running on 127.0.0.1 port 9090 69 | 70 | ### PHP Client Example 71 | 72 | ```php 73 | addTransportProvider(new PawlTransportProvider("ws://127.0.0.1:9090/")); 83 | 84 | $client->on('open', function (ClientSession $session) { 85 | 86 | // 1) subscribe to a topic 87 | $onevent = function ($args) { 88 | echo "Event {$args[0]}\n"; 89 | }; 90 | $session->subscribe('com.myapp.hello', $onevent); 91 | 92 | // 2) publish an event 93 | $session->publish('com.myapp.hello', ['Hello, world from PHP!!!'], [], ["acknowledge" => true])->then( 94 | function () { 95 | echo "Publish Acknowledged!\n"; 96 | }, 97 | function ($error) { 98 | // publish failed 99 | echo "Publish Error {$error}\n"; 100 | } 101 | ); 102 | 103 | // 3) register a procedure for remoting 104 | $add2 = function ($args) { 105 | return $args[0] + $args[1]; 106 | }; 107 | $session->register('com.myapp.add2', $add2); 108 | 109 | // 4) call a remote procedure 110 | $session->call('com.myapp.add2', [2, 3])->then( 111 | function ($res) { 112 | echo "Result: {$res}\n"; 113 | }, 114 | function ($error) { 115 | echo "Call Error: {$error}\n"; 116 | } 117 | ); 118 | }); 119 | 120 | 121 | $client->start(); 122 | ``` 123 | 124 | ### Javascript Clients 125 | 126 | You can also use [AutobahnJS](https://github.com/tavendo/AutobahnJS) or any other WAMPv2 compatible client. 127 | 128 | Here are some [examples] (https://github.com/tavendo/AutobahnJS#show-me-some-code) 129 | 130 | Here's a [plunker](http://plnkr.co/edit/8vcBDUzIhp48JtuTGIaj?p=info) that will allow you to run some tests against a local router 131 | 132 | For AngularJS on the frontend, use the [Angular WAMP](https://github.com/voryx/angular-wamp) wrapper. 133 | 134 | -------------------------------------------------------------------------------- /src/Logging/Logger.php: -------------------------------------------------------------------------------- 1 | log($level, $message, $context); 44 | } 45 | 46 | /** 47 | * @param null $object 48 | * @param null $object 49 | * @param $message 50 | * @param array $context 51 | * @return null 52 | */ 53 | public static function alert($object, $message, $context = []) 54 | { 55 | static::log($object, LogLevel::ALERT, $message, $context); 56 | } 57 | 58 | /** 59 | * @param null $object 60 | * @param $message 61 | * @param array $context 62 | * @return null 63 | */ 64 | public static function critical($object, $message, $context = []) 65 | { 66 | static::log($object, LogLevel::CRITICAL, $message, $context); 67 | } 68 | 69 | /** 70 | * @param null $object 71 | * @param $message 72 | * @param array $context 73 | * @return null 74 | */ 75 | public static function debug($object, $message, $context = []) 76 | { 77 | static::log($object, LogLevel::DEBUG, $message, $context); 78 | } 79 | 80 | /** 81 | * @param null $object 82 | * @param $message 83 | * @param array $context 84 | * @return null 85 | */ 86 | public static function emergency($object, $message, $context = []) 87 | { 88 | static::log($object, LogLevel::EMERGENCY, $message, $context); 89 | } 90 | 91 | /** 92 | * @param null $object 93 | * @param $message 94 | * @param array $context 95 | * @return null 96 | */ 97 | public static function error($object, $message, $context = []) 98 | { 99 | static::log($object, LogLevel::ERROR, $message, $context); 100 | } 101 | 102 | /** 103 | * @param null $object 104 | * @param $message 105 | * @param array $context 106 | * @return null 107 | */ 108 | public static function info($object, $message, $context = []) 109 | { 110 | static::log($object, LogLevel::INFO, $message, $context); 111 | } 112 | 113 | /** 114 | * @param null $object 115 | * @param $message 116 | * @param array $context 117 | * @return null 118 | */ 119 | public static function notice($object, $message, $context = []) 120 | { 121 | static::log($object, LogLevel::NOTICE, $message, $context); 122 | } 123 | 124 | /** 125 | * @param $message 126 | * @param array $context 127 | * @return null 128 | */ 129 | public static function warning($object, $message, $context = []) 130 | { 131 | static::log($object, LogLevel::WARNING, $message, $context); 132 | } 133 | 134 | /** 135 | * Protected constructor to prevent creating a new instance of the 136 | * *Singleton* via the `new` operator from outside of this class. 137 | */ 138 | protected function __construct() 139 | { 140 | } 141 | 142 | /** 143 | * Private clone method to prevent cloning of the instance of the 144 | * *Singleton* instance. 145 | * 146 | * @return void 147 | */ 148 | private function __clone() 149 | { 150 | } 151 | 152 | /** 153 | * Private unserialize method to prevent unserializing of the *Singleton* 154 | * instance. 155 | * 156 | * @return void 157 | */ 158 | public function __wakeup() 159 | { 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Role/Publisher.php: -------------------------------------------------------------------------------- 1 | publishRequests = []; 33 | } 34 | 35 | /** 36 | * Return supported features 37 | * 38 | * @return \stdClass 39 | */ 40 | public function getFeatures() { 41 | $features = new \stdClass(); 42 | 43 | $features->subscriber_blackwhite_listing = true; 44 | $features->publisher_exclusion = true; 45 | 46 | return $features; 47 | } 48 | 49 | /** 50 | * handle received message 51 | * 52 | * @param \Thruway\AbstractSession $session 53 | * @param \Thruway\Message\Message $msg 54 | * @return void 55 | */ 56 | public function onMessage(AbstractSession $session, Message $msg) 57 | { 58 | if ($msg instanceof PublishedMessage): 59 | $this->processPublished($msg); 60 | elseif ($msg instanceof ErrorMessage): 61 | $this->processError($msg); 62 | else: 63 | $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg)); 64 | endif; 65 | } 66 | 67 | /** 68 | * process PublishedMesage 69 | * 70 | * @param \Thruway\Message\PublishedMessage $msg 71 | */ 72 | protected function processPublished(PublishedMessage $msg) 73 | { 74 | if (isset($this->publishRequests[$msg->getRequestId()])) { 75 | /* @var $futureResult Deferred */ 76 | $futureResult = $this->publishRequests[$msg->getRequestId()]['future_result']; 77 | $futureResult->resolve($msg->getPublicationId()); 78 | unset($this->publishRequests[$msg->getRequestId()]); 79 | } 80 | } 81 | 82 | /** 83 | * process error 84 | * 85 | * @param \Thruway\Message\ErrorMessage $msg 86 | */ 87 | protected function processError(ErrorMessage $msg) 88 | { 89 | if (isset($this->publishRequests[$msg->getRequestId()])) { 90 | /* @var $futureResult Deferred */ 91 | $futureResult = $this->publishRequests[$msg->getRequestId()]['future_result']; 92 | $futureResult->reject($msg); 93 | unset($this->publishRequests[$msg->getRequestId()]); 94 | } 95 | } 96 | 97 | /** 98 | * Handle message 99 | * 100 | * @param \Thruway\Message\Message $msg 101 | * @return boolean 102 | */ 103 | public function handlesMessage(Message $msg) 104 | { 105 | $handledMsgCodes = [ 106 | Message::MSG_PUBLISHED, 107 | ]; 108 | 109 | if (in_array($msg->getMsgCode(), $handledMsgCodes, true)) { 110 | return true; 111 | } elseif ($msg instanceof ErrorMessage && $msg->getErrorMsgCode() === Message::MSG_PUBLISH) { 112 | return true; 113 | } else { 114 | return false; 115 | } 116 | } 117 | 118 | /** 119 | * process publish 120 | * 121 | * @param \Thruway\ClientSession $session 122 | * @param string $topicName 123 | * @param mixed $arguments 124 | * @param mixed $argumentsKw 125 | * @param mixed $options 126 | * @return \React\Promise\Promise 127 | */ 128 | public function publish(ClientSession $session, $topicName, $arguments, $argumentsKw, $options) 129 | { 130 | $options = (object)$options; 131 | 132 | $requestId = Utils::getUniqueId(); 133 | 134 | if (isset($options->acknowledge) && $options->acknowledge === true) { 135 | $futureResult = new Deferred(); 136 | $this->publishRequests[$requestId] = ['future_result' => $futureResult]; 137 | } 138 | 139 | 140 | $publishMsg = new PublishMessage($requestId, $options, $topicName, $arguments, $argumentsKw); 141 | 142 | $session->sendMessage($publishMsg); 143 | 144 | return isset($futureResult) ? $futureResult->promise() : false; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | options = $options; 53 | $this->client = new Client($options['realm'], $loop); 54 | $url = isset($options['url']) ? $options['url'] : null; 55 | $pawlTransport = new PawlTransportProvider($url, $connector); 56 | 57 | $this->client->addTransportProvider($pawlTransport); 58 | $this->client->setReconnectOptions($options); 59 | 60 | //Set Authid 61 | if (isset($options['authid'])) { 62 | $this->client->setAuthId($options['authid']); 63 | } 64 | 65 | //Register Handlers 66 | $this->handleOnChallenge(); 67 | $this->handleOnOpen(); 68 | $this->handleOnClose(); 69 | $this->handleOnError(); 70 | } 71 | 72 | /** 73 | * @deprecated 74 | * Process events at a set interval 75 | * 76 | * @param int $timer 77 | */ 78 | public function doEvents($timer = 1) 79 | { 80 | /*$loop = $this->getClient()->getLoop(); 81 | 82 | $looping = true; 83 | $loop->addTimer($timer, function () use (&$looping) { 84 | $looping = false; 85 | }); 86 | 87 | while ($looping) { 88 | usleep(1000); 89 | $loop->tick(); 90 | }*/ 91 | } 92 | 93 | /** 94 | * Starts the open sequence 95 | * @param bool $startLoop 96 | */ 97 | public function open($startLoop = true) 98 | { 99 | $this->client->start($startLoop); 100 | } 101 | 102 | /** 103 | * Starts the close sequence 104 | */ 105 | public function close() 106 | { 107 | $this->client->setAttemptRetry(false); 108 | $this->transport->close(); 109 | } 110 | 111 | /** 112 | * @return Client 113 | */ 114 | public function getClient() 115 | { 116 | return $this->client; 117 | } 118 | 119 | /** 120 | * Handle On Open event 121 | */ 122 | 123 | private function handleOnOpen() 124 | { 125 | $this->client->on('open', function (ClientSession $session, TransportInterface $transport, $details) { 126 | $this->transport = $transport; 127 | $this->emit('open', [$session, $transport, $details]); 128 | }); 129 | } 130 | 131 | /** 132 | * Handle On Close event 133 | */ 134 | private function handleOnClose() 135 | { 136 | $this->client->on('close', function ($reason) { 137 | $this->emit('close', [$reason]); 138 | }); 139 | 140 | if (isset($this->options['onClose']) && is_callable($this->options['onClose'])) { 141 | $this->on('close', $this->options['onClose']); 142 | } 143 | } 144 | 145 | /** 146 | * Handle On Error event 147 | */ 148 | private function handleOnError() 149 | { 150 | $this->client->on('error', function () { 151 | $this->emit('error', func_get_args()); 152 | }); 153 | } 154 | 155 | /** 156 | * Setup the onChallenge callback 157 | */ 158 | private function handleOnChallenge() 159 | { 160 | 161 | $options = $this->options; 162 | 163 | if (isset($options['onChallenge']) && is_callable($options['onChallenge']) 164 | && isset($options['authmethods']) 165 | && is_array($options['authmethods']) 166 | ) { 167 | $this->client->setAuthMethods($options['authmethods']); 168 | 169 | $this->client->on('challenge', function (ClientSession $session, ChallengeMessage $msg) use ($options) { 170 | $token = call_user_func($options['onChallenge'], $session, $msg->getAuthMethod(), $msg); 171 | $session->sendMessage(new AuthenticateMessage($token)); 172 | }); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/AbstractSession.php: -------------------------------------------------------------------------------- 1 | state = $state; 84 | } 85 | 86 | /** 87 | * Get client state 88 | * 89 | * @return int 90 | */ 91 | public function getState() 92 | { 93 | return $this->state; 94 | } 95 | 96 | /** 97 | * Set athentication state (authenticated or not) 98 | * 99 | * @param boolean $authenticated 100 | */ 101 | public function setAuthenticated($authenticated) 102 | { 103 | $this->authenticated = $authenticated; 104 | } 105 | 106 | /** 107 | * Get authentication state (authenticated or not) 108 | * 109 | * @return boolean 110 | */ 111 | public function getAuthenticated() 112 | { 113 | return $this->authenticated; 114 | } 115 | 116 | /** 117 | * check is authenticated 118 | * 119 | * @return boolean 120 | */ 121 | public function isAuthenticated() 122 | { 123 | return $this->getAuthenticated(); 124 | } 125 | 126 | /** 127 | * Set realm 128 | * 129 | * @param \Thruway\Realm $realm 130 | */ 131 | public function setRealm($realm) 132 | { 133 | $this->realm = $realm; 134 | } 135 | 136 | /** 137 | * Get realm 138 | * 139 | * @return \Thruway\Realm 140 | */ 141 | public function getRealm() 142 | { 143 | return $this->realm; 144 | } 145 | 146 | 147 | /** 148 | * Get session ID 149 | * 150 | * @return int 151 | */ 152 | public function getSessionId() 153 | { 154 | return $this->sessionId; 155 | } 156 | 157 | /** 158 | * Get transport 159 | * 160 | * @return \Thruway\Transport\TransportInterface 161 | */ 162 | public function getTransport() 163 | { 164 | return $this->transport; 165 | } 166 | 167 | /** 168 | * Check sent Goodbye message 169 | * 170 | * @return boolean 171 | */ 172 | public function isGoodbyeSent() 173 | { 174 | return $this->goodbyeSent; 175 | } 176 | 177 | /** 178 | * Set state sent goodbye message ? 179 | * 180 | * @param boolean $goodbyeSent 181 | */ 182 | public function setGoodbyeSent($goodbyeSent) 183 | { 184 | $this->goodbyeSent = $goodbyeSent; 185 | } 186 | 187 | /** 188 | * Ping 189 | * 190 | * @param int $timeout 191 | * @return \React\Promise\Promise 192 | */ 193 | public function ping($timeout = 5) 194 | { 195 | return timeout($this->getTransport()->ping(), $timeout, $this->loop); 196 | } 197 | 198 | /** 199 | * process abort request 200 | * 201 | * @param mixed $details 202 | * @param mixed $responseURI 203 | * @throws \Exception 204 | */ 205 | public function abort($details = null, $responseURI = null) 206 | { 207 | if ($this->isAuthenticated()) { 208 | throw new \Exception("Session::abort called after we are authenticated"); 209 | } 210 | 211 | $abortMsg = new AbortMessage($details, $responseURI); 212 | 213 | $this->sendMessage($abortMsg); 214 | 215 | $this->shutdown(); 216 | } 217 | 218 | /** 219 | * Process Shutdown session 220 | */ 221 | public function shutdown() 222 | { 223 | // we want to immediately remove 224 | // all references 225 | 226 | $this->onClose(); 227 | 228 | $this->transport->close(); 229 | } 230 | 231 | /** 232 | * Set loop 233 | * 234 | * @param \React\EventLoop\LoopInterface $loop 235 | */ 236 | public function setLoop($loop) 237 | { 238 | $this->loop = $loop; 239 | } 240 | 241 | /** 242 | * Get loop 243 | * 244 | * @return \React\EventLoop\LoopInterface 245 | */ 246 | public function getLoop() 247 | { 248 | return $this->loop; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/Role/Caller.php: -------------------------------------------------------------------------------- 1 | callRequests = []; 36 | } 37 | 38 | /** 39 | * Return supported features 40 | * 41 | * @return \stdClass 42 | */ 43 | public function getFeatures() 44 | { 45 | $features = new \stdClass(); 46 | 47 | $features->caller_identification = true; 48 | $features->progressive_call_results = true; 49 | $features->call_canceling = true; 50 | 51 | return $features; 52 | } 53 | 54 | /** 55 | * process message 56 | * 57 | * @param \Thruway\AbstractSession $session 58 | * @param \Thruway\Message\Message $msg 59 | * @return void 60 | */ 61 | public function onMessage(AbstractSession $session, Message $msg) 62 | { 63 | 64 | if ($msg instanceof ResultMessage): 65 | $this->processResult($msg); 66 | elseif ($msg instanceof ErrorMessage): 67 | $this->processError($msg); 68 | else: 69 | $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg)); 70 | endif; 71 | } 72 | 73 | /** 74 | * Process ResultMessage 75 | * 76 | * @param \Thruway\Message\ResultMessage $msg 77 | */ 78 | protected function processResult(ResultMessage $msg) 79 | { 80 | if (isset($this->callRequests[$msg->getRequestId()])) { 81 | /* @var $futureResult Deferred */ 82 | $futureResult = $this->callRequests[$msg->getRequestId()]['future_result']; 83 | 84 | $callResult = new CallResult($msg); 85 | 86 | $details = $msg->getDetails(); 87 | if (is_object($details) && isset($details->progress) && $details->progress) { 88 | // TODO: what if we didn't want progress? 89 | $futureResult->progress($callResult); 90 | } else { 91 | $futureResult->resolve($callResult); 92 | unset($this->callRequests[$msg->getRequestId()]); 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * Process ErrorMessage 99 | * 100 | * @param \Thruway\Message\ErrorMessage $msg 101 | */ 102 | protected function processError(ErrorMessage $msg) 103 | { 104 | switch ($msg->getErrorMsgCode()) { 105 | case Message::MSG_CALL: 106 | if (isset($this->callRequests[$msg->getRequestId()])) { 107 | /* @var $futureResult Deferred */ 108 | $futureResult = $this->callRequests[$msg->getRequestId()]['future_result']; 109 | $futureResult->reject($msg); 110 | unset($this->callRequests[$msg->getRequestId()]); 111 | } 112 | break; 113 | } 114 | } 115 | 116 | /** 117 | * handle message 118 | * Returns true if this role handles this message. 119 | * 120 | * @param \Thruway\Message\Message $msg 121 | * @return boolean 122 | */ 123 | public function handlesMessage(Message $msg) 124 | { 125 | $handledMsgCodes = [ 126 | Message::MSG_RESULT, 127 | ]; 128 | 129 | if (in_array($msg->getMsgCode(), $handledMsgCodes)) { 130 | return true; 131 | } elseif ($msg instanceof ErrorMessage && $msg->getErrorMsgCode() === Message::MSG_CALL) { 132 | return true; 133 | } else { 134 | return false; 135 | } 136 | } 137 | 138 | /** 139 | * process call 140 | * 141 | * @param \Thruway\ClientSession $session 142 | * @param string $procedureName 143 | * @param mixed $arguments 144 | * @param mixed $argumentsKw 145 | * @param mixed $options 146 | * @return \React\Promise\Promise 147 | */ 148 | public function call(ClientSession $session, $procedureName, $arguments = null, $argumentsKw = null, $options = null) 149 | { 150 | $requestId = Utils::getUniqueId(); 151 | 152 | //This promise gets resolved in Caller::processResult 153 | $futureResult = new Deferred(function () use ($session, $requestId) { 154 | $session->sendMessage(new CancelMessage($requestId, (object)[])); 155 | }); 156 | 157 | $this->callRequests[$requestId] = [ 158 | 'procedure_name' => $procedureName, 159 | 'future_result' => $futureResult 160 | ]; 161 | 162 | if (is_array($options)) { 163 | $options = (object) $options; 164 | } 165 | 166 | if (!is_object($options)) { 167 | if ($options !== null) { 168 | Logger::warning($this, "Options don't appear to be the correct type."); 169 | } 170 | $options = new \stdClass(); 171 | } 172 | 173 | $callMsg = new CallMessage($requestId, $options, $procedureName, $arguments, $argumentsKw); 174 | 175 | $session->sendMessage($callMsg); 176 | 177 | return $futureResult->promise(); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Role/Subscriber.php: -------------------------------------------------------------------------------- 1 | subscriptions = []; 37 | } 38 | 39 | /** 40 | * Return supported features 41 | * 42 | * @return \stdClass 43 | */ 44 | public function getFeatures() 45 | { 46 | $features = new \stdClass(); 47 | 48 | // $features->subscriber_metaevents = true; 49 | 50 | return $features; 51 | } 52 | 53 | /** 54 | * Handle on recieved message 55 | * 56 | * @param \Thruway\AbstractSession $session 57 | * @param \Thruway\Message\Message $msg 58 | * @return void 59 | */ 60 | public function onMessage(AbstractSession $session, Message $msg) 61 | { 62 | if ($msg instanceof SubscribedMessage): 63 | $this->processSubscribed($session, $msg); 64 | elseif ($msg instanceof UnsubscribedMessage): 65 | $this->processUnsubscribed($session, $msg); 66 | elseif ($msg instanceof EventMessage): 67 | $this->processEvent($session, $msg); 68 | elseif ($msg instanceof ErrorMessage): 69 | $this->processError($session, $msg); 70 | else: 71 | $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg)); 72 | endif; 73 | } 74 | 75 | /** 76 | * Process error 77 | * 78 | * @param \Thruway\AbstractSession $session 79 | * @param \Thruway\Message\ErrorMessage $msg 80 | */ 81 | protected function processError(AbstractSession $session, ErrorMessage $msg) 82 | { 83 | switch ($msg->getErrorMsgCode()) { 84 | case Message::MSG_SUBSCRIBE: 85 | $this->processSubscribeError($session, $msg); 86 | break; 87 | case Message::MSG_UNSUBSCRIBE: 88 | // TODO 89 | break; 90 | default: 91 | Logger::critical($this, 'Unhandled error'); 92 | } 93 | } 94 | 95 | /** 96 | * Process subscribe error 97 | * 98 | * @param \Thruway\AbstractSession $session 99 | * @param \Thruway\Message\ErrorMessage $msg 100 | */ 101 | protected function processSubscribeError(AbstractSession $session, ErrorMessage $msg) 102 | { 103 | foreach ($this->subscriptions as $key => $subscription) { 104 | if ($subscription['request_id'] === $msg->getErrorRequestId()) { 105 | // reject the promise 106 | $this->subscriptions[$key]['deferred']->reject($msg); 107 | 108 | unset($this->subscriptions[$key]); 109 | break; 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * process subscribed 116 | * 117 | * @param \Thruway\ClientSession $session 118 | * @param \Thruway\Message\SubscribedMessage $msg 119 | */ 120 | protected function processSubscribed(ClientSession $session, SubscribedMessage $msg) 121 | { 122 | foreach ($this->subscriptions as $key => $subscription) { 123 | if ($subscription['request_id'] === $msg->getRequestId()) { 124 | $this->subscriptions[$key]['subscription_id'] = $msg->getSubscriptionId(); 125 | $this->subscriptions[$key]['deferred']->resolve($msg); 126 | break; 127 | } 128 | } 129 | } 130 | 131 | /** 132 | * process unsubscribed 133 | * 134 | * @param \Thruway\ClientSession $session 135 | * @param \Thruway\Message\UnsubscribedMessage $msg 136 | */ 137 | protected function processUnsubscribed(ClientSession $session, UnsubscribedMessage $msg) 138 | { 139 | foreach ($this->subscriptions as $key => $subscription) { 140 | if (isset($subscription['unsubscribed_request_id']) && $subscription['unsubscribed_request_id'] === $msg->getRequestId()) { 141 | /* @var $deferred \React\Promise\Deferred */ 142 | $deferred = $subscription['unsubscribed_deferred']; 143 | $deferred->resolve(); 144 | 145 | unset($this->subscriptions[$key]); 146 | return; 147 | } 148 | } 149 | // $this->logger->error("---Got an Unsubscribed Message, but couldn't find corresponding request.\n"); 150 | } 151 | 152 | /** 153 | * Process event 154 | * 155 | * @param \Thruway\ClientSession $session 156 | * @param \Thruway\Message\EventMessage $msg 157 | */ 158 | protected function processEvent(ClientSession $session, EventMessage $msg) 159 | { 160 | foreach ($this->subscriptions as $key => $subscription) { 161 | if ($subscription['subscription_id'] === $msg->getSubscriptionId()) { 162 | call_user_func($subscription['callback'], 163 | $msg->getArguments(), $msg->getArgumentsKw(), $msg->getDetails(), $msg->getPublicationId()); 164 | break; 165 | } 166 | } 167 | } 168 | 169 | /** 170 | * Returns true if this role handles this message. 171 | * 172 | * @param \Thruway\Message\Message $msg 173 | * @return boolean 174 | */ 175 | public function handlesMessage(Message $msg) 176 | { 177 | $handledMsgCodes = [ 178 | Message::MSG_SUBSCRIBED, 179 | Message::MSG_UNSUBSCRIBED, 180 | Message::MSG_EVENT, 181 | Message::MSG_SUBSCRIBE, // for error handling 182 | Message::MSG_UNSUBSCRIBE // for error handling 183 | ]; 184 | 185 | $codeToCheck = $msg->getMsgCode(); 186 | 187 | if ($msg instanceof ErrorMessage) { 188 | $codeToCheck = $msg->getErrorMsgCode(); 189 | } 190 | 191 | return in_array($codeToCheck, $handledMsgCodes, true) ? true : false; 192 | } 193 | 194 | /** 195 | * process subscribe 196 | * 197 | * @param \Thruway\ClientSession $session 198 | * @param string $topicName 199 | * @param callable $callback 200 | * @param $options 201 | * @return Promise 202 | */ 203 | public function subscribe(ClientSession $session, $topicName, callable $callback, $options = null) 204 | { 205 | $requestId = Utils::getUniqueId(); 206 | $options = $options ? (object)$options : (object)[]; 207 | $deferred = new Deferred(); 208 | 209 | $subscription = [ 210 | 'topic_name' => $topicName, 211 | 'callback' => $callback, 212 | 'request_id' => $requestId, 213 | 'options' => $options, 214 | 'deferred' => $deferred 215 | ]; 216 | 217 | $this->subscriptions[] = $subscription; 218 | 219 | $subscribeMsg = new SubscribeMessage($requestId, $options, $topicName); 220 | $session->sendMessage($subscribeMsg); 221 | 222 | return $deferred->promise(); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/Peer/Client.php: -------------------------------------------------------------------------------- 1 | realm = $realm; 139 | $this->loop = $loop ? $loop : Factory::create(); 140 | $this->transportProvider = null; 141 | $this->roles = []; 142 | $this->authMethods = []; 143 | $this->session = null; 144 | $this->clientAuthenticators = []; 145 | $this->authId = 'anonymous'; 146 | 147 | $this->reconnectOptions = [ 148 | 'max_retries' => 15, 149 | 'initial_retry_delay' => 1.5, 150 | 'max_retry_delay' => 300, 151 | 'retry_delay_growth' => 1.5, 152 | 'retry_delay_jitter' => 0.1 //not implemented 153 | ]; 154 | 155 | $this->on('open', [$this, 'onSessionStart']); 156 | 157 | Logger::info($this, 'New client created'); 158 | 159 | } 160 | 161 | /** 162 | * @return string 163 | */ 164 | public function __toString() 165 | { 166 | return get_class($this); 167 | } 168 | 169 | /** 170 | * This is meant to be overridden so that the client can do its 171 | * thing 172 | * 173 | * @param \Thruway\ClientSession $session 174 | * @param \Thruway\Transport\TransportInterface $transport 175 | */ 176 | public function onSessionStart($session, $transport) 177 | { 178 | 179 | } 180 | 181 | /** 182 | * Add transport provider 183 | * 184 | * @param \Thruway\Transport\ClientTransportProviderInterface $transportProvider 185 | * @throws \Exception 186 | */ 187 | public function addTransportProvider(ClientTransportProviderInterface $transportProvider) 188 | { 189 | if ($this->transportProvider !== null) { 190 | throw new \Exception('You can only have one transport provider for a client'); 191 | } 192 | $this->transportProvider = $transportProvider; 193 | } 194 | 195 | /** 196 | * Set reconnect options 197 | * 198 | * @param array $reconnectOptions 199 | */ 200 | public function setReconnectOptions($reconnectOptions) 201 | { 202 | $this->reconnectOptions = array_merge($this->reconnectOptions, $reconnectOptions); 203 | } 204 | 205 | /** 206 | * Add client authenticator 207 | * 208 | * @param \Thruway\Authentication\ClientAuthenticationInterface $ca 209 | */ 210 | 211 | public function addClientAuthenticator(ClientAuthenticationInterface $ca) 212 | { 213 | $this->clientAuthenticators[] = $ca; 214 | 215 | $this->authMethods = array_merge($this->authMethods, $ca->getAuthMethods()); 216 | } 217 | 218 | /** 219 | * Start the transport 220 | * 221 | * @param boolean $startLoop 222 | * @throws \Exception 223 | */ 224 | public function start($startLoop = true) 225 | { 226 | if ($this->transportProvider === null) { 227 | throw new \Exception('You must add exactly one transport provider prior to starting'); 228 | } 229 | 230 | $this->transportProvider->startTransportProvider($this, $this->loop); 231 | 232 | if ($startLoop) { 233 | $this->loop->run(); 234 | } 235 | } 236 | 237 | /** 238 | * Handle open transport 239 | * 240 | * @param TransportInterface $transport 241 | */ 242 | public function onOpen(TransportInterface $transport) 243 | { 244 | $this->retryTimer = 0; 245 | $this->retryAttempts = 0; 246 | $this->transport = $transport; 247 | $session = new ClientSession($transport, $this); 248 | $this->session = $session; 249 | 250 | $session->setLoop($this->getLoop()); 251 | $session->setState(ClientSession::STATE_DOWN); 252 | 253 | $this->startSession($session); 254 | } 255 | 256 | /** 257 | * Start client session 258 | * 259 | * @param \Thruway\ClientSession $session 260 | */ 261 | public function startSession(ClientSession $session) 262 | { 263 | $this->addRole(new Callee()) 264 | ->addRole(new Caller()) 265 | ->addRole(new Publisher()) 266 | ->addRole(new Subscriber()); 267 | 268 | $details = (object)[ 269 | 'roles' => $this->getRoleInfoObject() 270 | ]; 271 | 272 | $details->authmethods = $this->authMethods; 273 | $details->authid = $this->authId; 274 | 275 | $session->setRealm($this->realm); 276 | 277 | $session->sendMessage(new HelloMessage($session->getRealm(), $details)); 278 | } 279 | 280 | /** 281 | * @return object 282 | */ 283 | public function getRoleInfoObject() 284 | { 285 | return (object)[ 286 | 'publisher' => (object)['features' => $this->getPublisher()->getFeatures()], 287 | 'subscriber' => (object)['features' => $this->getSubscriber()->getFeatures()], 288 | 'caller' => (object)['features' => $this->getCaller()->getFeatures()], 289 | 'callee' => (object)['features' => $this->getCallee()->getFeatures()] 290 | ]; 291 | } 292 | 293 | /** 294 | * Add role 295 | * 296 | * @param \Thruway\Role\AbstractRole $role 297 | * @return \Thruway\Peer\Client 298 | */ 299 | public function addRole(AbstractRole $role) 300 | { 301 | 302 | if ($role instanceof Publisher): 303 | $this->publisher = $role; 304 | elseif ($role instanceof Subscriber): 305 | $this->subscriber = $role; 306 | elseif ($role instanceof Callee): 307 | $this->callee = $role; 308 | elseif ($role instanceof Caller): 309 | $this->caller = $role; 310 | endif; 311 | 312 | array_push($this->roles, $role); 313 | 314 | return $this; 315 | } 316 | 317 | /** 318 | * Handle process message 319 | * 320 | * @param \Thruway\Transport\TransportInterface $transport 321 | * @param \Thruway\Message\Message $msg 322 | * @return mixed|void 323 | */ 324 | public function onMessage(TransportInterface $transport, Message $msg) 325 | { 326 | 327 | Logger::debug($this, "Client onMessage: {$msg}"); 328 | 329 | $session = $this->session; 330 | 331 | if($session === null) { 332 | Logger::warning($this, "Message discarded, no session: {$msg}"); 333 | return; 334 | } 335 | 336 | if ($msg instanceof WelcomeMessage): 337 | $this->processWelcome($session, $msg); 338 | elseif ($msg instanceof AbortMessage): 339 | $this->processAbort($session, $msg); 340 | elseif ($msg instanceof GoodbyeMessage): 341 | $this->processGoodbye($session, $msg); 342 | elseif ($msg instanceof ChallengeMessage): //advanced 343 | { 344 | $this->processChallenge($session, $msg); 345 | } else: 346 | $this->processOther($session, $msg); 347 | endif; 348 | } 349 | 350 | /** 351 | * Process Welcome message 352 | * 353 | * @param \Thruway\ClientSession $session 354 | * @param \Thruway\Message\WelcomeMessage $msg 355 | */ 356 | public function processWelcome(ClientSession $session, WelcomeMessage $msg) 357 | { 358 | Logger::info($this, "We have been welcomed..."); 359 | //TODO: I'm sure that there are some other things that we need to do here 360 | $session->setSessionId($msg->getSessionId()); 361 | $this->emit('open', [$session, $this->transport, $msg->getDetails()]); 362 | 363 | $session->setState(ClientSession::STATE_UP); 364 | } 365 | 366 | /** 367 | * @param \Thruway\ClientSession $session 368 | * @param \Thruway\Message\AbortMessage $msg 369 | */ 370 | public function processAbort(ClientSession $session, AbortMessage $msg) 371 | { 372 | $this->emit('error', [$msg->getResponseURI(), $msg]); 373 | $session->shutdown(); 374 | } 375 | 376 | /** 377 | * Handle process challenge message 378 | * 379 | * @param \Thruway\ClientSession $session 380 | * @param \Thruway\Message\ChallengeMessage $msg 381 | */ 382 | public function processChallenge(ClientSession $session, ChallengeMessage $msg) 383 | { 384 | $authMethod = $msg->getAuthMethod(); 385 | 386 | // look for authenticator 387 | /** @var ClientAuthenticationInterface $ca */ 388 | foreach ($this->clientAuthenticators as $ca) { 389 | if (in_array($authMethod, $ca->getAuthMethods())) { 390 | $authenticateMsg = $ca->getAuthenticateFromChallenge($msg); 391 | $session->sendMessage($authenticateMsg); 392 | 393 | return; 394 | } 395 | } 396 | 397 | $this->emit('challenge', [$session, $msg]); 398 | } 399 | 400 | /** 401 | * Handle process goodbye message 402 | * 403 | * @param \Thruway\ClientSession $session 404 | * @param \Thruway\Message\GoodbyeMessage $msg 405 | */ 406 | public function processGoodbye(ClientSession $session, GoodbyeMessage $msg) 407 | { 408 | if (!$session->isGoodbyeSent()) { 409 | $goodbyeMsg = new GoodbyeMessage(new \stdClass(), "wamp.error.goodbye_and_out"); 410 | $session->sendMessage($goodbyeMsg); 411 | $session->setGoodbyeSent(true); 412 | } 413 | } 414 | 415 | /** 416 | * Handle process other Message 417 | * 418 | * @param \Thruway\ClientSession $session 419 | * @param Message $msg 420 | */ 421 | public function processOther(ClientSession $session, Message $msg) 422 | { 423 | /* @var $role AbstractRole */ 424 | foreach ($this->roles as $role) { 425 | if ($role->handlesMessage($msg)) { 426 | $role->onMessage($session, $msg); 427 | break; 428 | } 429 | } 430 | } 431 | 432 | /** 433 | * Handle end session 434 | * 435 | * @param \Thruway\ClientSession $session 436 | */ 437 | public function onSessionEnd($session) 438 | { 439 | 440 | } 441 | 442 | /** 443 | * Handle close session 444 | * 445 | * @param mixed $reason 446 | */ 447 | public function onClose($reason) 448 | { 449 | 450 | if (isset($this->session)) { 451 | $this->onSessionEnd($this->session); 452 | $this->session->onClose(); 453 | $this->session = null; 454 | $this->emit('close', [$reason]); 455 | } 456 | 457 | $this->roles = []; 458 | $this->callee = null; 459 | $this->caller = null; 460 | $this->subscriber = null; 461 | $this->publisher = null; 462 | 463 | $this->retryConnection(); 464 | 465 | } 466 | 467 | /** 468 | * Retry connecting to the transport 469 | */ 470 | public function retryConnection() 471 | { 472 | $options = $this->reconnectOptions; 473 | 474 | if ($this->attemptRetry === false) { 475 | return; 476 | } 477 | 478 | if ($options['max_retries'] <= $this->retryAttempts) { 479 | return; 480 | } 481 | 482 | $this->retryAttempts++; 483 | 484 | if ($this->retryTimer >= $options['max_retry_delay']) { 485 | $this->retryTimer = $options['max_retry_delay']; 486 | } elseif ($this->retryTimer === 0) { 487 | $this->retryTimer = $options['initial_retry_delay']; 488 | } else { 489 | $this->retryTimer *= $options['retry_delay_growth']; 490 | } 491 | 492 | $this->loop->addTimer( 493 | $this->retryTimer, 494 | function () { 495 | $this->transportProvider->startTransportProvider($this, $this->loop); 496 | } 497 | ); 498 | } 499 | 500 | 501 | /** 502 | * Set attempt retry 503 | * 504 | * @param boolean $attemptRetry 505 | */ 506 | public function setAttemptRetry($attemptRetry) 507 | { 508 | $this->attemptRetry = $attemptRetry; 509 | } 510 | 511 | /** 512 | * Get callee 513 | * 514 | * @return \Thruway\Role\Callee 515 | */ 516 | public function getCallee() 517 | { 518 | return $this->callee; 519 | } 520 | 521 | /** 522 | * Get caller 523 | * 524 | * @return \Thruway\Role\Caller 525 | */ 526 | public function getCaller() 527 | { 528 | return $this->caller; 529 | } 530 | 531 | /** 532 | * Get publisher 533 | * 534 | * @return \Thruway\Role\Publisher 535 | */ 536 | public function getPublisher() 537 | { 538 | return $this->publisher; 539 | } 540 | 541 | /** 542 | * Get subscriber 543 | * 544 | * @return \Thruway\Role\Subscriber 545 | */ 546 | public function getSubscriber() 547 | { 548 | return $this->subscriber; 549 | } 550 | 551 | /** 552 | * Get list roles 553 | * 554 | * @return array 555 | */ 556 | public function getRoles() 557 | { 558 | return $this->roles; 559 | } 560 | 561 | /** 562 | * Get loop 563 | * 564 | * @return \React\EventLoop\LoopInterface 565 | */ 566 | public function getLoop() 567 | { 568 | return $this->loop; 569 | } 570 | 571 | /** 572 | * Set authenticate ID 573 | * 574 | * @param string $authId 575 | */ 576 | public function setAuthId($authId) 577 | { 578 | $this->authId = $authId; 579 | } 580 | 581 | /** 582 | * Get authenticate ID 583 | * 584 | * @return string 585 | */ 586 | public function getAuthId() 587 | { 588 | return $this->authId; 589 | } 590 | 591 | /** 592 | * Get list authenticate methods 593 | * 594 | * @return array 595 | */ 596 | public function getAuthMethods() 597 | { 598 | return $this->authMethods; 599 | } 600 | 601 | /** 602 | * Set list authenticate methods 603 | * 604 | * @param array $authMethods 605 | */ 606 | public function setAuthMethods($authMethods) 607 | { 608 | $this->authMethods = $authMethods; 609 | } 610 | 611 | /** 612 | * Get client session 613 | * 614 | * @return \Thruway\ClientSession 615 | */ 616 | public function getSession() 617 | { 618 | return $this->session; 619 | } 620 | 621 | /** 622 | * @return string 623 | */ 624 | public function getRealm() 625 | { 626 | return $this->realm; 627 | } 628 | 629 | /** 630 | * @param LoopInterface $loop 631 | */ 632 | public function setLoop(LoopInterface $loop) 633 | { 634 | $this->loop = $loop; 635 | } 636 | } 637 | -------------------------------------------------------------------------------- /src/Role/Callee.php: -------------------------------------------------------------------------------- 1 | registrations = []; 48 | } 49 | 50 | /** 51 | * Return supported features 52 | * 53 | * @return \stdClass 54 | */ 55 | public function getFeatures() 56 | { 57 | $features = new \stdClass(); 58 | 59 | $features->caller_identification = true; 60 | $features->progressive_call_results = true; 61 | $features->call_canceling = true; 62 | 63 | return $features; 64 | } 65 | 66 | /** 67 | * Handle process reveiced message 68 | * 69 | * @param \Thruway\AbstractSession $session 70 | * @param \Thruway\Message\Message $msg 71 | * @return mixed|void 72 | */ 73 | public function onMessage(AbstractSession $session, Message $msg) 74 | { 75 | if ($msg instanceof RegisteredMessage): 76 | $this->processRegistered($msg); 77 | elseif ($msg instanceof UnregisteredMessage): 78 | $this->processUnregistered($msg); 79 | elseif ($msg instanceof InvocationMessage): 80 | $this->processInvocation($session, $msg); 81 | elseif ($msg instanceof InterruptMessage): 82 | $this->processInterrupt($session, $msg); 83 | elseif ($msg instanceof ErrorMessage): 84 | $this->processError($session, $msg); 85 | else: 86 | $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg)); 87 | endif; 88 | } 89 | 90 | /** 91 | * Process RegisteredMessage 92 | * 93 | * @param \Thruway\Message\RegisteredMessage $msg 94 | * @return void 95 | */ 96 | protected function processRegistered(RegisteredMessage $msg) 97 | { 98 | foreach ($this->registrations as $key => $registration) { 99 | if ($registration["request_id"] === $msg->getRequestId()) { 100 | Logger::info($this, "Setting registration_id for ".$registration['procedure_name']." (".$key.")"); 101 | $this->registrations[$key]['registration_id'] = $msg->getRegistrationId(); 102 | 103 | if ($this->registrations[$key]['futureResult'] instanceof Deferred) { 104 | /* @var $futureResult \React\Promise\Deferred */ 105 | $futureResult = $this->registrations[$key]['futureResult']; 106 | $futureResult->resolve(); 107 | } 108 | 109 | return; 110 | } 111 | } 112 | Logger::error($this, "Got a Registered Message, but the request ids don't match"); 113 | } 114 | 115 | /** 116 | * Process Unregistered 117 | * 118 | * @param \Thruway\Message\UnregisteredMessage $msg 119 | */ 120 | protected function processUnregistered(UnregisteredMessage $msg) 121 | { 122 | foreach ($this->registrations as $key => $registration) { 123 | if (isset($registration['unregister_request_id'])) { 124 | if ($registration["unregister_request_id"] == $msg->getRequestId()) { 125 | /** @var $deferred \React\Promise\Deferred */ 126 | $deferred = $registration['unregister_deferred']; 127 | $deferred->resolve(); 128 | 129 | unset($this->registrations[$key]); 130 | 131 | return; 132 | } 133 | } 134 | } 135 | Logger::error($this, "Got an Unregistered Message, but couldn't find corresponding request"); 136 | } 137 | 138 | private function processExceptionFromRPCCall(ClientSession $session, InvocationMessage $msg, $registration, \Exception $e) { 139 | if ($e instanceof WampErrorException) { 140 | $errorMsg = ErrorMessage::createErrorMessageFromMessage($msg); 141 | $errorMsg->setErrorURI($e->getErrorUri()); 142 | $errorMsg->setArguments($e->getArguments()); 143 | $errorMsg->setArgumentsKw($e->getArgumentsKw()); 144 | $errorMsg->setDetails($e->getDetails()); 145 | 146 | $session->sendMessage($errorMsg); 147 | return; 148 | } 149 | 150 | $errorMsg = ErrorMessage::createErrorMessageFromMessage($msg); 151 | $errorMsg->setErrorURI($registration['procedure_name'].'.error'); 152 | $errorMsg->setArguments([$e->getMessage()]); 153 | $errorMsg->setArgumentsKw($e); 154 | 155 | $session->sendMessage($errorMsg); 156 | } 157 | 158 | /** 159 | * Process InvocationMessage 160 | * 161 | * @param \Thruway\ClientSession $session 162 | * @param \Thruway\Message\InvocationMessage $msg 163 | */ 164 | protected function processInvocation(ClientSession $session, InvocationMessage $msg) 165 | { 166 | foreach ($this->registrations as $key => $registration) { 167 | if (!isset($registration['registration_id'])) { 168 | Logger::info($this, 'Registration_id not set for '.$registration['procedure_name']); 169 | } else { 170 | if ($registration['registration_id'] === $msg->getRegistrationId()) { 171 | 172 | if ($registration['callback'] === null) { 173 | // this is where calls end up if the client has called unregister but 174 | // have not yet received confirmation from the router about the 175 | // unregistration 176 | $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg)); 177 | 178 | return; 179 | } 180 | 181 | try { 182 | $results = $registration['callback']($msg->getArguments(), $msg->getArgumentsKw(), $msg->getDetails()); 183 | 184 | if ($results instanceof PromiseInterface) { 185 | if ($results instanceof CancellablePromiseInterface) { 186 | $this->invocationCanceller[$msg->getRequestId()] = function () use ($results) { 187 | $results->cancel(); 188 | }; 189 | $results = $results->then(function ($result) use ($msg) { 190 | unset($this->invocationCanceller[$msg->getRequestId()]); 191 | return $result; 192 | }); 193 | } 194 | $this->processResultAsPromise($results, $msg, $session, $registration); 195 | } else { 196 | $this->processResultAsArray($results, $msg, $session); 197 | } 198 | 199 | } catch (\Exception $e) { 200 | $this->processExceptionFromRPCCall($session, $msg, $registration, $e); 201 | } 202 | 203 | break; 204 | } 205 | } 206 | } 207 | 208 | } 209 | 210 | private function processInterrupt(ClientSession $session, InterruptMessage $msg) 211 | { 212 | if (isset($this->invocationCanceller[$msg->getRequestId()])) { 213 | $callable = $this->invocationCanceller[$msg->getRequestId()]; 214 | unset($this->invocationCanceller[$msg->getRequestId()]); 215 | $callable(); 216 | } 217 | } 218 | 219 | /** 220 | * Process a result as a promise 221 | * 222 | * @param \React\Promise\PromiseInterface $promise 223 | * @param \Thruway\Message\InvocationMessage $msg 224 | * @param \Thruway\ClientSession $session 225 | * @param array $registration 226 | */ 227 | private function processResultAsPromise(PromiseInterface $promise, InvocationMessage $msg, ClientSession $session, $registration) 228 | { 229 | 230 | $promise->then( 231 | function ($promiseResults) use ($msg, $session) { 232 | $options = new \stdClass(); 233 | if ($promiseResults instanceof Result) { 234 | $yieldMsg = new YieldMessage($msg->getRequestId(), $options, 235 | $promiseResults->getArguments(), $promiseResults->getArgumentsKw()); 236 | } else { 237 | $promiseResults = is_array($promiseResults) ? $promiseResults : [$promiseResults]; 238 | $promiseResults = !$this::is_list($promiseResults) ? [$promiseResults] : $promiseResults; 239 | 240 | $yieldMsg = new YieldMessage($msg->getRequestId(), $options, $promiseResults); 241 | } 242 | 243 | $session->sendMessage($yieldMsg); 244 | }, 245 | function ($e) use ($msg, $session, $registration) { 246 | if ($e instanceof \Exception) { 247 | $this->processExceptionFromRPCCall($session, $msg, $registration, $e); 248 | return; 249 | } 250 | 251 | $errorMsg = ErrorMessage::createErrorMessageFromMessage($msg); 252 | $errorMsg->setErrorURI($registration['procedure_name'].'.error'); 253 | 254 | $session->sendMessage($errorMsg); 255 | }, 256 | function ($results) use ($msg, $session, $registration) { 257 | $options = new \stdClass(); 258 | $options->progress = true; 259 | if ($results instanceof Result) { 260 | $yieldMsg = new YieldMessage($msg->getRequestId(), $options, $results->getArguments(), 261 | $results->getArgumentsKw()); 262 | } else { 263 | $results = is_array($results) ? $results : [$results]; 264 | $results = !$this::is_list($results) ? [$results] : $results; 265 | 266 | $yieldMsg = new YieldMessage($msg->getRequestId(), $options, $results); 267 | } 268 | 269 | $session->sendMessage($yieldMsg); 270 | } 271 | ); 272 | } 273 | 274 | /** 275 | * Process result as an array 276 | * 277 | * @param mixed $results 278 | * @param \Thruway\Message\InvocationMessage $msg 279 | * @param \Thruway\ClientSession $session 280 | */ 281 | private function processResultAsArray($results, InvocationMessage $msg, ClientSession $session) 282 | { 283 | $options = new \stdClass(); 284 | if ($results instanceof Result) { 285 | $yieldMsg = new YieldMessage($msg->getRequestId(), $options, $results->getArguments(), 286 | $results->getArgumentsKw()); 287 | } else { 288 | $results = is_array($results) ? $results : [$results]; 289 | $results = !$this::is_list($results) ? [$results] : $results; 290 | 291 | $yieldMsg = new YieldMessage($msg->getRequestId(), $options, $results); 292 | } 293 | 294 | $session->sendMessage($yieldMsg); 295 | } 296 | 297 | /** 298 | * Process ErrorMessage 299 | * 300 | * @param \Thruway\ClientSession $session 301 | * @param \Thruway\Message\ErrorMessage $msg 302 | */ 303 | public function processError(ClientSession $session, ErrorMessage $msg) 304 | { 305 | if ($msg->getErrorMsgCode() === Message::MSG_REGISTER) { 306 | $this->handleErrorRegister($session, $msg); 307 | } elseif ($msg->getErrorMsgCode() === Message::MSG_UNREGISTER) { 308 | $this->handleErrorUnregister($session, $msg); 309 | } else { 310 | Logger::error($this, 'Unhandled error message: '.json_encode($msg)); 311 | } 312 | } 313 | 314 | /** 315 | * Handle error when register 316 | * 317 | * @param \Thruway\ClientSession $session 318 | * @param \Thruway\Message\ErrorMessage $msg 319 | */ 320 | public function handleErrorRegister(ClientSession $session, ErrorMessage $msg) 321 | { 322 | foreach ($this->registrations as $key => $registration) { 323 | if ($registration['request_id'] === $msg->getRequestId()) { 324 | /** @var Deferred $deferred */ 325 | $deferred = $registration['futureResult']; 326 | $deferred->reject($msg); 327 | unset($this->registrations[$key]); 328 | break; 329 | } 330 | } 331 | } 332 | 333 | /** 334 | * Handle error when unregister 335 | * 336 | * @param \Thruway\ClientSession $session 337 | * @param \Thruway\Message\ErrorMessage $msg 338 | */ 339 | public function handleErrorUnregister(ClientSession $session, ErrorMessage $msg) 340 | { 341 | foreach ($this->registrations as $key => $registration) { 342 | if (isset($registration['unregister_request_id'])) { 343 | if ($registration['unregister_request_id'] === $msg->getRequestId()) { 344 | /** @var Deferred $deferred */ 345 | $deferred = $registration['unregister_deferred']; 346 | $deferred->reject($msg); 347 | 348 | // I guess we get rid of the registration now? 349 | unset($this->registrations[$key]); 350 | break; 351 | } 352 | } 353 | } 354 | } 355 | 356 | /** 357 | * Returns true if this role handles this message. 358 | * Error messages are checked according to the 359 | * message the error corresponds to. 360 | * 361 | * @param \Thruway\Message\Message $msg 362 | * @return boolean 363 | */ 364 | public function handlesMessage(Message $msg) 365 | { 366 | $handledMsgCodes = [ 367 | Message::MSG_REGISTERED, 368 | Message::MSG_UNREGISTERED, 369 | Message::MSG_INVOCATION, 370 | Message::MSG_REGISTER, 371 | Message::MSG_INTERRUPT 372 | ]; 373 | 374 | $codeToCheck = $msg->getMsgCode(); 375 | 376 | if ($msg instanceof ErrorMessage) { 377 | $codeToCheck = $msg->getErrorMsgCode(); 378 | } 379 | 380 | if (in_array($codeToCheck, $handledMsgCodes, true)) { 381 | return true; 382 | } else { 383 | return false; 384 | } 385 | } 386 | 387 | /** 388 | * process register 389 | * 390 | * @param \Thruway\ClientSession $session 391 | * @param string $procedureName 392 | * @param callable $callback 393 | * @param mixed $options 394 | * @return \React\Promise\Promise 395 | */ 396 | public function register(ClientSession $session, $procedureName, callable $callback, $options = null) 397 | { 398 | $futureResult = new Deferred(); 399 | 400 | $requestId = Utils::getUniqueId(); 401 | $options = isset($options) ? (object) $options : new \stdClass(); 402 | $registration = [ 403 | 'procedure_name' => $procedureName, 404 | 'callback' => $callback, 405 | 'request_id' => $requestId, 406 | 'options' => $options, 407 | 'futureResult' => $futureResult 408 | ]; 409 | 410 | array_push($this->registrations, $registration); 411 | 412 | $registerMsg = new RegisterMessage($requestId, $options, $procedureName); 413 | 414 | $session->sendMessage($registerMsg); 415 | 416 | return $futureResult->promise(); 417 | } 418 | 419 | /** 420 | * process unregister 421 | * 422 | * @param \Thruway\ClientSession $session 423 | * @param string $Uri 424 | * @throws \Exception 425 | * @return \React\Promise\Promise|false 426 | */ 427 | public function unregister(ClientSession $session, $Uri) 428 | { 429 | // TODO: maybe add an option to wait for pending calls to finish 430 | 431 | $registration = null; 432 | 433 | foreach ($this->registrations as $k => $r) { 434 | if (isset($r['procedure_name'])) { 435 | if ($r['procedure_name'] === $Uri) { 436 | $registration = &$this->registrations[$k]; 437 | break; 438 | } 439 | } 440 | } 441 | 442 | if ($registration === null) { 443 | Logger::warning($this, 'registration not found: '.$Uri); 444 | 445 | return false; 446 | } 447 | 448 | // we remove the callback from the client here 449 | // because we don't want the client to respond to any more calls 450 | $registration['callback'] = null; 451 | 452 | $futureResult = new Deferred(); 453 | 454 | if (!isset($registration['registration_id'])) { 455 | // this would happen if the registration was never acknowledged by the router 456 | // we should remove the registration and resolve any pending deferreds 457 | Logger::error($this, 'Registration ID is not set while attempting to unregister '.$Uri); 458 | 459 | // reject the pending registration 460 | $registration['futureResult']->reject(); 461 | 462 | // TODO: need to figure out what to do in this off chance 463 | // We should still probably return a promise here that just rejects 464 | // there is an issue with the pending registration too that 465 | // the router may have a "REGISTERED" in transit and may still think that is 466 | // good to go - so maybe still send the unregister? 467 | } 468 | 469 | $requestId = Utils::getUniqueId(); 470 | 471 | // save the request id so we can find this in the registration 472 | // list to call the deferred and remove it from the list 473 | $registration['unregister_request_id'] = $requestId; 474 | $registration['unregister_deferred'] = $futureResult; 475 | 476 | $unregisterMsg = new UnregisterMessage($requestId, $registration['registration_id']); 477 | 478 | $session->sendMessage($unregisterMsg); 479 | 480 | return $futureResult->promise(); 481 | } 482 | 483 | /** 484 | * This belongs somewhere else I am thinking 485 | * 486 | * @param array $array 487 | * @return boolean 488 | */ 489 | public static function is_list($array) 490 | { 491 | if (!is_array($array)) { 492 | return false; 493 | } 494 | 495 | // Keys of the array 496 | $keys = array_keys($array); 497 | 498 | // If the array keys of the keys match the keys, then the array must 499 | // not be associative (e.g. the keys array looked like {0:0, 1:1...}). 500 | return array_keys($keys) === $keys; 501 | } 502 | } 503 | --------------------------------------------------------------------------------