├── COPYRIGHT.md ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── psalm-baseline.xml ├── psalm.xml.dist ├── renovate.json └── src ├── Cache.php ├── Client.php ├── Error.php ├── Exception ├── BadMethodCallException.php ├── ErrorException.php ├── ExceptionInterface.php ├── HttpException.php ├── InvalidArgumentException.php └── RuntimeException.php ├── Request.php ├── Request └── Http.php ├── Response.php ├── Response └── Http.php ├── Server.php ├── Smd.php └── Smd └── Service.php /COPYRIGHT.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/) 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | - Neither the name of Laminas Foundation nor the names of its contributors may 14 | be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laminas-json-server 2 | 3 | [![Build Status](https://github.com/laminas/laminas-json-server/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/laminas/laminas-json-server/actions/workflows/continuous-integration.yml) 4 | 5 | > ## 🇷🇺 Русским гражданам 6 | > 7 | > Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм. 8 | > 9 | > У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую. 10 | > 11 | > Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!" 12 | > 13 | > ## 🇺🇸 To Citizens of Russia 14 | > 15 | > We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism. 16 | > 17 | > One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences. 18 | > 19 | > You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!" 20 | 21 | Provides a JSON-RPC server implementation. 22 | 23 | - File issues at https://github.com/laminas/laminas-json-server/issues 24 | - Documentation is at https://docs.laminas.dev/laminas-json-server/ 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laminas/laminas-json-server", 3 | "description": "Laminas Json-Server is a JSON-RPC server implementation.", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "laminas", 7 | "json", 8 | "server", 9 | "json-server" 10 | ], 11 | "homepage": "https://laminas.dev", 12 | "support": { 13 | "docs": "https://docs.laminas.dev/laminas-json-server/", 14 | "issues": "https://github.com/laminas/laminas-json-server/issues", 15 | "source": "https://github.com/laminas/laminas-json-server", 16 | "rss": "https://github.com/laminas/laminas-json-server/releases.atom", 17 | "chat": "https://laminas.dev/chat", 18 | "forum": "https://discourse.laminas.dev" 19 | }, 20 | "config": { 21 | "allow-plugins": { 22 | "dealerdirect/phpcodesniffer-composer-installer": true 23 | }, 24 | "sort-packages": true, 25 | "platform": { 26 | "php": "8.1.99" 27 | } 28 | }, 29 | "require": { 30 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0", 31 | "laminas/laminas-http": "^2.19.0", 32 | "laminas/laminas-json": "^3.6.0", 33 | "laminas/laminas-server": "^2.16.0" 34 | }, 35 | "require-dev": { 36 | "ext-json": "*", 37 | "laminas/laminas-coding-standard": "^2.4.0", 38 | "phpunit/phpunit": "^10.5", 39 | "psalm/plugin-phpunit": "^0.18.0", 40 | "vimeo/psalm": "^5.17" 41 | }, 42 | "conflict": { 43 | "laminas/laminas-stdlib": "<3.2.1" 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "Laminas\\Json\\Server\\": "src/" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "files": [ 52 | "test/TestAsset/FooFunc.php" 53 | ], 54 | "psr-4": { 55 | "LaminasTest\\Json\\Server\\": "test/" 56 | } 57 | }, 58 | "scripts": { 59 | "check": [ 60 | "@cs-check", 61 | "@test" 62 | ], 63 | "cs-check": "phpcs", 64 | "cs-fix": "phpcbf", 65 | "static-analysis": "psalm --shepherd --stats", 66 | "test": "phpunit --colors=always", 67 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 68 | }, 69 | "replace": { 70 | "zendframework/zend-json-server": "^3.2.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /psalm-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ! is_string($filename) 6 | ! is_string($filename) 7 | 8 | 9 | static function ($errno, $errstr): void { 10 | // swallow errors; method returns false on failure 11 | } 12 | static function ($errno, $errstr): void { 13 | // swallow errors; method returns false on failure 14 | } 15 | 16 | 17 | is_string($filename) 18 | 19 | 20 | $errno 21 | $errno 22 | $errstr 23 | $errstr 24 | 25 | 26 | 27 | 28 | Client 29 | 30 | 31 | getUriString() === null]]> 32 | 33 | 34 | addHeaderLine 35 | addHeaders 36 | get 37 | has 38 | has 39 | 40 | 41 | getMessage 42 | 43 | 44 | addHeaderLine 45 | addHeaders 46 | get 47 | has 48 | has 49 | 50 | 51 | $lastRequest 52 | $lastResponse 53 | 54 | 55 | 56 | 57 | methodRegex]]> 58 | 59 | 60 | ! is_string($key) 61 | 62 | 63 | $method 64 | 65 | 66 | $options 67 | $value 68 | 69 | 70 | $key 71 | $key 72 | 73 | 74 | $options 75 | $value 76 | $value 77 | 78 | 79 | string 80 | 81 | 82 | id]]> 83 | 84 | 85 | getId())]]> 86 | 87 | 88 | 89 | 90 | getRawJson 91 | 92 | 93 | Http 94 | 95 | 96 | 97 | 98 | $id 99 | $id 100 | $serviceMap 101 | $serviceMap 102 | 103 | 104 | 105 | $value 106 | 107 | 108 | $errorData 109 | 110 | error]]> 111 | id]]> 112 | $value 113 | 114 | 115 | toArray 116 | 117 | 118 | getArgs 119 | setArgs 120 | 121 | 122 | (string) $version 123 | 124 | 125 | 126 | 127 | getClass()]]> 128 | getFunction()]]> 129 | 130 | 131 | Server 132 | 133 | 134 | _buildSignature 135 | _buildSignature 136 | _dispatch 137 | 138 | 139 | getMethod()]]> 140 | request]]> 141 | response]]> 142 | serviceMap]]> 143 | 144 | 145 | $function 146 | 147 | 148 | Server 149 | Server 150 | self 151 | 152 | 153 | static function ($count, $param) { 154 | 155 | 156 | $argv 157 | $method 158 | $method 159 | $method 160 | $method 161 | $param 162 | 163 | 164 | 165 | $key 166 | 167 | 168 | 169 | 170 | 171 | 172 | getType()]]> 173 | 174 | 175 | 176 | 177 | $params[$key] 178 | $params[$key] 179 | $params[$key] 180 | $params[$key] 181 | $params[$key] 182 | $params[$key] 183 | 184 | 185 | 186 | $count 187 | $default 188 | $description 189 | $key 190 | $method 191 | $method 192 | $method 193 | $newType 194 | getName()]]]> 195 | $param 196 | $parameter 197 | 198 | 199 | $prototype 200 | $prototype 201 | $requiredParamsCount 202 | $result 203 | $return[] 204 | $value 205 | 206 | 207 | string|array 208 | 209 | 210 | getDefaultValue 211 | getDescription 212 | getName 213 | getParameterObjects 214 | getReturnType 215 | getType 216 | getType 217 | getType 218 | isOptional 219 | 220 | 221 | $count 222 | 223 | 224 | $return[0] 225 | 226 | 227 | $class 228 | $fault 229 | $request 230 | 231 | 232 | getCode()]]> 233 | $function 234 | 235 | 236 | $function 237 | 238 | 239 | getParams 240 | 241 | 242 | $argv 243 | $argv 244 | getClass()]]> 245 | getFunction()]]> 246 | getMethod()]]> 247 | $invokable 248 | 249 | 250 | $method 251 | 252 | 253 | Error|null 254 | 255 | 256 | $request 257 | $response 258 | $serviceMap 259 | $smdMethods 260 | 261 | 262 | (bool) $flag 263 | 264 | 265 | smdMethods]]> 266 | getId())]]> 267 | getVersion())]]> 268 | 269 | 270 | 271 | 272 | contentTypeRegex]]> 273 | 274 | 275 | $description 276 | $id 277 | $target 278 | 279 | 280 | $param 281 | $service 282 | 283 | 284 | $key 285 | 286 | 287 | $param 288 | $service 289 | 290 | $svc 291 | $svc 292 | $value 293 | 294 | 295 | bool|Smd\Service 296 | 297 | 298 | getParams 299 | setEnvelope 300 | toArray 301 | 302 | 303 | services[$name]]]> 304 | 305 | 306 | self 307 | 308 | 309 | (bool) $flag 310 | (string) $description 311 | (string) $id 312 | (string) $target 313 | 314 | 315 | getId())]]> 316 | getTarget())]]> 317 | 318 | 319 | 320 | 321 | nameRegex]]> 322 | 323 | 324 | 325 | 326 | is_string($type) 327 | 328 | 329 | $order 330 | $paramType 331 | $returnType 332 | $type 333 | 334 | 335 | $key 336 | $key 337 | envelopeTypes]]> 338 | transportTypes]]> 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | $callback 351 | $order 352 | $param 353 | $paramOptions[$key] 354 | $paramType 355 | $paramType 356 | $params[$index] 357 | 358 | $returnType 359 | $type 360 | $value 361 | $value 362 | 363 | 364 | $callback($value) 365 | 366 | 367 | string 368 | 369 | 370 | $paramType 371 | 372 | 373 | $type 374 | 375 | 376 | self 377 | 378 | 379 | (string) $target 380 | 381 | 382 | is_array($spec) 383 | 384 | 385 | 386 | 387 | cacheFile]]> 388 | cacheFile]]> 389 | cacheFile]]> 390 | cacheFile]]> 391 | cacheFile]]> 392 | cacheFile]]> 393 | cacheFile]]> 394 | 395 | 396 | 397 | 398 | Exception\ExceptionInterface::class 399 | 400 | 401 | makeHttpResponseFor 402 | mockHttpClient 403 | 404 | 405 | 406 | 407 | Json\Json::decode($json, Json\Json::TYPE_ARRAY) 408 | Json\Json::decode($json, Json\Json::TYPE_ARRAY) 409 | 410 | 411 | array 412 | 413 | 414 | assertIsArray 415 | 416 | 417 | 418 | 419 | $spec[1] 420 | 421 | 422 | $test 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | $version 432 | 433 | 434 | getCode 435 | getData 436 | getMessage 437 | 438 | 439 | 440 | 441 | $object 442 | $object 443 | new Json\Json() 444 | 445 | 446 | $object 447 | $object 448 | new Json\Json() 449 | 450 | 451 | 452 | 453 | 454 | setContentType 455 | setContentType 456 | setDescription 457 | setDescription 458 | setEnvelope 459 | setEnvelope 460 | setId 461 | setId 462 | setTarget 463 | setTarget 464 | 465 | 466 | getCallback 467 | getCode 468 | getCode 469 | getCode 470 | getCode 471 | getCode 472 | getCode 473 | getCode 474 | getMessage 475 | getResult 476 | getResult 477 | getResult 478 | getResult 479 | 480 | 481 | assertIsArray 482 | 483 | 484 | 485 | 486 | $params 487 | $params 488 | $params 489 | 123 490 | new stdClass() 491 | new stdClass() 492 | 493 | 494 | $params 495 | $smd 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | $param 517 | $param 518 | $param 519 | $param 520 | $param 521 | $param 522 | $param 523 | $param 524 | $param 525 | $param 526 | $param 527 | $param 528 | $params 529 | $smd 530 | 531 | 532 | null 533 | null 534 | 535 | 536 | 537 | 538 | $methods 539 | $methods 540 | $service 541 | $services 542 | $services 543 | 544 | 545 | $bar 546 | $foo 547 | $methods 548 | $services 549 | $smd 550 | $smd 551 | 552 | 553 | $bar 554 | $foo 555 | $methods 556 | $services 557 | $smd 558 | $smd 559 | 560 | 561 | assertIsArray 562 | 563 | 564 | assertIsArray 565 | assertIsArray 566 | 567 | 568 | 569 | 570 | $someval 571 | 572 | 573 | $val 574 | 575 | 576 | baz 577 | 578 | 579 | array 580 | 581 | 582 | 583 | 584 | bar 585 | baz 586 | 587 | 588 | 589 | 590 | bar 591 | baz 592 | 593 | 594 | 595 | 596 | JsonSerializableBuiltinImpl 597 | 598 | 599 | 600 | 601 | TestIteratorAggregate 602 | 603 | 604 | 605 | -------------------------------------------------------------------------------- /psalm.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>laminas/.github:renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/Cache.php: -------------------------------------------------------------------------------- 1 | getServiceMap()->toJson()); 49 | 50 | restore_error_handler(); 51 | 52 | if (0 === $test) { 53 | return false; 54 | } 55 | 56 | return true; 57 | } 58 | 59 | /** 60 | * Retrieve a cached SMD 61 | * 62 | * On success, returns the cached SMD (a JSON string); a failure, returns 63 | * boolean false. 64 | * 65 | * @param string $filename 66 | * @return string|false 67 | */ 68 | public static function getSmd($filename) 69 | { 70 | if ( 71 | ! is_string($filename) 72 | || ! file_exists($filename) 73 | || ! is_readable($filename) 74 | ) { 75 | return false; 76 | } 77 | 78 | set_error_handler(static function ($errno, $errstr): void { 79 | // swallow errors; method returns false on failure 80 | }, E_WARNING); 81 | 82 | $smd = file_get_contents($filename); 83 | 84 | restore_error_handler(); 85 | 86 | if (false === $smd) { 87 | return false; 88 | } 89 | 90 | return $smd; 91 | } 92 | 93 | /** 94 | * Delete a file containing a cached SMD 95 | * 96 | * @param string $filename 97 | * @return bool 98 | */ 99 | public static function deleteSmd($filename) 100 | { 101 | if (is_string($filename) && file_exists($filename)) { 102 | unlink($filename); 103 | return true; 104 | } 105 | 106 | return false; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | httpClient = $httpClient ?: new HttpClient(); 56 | $this->serverAddress = $server; 57 | } 58 | 59 | /** 60 | * Sets the HTTP client object to use for connecting the JSON-RPC server. 61 | * 62 | * @param HttpClient $httpClient New HTTP client to use. 63 | * @return Client Self instance. 64 | */ 65 | public function setHttpClient(HttpClient $httpClient) 66 | { 67 | $this->httpClient = $httpClient; 68 | return $this; 69 | } 70 | 71 | /** 72 | * Gets the HTTP client object. 73 | * 74 | * @return HttpClient HTTP client. 75 | */ 76 | public function getHttpClient() 77 | { 78 | return $this->httpClient; 79 | } 80 | 81 | /** 82 | * The request of the last method call. 83 | * 84 | * @return Request 85 | */ 86 | public function getLastRequest() 87 | { 88 | return $this->lastRequest; 89 | } 90 | 91 | /** 92 | * The response received from the last method call. 93 | * 94 | * @return Response 95 | */ 96 | public function getLastResponse() 97 | { 98 | return $this->lastResponse; 99 | } 100 | 101 | /** 102 | * Perform a JSON-RPC request and return a response. 103 | * 104 | * @param Request $request Request. 105 | * @return Response Response. 106 | * @throws Exception\HttpException When HTTP communication fails. 107 | */ 108 | public function doRequest($request) 109 | { 110 | $this->lastRequest = $request; 111 | 112 | $httpRequest = $this->httpClient->getRequest(); 113 | if ($httpRequest->getUriString() === null) { 114 | $this->httpClient->setUri($this->serverAddress); 115 | } 116 | 117 | // Set default Accept and Content-Type headers unless already set. 118 | $headers = $httpRequest->getHeaders(); 119 | $headersToAdd = []; 120 | if (! $headers->has('Content-Type')) { 121 | $headersToAdd['Content-Type'] = 'application/json-rpc'; 122 | } 123 | if (! $headers->has('Accept')) { 124 | $headersToAdd['Accept'] = 'application/json-rpc'; 125 | } 126 | $headers->addHeaders($headersToAdd); 127 | 128 | if (! $headers->get('User-Agent')) { 129 | $headers->addHeaderLine('User-Agent', 'Laminas_Json_Server_Client'); 130 | } 131 | 132 | $this->httpClient->setRawBody($request->__toString()); 133 | $this->httpClient->setMethod('POST'); 134 | $httpResponse = $this->httpClient->send(); 135 | 136 | if (! $httpResponse->isSuccess()) { 137 | throw new Exception\HttpException( 138 | $httpResponse->getReasonPhrase(), 139 | $httpResponse->getStatusCode() 140 | ); 141 | } 142 | 143 | $response = new Response(); 144 | 145 | $this->lastResponse = $response; 146 | 147 | // import all response data from JSON HTTP response 148 | $response->loadJson($httpResponse->getBody()); 149 | 150 | return $response; 151 | } 152 | 153 | /** 154 | * Send a JSON-RPC request to the service (for a specific method). 155 | * 156 | * @param string $method Name of the method we want to call. 157 | * @param array $params Array of parameters for the method. 158 | * @return mixed Method call results. 159 | * @throws Exception\ErrorException When remote call fails. 160 | */ 161 | public function call($method, $params = []) 162 | { 163 | $request = $this->createRequest($method, $params); 164 | 165 | $response = $this->doRequest($request); 166 | 167 | if ($response->isError()) { 168 | $error = $response->getError(); 169 | throw new Exception\ErrorException( 170 | $error->getMessage(), 171 | $error->getCode() 172 | ); 173 | } 174 | 175 | return $response->getResult(); 176 | } 177 | 178 | /** 179 | * Create request object. 180 | * 181 | * @param string $method Method to call. 182 | * @param array $params List of arguments. 183 | * @return Request Created request. 184 | */ 185 | protected function createRequest($method, array $params) 186 | { 187 | $request = new Request(); 188 | $request->setMethod($method) 189 | ->setParams($params) 190 | ->setId(++$this->id); 191 | return $request; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/Error.php: -------------------------------------------------------------------------------- 1 | setMessage($message) 53 | ->setCode($code) 54 | ->setData($data); 55 | } 56 | 57 | /** 58 | * Set error code. 59 | * 60 | * If the error code is 0, it will be set to -32000 (ERROR_OTHER). 61 | * 62 | * @param mixed $code 63 | * @return self 64 | */ 65 | public function setCode($code) 66 | { 67 | if (! is_scalar($code) || is_bool($code) || is_float($code)) { 68 | return $this; 69 | } 70 | 71 | if (is_string($code) && ! is_numeric($code)) { 72 | return $this; 73 | } 74 | 75 | $code = (int) $code; 76 | 77 | if (0 === $code) { 78 | $this->code = self::ERROR_OTHER; 79 | return $this; 80 | } 81 | 82 | $this->code = $code; 83 | return $this; 84 | } 85 | 86 | /** 87 | * Get error code 88 | * 89 | * @return int 90 | */ 91 | public function getCode() 92 | { 93 | return $this->code; 94 | } 95 | 96 | /** 97 | * Set error message 98 | * 99 | * @param mixed $message 100 | * @return self 101 | */ 102 | public function setMessage($message) 103 | { 104 | if (! is_scalar($message)) { 105 | return $this; 106 | } 107 | 108 | $this->message = (string) $message; 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * Get error message 115 | * 116 | * @return string 117 | */ 118 | public function getMessage() 119 | { 120 | return $this->message; 121 | } 122 | 123 | /** 124 | * Set error data 125 | * 126 | * @param mixed $data 127 | * @return self 128 | */ 129 | public function setData($data) 130 | { 131 | $this->data = $data; 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * Get error data 138 | * 139 | * @return mixed 140 | */ 141 | public function getData() 142 | { 143 | return $this->data; 144 | } 145 | 146 | /** 147 | * Cast error to array 148 | * 149 | * @return array 150 | */ 151 | public function toArray() 152 | { 153 | return [ 154 | 'code' => $this->getCode(), 155 | 'message' => $this->getMessage(), 156 | 'data' => $this->getData(), 157 | ]; 158 | } 159 | 160 | /** 161 | * Cast error to JSON 162 | * 163 | * @return string 164 | */ 165 | public function toJson() 166 | { 167 | return Json::encode($this->toArray()); 168 | } 169 | 170 | /** 171 | * Cast to string (JSON) 172 | * 173 | * @return string 174 | */ 175 | public function __toString() 176 | { 177 | return $this->toJson(); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | $value) { 83 | $method = 'set' . ucfirst($key); 84 | if (in_array($method, $methods)) { 85 | $this->$method($value); 86 | continue; 87 | } 88 | 89 | if ($key === 'jsonrpc') { 90 | $this->setVersion($value); 91 | continue; 92 | } 93 | } 94 | return $this; 95 | } 96 | 97 | /** 98 | * Add a parameter to the request. 99 | * 100 | * @param mixed $value 101 | * @param string $key 102 | * @return self 103 | */ 104 | public function addParam($value, $key = null) 105 | { 106 | if ((null === $key) || ! is_string($key)) { 107 | $index = count($this->params); 108 | $this->params[$index] = $value; 109 | return $this; 110 | } 111 | 112 | $this->params[$key] = $value; 113 | return $this; 114 | } 115 | 116 | /** 117 | * Add many params. 118 | * 119 | * @param array $params 120 | * @return self 121 | */ 122 | public function addParams(array $params) 123 | { 124 | foreach ($params as $key => $value) { 125 | $this->addParam($value, $key); 126 | } 127 | return $this; 128 | } 129 | 130 | /** 131 | * Overwrite params. 132 | * 133 | * @param array $params 134 | * @return Request 135 | */ 136 | public function setParams(array $params) 137 | { 138 | $this->params = []; 139 | return $this->addParams($params); 140 | } 141 | 142 | /** 143 | * Retrieve param by index or key. 144 | * 145 | * @param int|string $index 146 | * @return mixed|null Null when not found 147 | */ 148 | public function getParam($index) 149 | { 150 | if (! array_key_exists($index, $this->params)) { 151 | return null; 152 | } 153 | 154 | return $this->params[$index]; 155 | } 156 | 157 | /** 158 | * Retrieve parameters. 159 | * 160 | * @return array 161 | */ 162 | public function getParams() 163 | { 164 | return $this->params; 165 | } 166 | 167 | /** 168 | * Set request method. 169 | * 170 | * @param string $name 171 | * @return self 172 | */ 173 | public function setMethod($name) 174 | { 175 | if (! preg_match($this->methodRegex, $name)) { 176 | $this->isMethodError = true; 177 | return $this; 178 | } 179 | 180 | $this->method = $name; 181 | return $this; 182 | } 183 | 184 | /** 185 | * Get request method name. 186 | * 187 | * @return string 188 | */ 189 | public function getMethod() 190 | { 191 | return $this->method; 192 | } 193 | 194 | /** 195 | * Was a bad method provided? 196 | * 197 | * @return bool 198 | */ 199 | public function isMethodError() 200 | { 201 | return $this->isMethodError; 202 | } 203 | 204 | /** 205 | * Was a malformed JSON provided? 206 | * 207 | * @return bool 208 | */ 209 | public function isParseError() 210 | { 211 | return $this->isParseError; 212 | } 213 | 214 | /** 215 | * Set request identifier 216 | * 217 | * @param mixed $name 218 | * @return self 219 | */ 220 | public function setId($name) 221 | { 222 | $this->id = (string) $name; 223 | return $this; 224 | } 225 | 226 | /** 227 | * Retrieve request identifier. 228 | * 229 | * @return string 230 | */ 231 | public function getId() 232 | { 233 | return $this->id; 234 | } 235 | 236 | /** 237 | * Set JSON-RPC version 238 | * 239 | * @param string $version 240 | * @return self 241 | */ 242 | public function setVersion($version) 243 | { 244 | if ('2.0' === $version) { 245 | $this->version = '2.0'; 246 | return $this; 247 | } 248 | 249 | $this->version = '1.0'; 250 | return $this; 251 | } 252 | 253 | /** 254 | * Retrieve JSON-RPC version. 255 | * 256 | * @return string 257 | */ 258 | public function getVersion() 259 | { 260 | return $this->version; 261 | } 262 | 263 | /** 264 | * Set request state based on JSON. 265 | * 266 | * @param string $json 267 | * @return void 268 | */ 269 | public function loadJson($json) 270 | { 271 | try { 272 | $options = Json::decode($json, Json::TYPE_ARRAY); 273 | $this->setOptions($options); 274 | } catch (Exception $e) { 275 | $this->isParseError = true; 276 | } 277 | } 278 | 279 | /** 280 | * Cast request to JSON. 281 | * 282 | * @return string 283 | */ 284 | public function toJson() 285 | { 286 | $jsonArray = [ 287 | 'method' => $this->getMethod(), 288 | ]; 289 | 290 | if (null !== ($id = $this->getId())) { 291 | $jsonArray['id'] = $id; 292 | } 293 | 294 | $params = $this->getParams(); 295 | if (! empty($params)) { 296 | $jsonArray['params'] = $params; 297 | } 298 | 299 | if ('2.0' === $this->getVersion()) { 300 | $jsonArray['jsonrpc'] = '2.0'; 301 | } 302 | 303 | return Json::encode($jsonArray); 304 | } 305 | 306 | /** 307 | * Cast request to string (JSON) 308 | * 309 | * @return string 310 | */ 311 | public function __toString() 312 | { 313 | return $this->toJson(); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/Request/Http.php: -------------------------------------------------------------------------------- 1 | rawJson = $json; 27 | if (! empty($json)) { 28 | $this->loadJson($json); 29 | } 30 | } 31 | 32 | /** 33 | * Get JSON from raw POST body 34 | * 35 | * @return string 36 | */ 37 | public function getRawJson() 38 | { 39 | return $this->rawJson; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | $value) { 72 | $method = 'set' . ucfirst((string) $key); 73 | if (in_array($method, $methods)) { 74 | $this->$method($value); 75 | continue; 76 | } 77 | 78 | if ('jsonrpc' === $key) { 79 | $this->setVersion($value); 80 | continue; 81 | } 82 | } 83 | return $this; 84 | } 85 | 86 | /** 87 | * Set response state based on JSON. 88 | * 89 | * @param string $json 90 | * @return void 91 | * @throws Exception\RuntimeException 92 | */ 93 | public function loadJson($json) 94 | { 95 | try { 96 | $options = Json::decode($json, Json::TYPE_ARRAY); 97 | } catch (RuntimeException $e) { 98 | throw new Exception\RuntimeException( 99 | 'json is not a valid response; array expected', 100 | $e->getCode(), 101 | $e 102 | ); 103 | } 104 | 105 | if (! is_array($options)) { 106 | throw new Exception\RuntimeException('json is not a valid response; array expected'); 107 | } 108 | 109 | $this->setOptions($options); 110 | } 111 | 112 | /** 113 | * Set result. 114 | * 115 | * @param mixed $value 116 | * @return self 117 | */ 118 | public function setResult($value) 119 | { 120 | $this->result = $value; 121 | return $this; 122 | } 123 | 124 | /** 125 | * Get result. 126 | * 127 | * @return mixed 128 | */ 129 | public function getResult() 130 | { 131 | return $this->result; 132 | } 133 | 134 | /** 135 | * Set result error 136 | * 137 | * RPC error, if response results in fault. 138 | * 139 | * @param mixed $error 140 | * @return self 141 | */ 142 | public function setError(?Error $error = null) 143 | { 144 | $this->error = $error; 145 | return $this; 146 | } 147 | 148 | /** 149 | * Get response error 150 | * 151 | * @return null|Error 152 | */ 153 | public function getError() 154 | { 155 | return $this->error; 156 | } 157 | 158 | /** 159 | * Is the response an error? 160 | * 161 | * @return bool 162 | */ 163 | public function isError() 164 | { 165 | return $this->getError() instanceof Error; 166 | } 167 | 168 | /** 169 | * Set request ID 170 | * 171 | * @param mixed $name 172 | * @return self 173 | */ 174 | public function setId($name) 175 | { 176 | $this->id = $name; 177 | return $this; 178 | } 179 | 180 | /** 181 | * Get request ID. 182 | * 183 | * @return mixed 184 | */ 185 | public function getId() 186 | { 187 | return $this->id; 188 | } 189 | 190 | /** 191 | * Set JSON-RPC version. 192 | * 193 | * @param string $version 194 | * @return self 195 | */ 196 | public function setVersion($version) 197 | { 198 | $version = (string) $version; 199 | if ('2.0' === $version) { 200 | $this->version = '2.0'; 201 | return $this; 202 | } 203 | 204 | $this->version = null; 205 | return $this; 206 | } 207 | 208 | /** 209 | * Retrieve JSON-RPC version 210 | * 211 | * @return null|string 212 | */ 213 | public function getVersion() 214 | { 215 | return $this->version; 216 | } 217 | 218 | /** 219 | * Cast to JSON 220 | * 221 | * @return string 222 | */ 223 | public function toJson() 224 | { 225 | $response = ['id' => $this->getId()]; 226 | 227 | if ($this->isError()) { 228 | $response['error'] = $this->getError()->toArray(); 229 | } else { 230 | $response['result'] = $this->getResult(); 231 | } 232 | 233 | if (null !== ($version = $this->getVersion())) { 234 | $response['jsonrpc'] = $version; 235 | } 236 | 237 | return Json::encode($response); 238 | } 239 | 240 | /** 241 | * Retrieve args. 242 | * 243 | * @return mixed 244 | */ 245 | public function getArgs() 246 | { 247 | return $this->args; 248 | } 249 | 250 | /** 251 | * Set args. 252 | * 253 | * @param mixed $args 254 | * @return self 255 | */ 256 | public function setArgs($args) 257 | { 258 | $this->args = $args; 259 | return $this; 260 | } 261 | 262 | /** 263 | * Set service map object. 264 | * 265 | * @param Smd $serviceMap 266 | * @return self 267 | */ 268 | public function setServiceMap($serviceMap) 269 | { 270 | $this->serviceMap = $serviceMap; 271 | return $this; 272 | } 273 | 274 | /** 275 | * Retrieve service map. 276 | * 277 | * @return Smd|null 278 | */ 279 | public function getServiceMap() 280 | { 281 | return $this->serviceMap; 282 | } 283 | 284 | /** 285 | * Cast to string (JSON). 286 | * 287 | * @return string 288 | */ 289 | public function __toString() 290 | { 291 | return $this->toJson(); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/Response/Http.php: -------------------------------------------------------------------------------- 1 | sendHeaders(); 26 | 27 | if (! $this->isError() && null === $this->getId()) { 28 | return ''; 29 | } 30 | 31 | return parent::toJson(); 32 | } 33 | 34 | /** 35 | * Send headers 36 | * 37 | * If headers are already sent, do nothing. 38 | * 39 | * If null ID, send HTTP 204 header. 40 | * 41 | * Otherwise, send content type header based on content type of service 42 | * map. 43 | * 44 | * @return void 45 | */ 46 | public function sendHeaders() 47 | { 48 | if (headers_sent()) { 49 | return; 50 | } 51 | 52 | if (! $this->isError() && (null === $this->getId())) { 53 | header('HTTP/1.1 204 No Content'); 54 | return; 55 | } 56 | 57 | if (null === ($smd = $this->getServiceMap())) { 58 | return; 59 | } 60 | 61 | $contentType = $smd->getContentType(); 62 | if (! empty($contentType)) { 63 | header('Content-Type: ' . $contentType); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Server.php: -------------------------------------------------------------------------------- 1 | getMethods(); 116 | $found = false; 117 | foreach ($methods as $method) { 118 | if ($action === $method->getName()) { 119 | $found = true; 120 | break; 121 | } 122 | } 123 | if (! $found) { 124 | $this->fault('Method not found', Error::ERROR_INVALID_METHOD); 125 | return $this; 126 | } 127 | } 128 | 129 | $definition = $this->_buildSignature($method, $class); 130 | $this->addMethodServiceMap($definition); 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * Register a class with the server. 137 | * 138 | * @param string $class 139 | * @param string $namespace Ignored 140 | * @param mixed $argv Ignored 141 | * @return Server 142 | */ 143 | public function setClass($class, $namespace = '', $argv = null) 144 | { 145 | if (2 < func_num_args()) { 146 | $argv = func_get_args(); 147 | $argv = array_slice($argv, 2); 148 | } 149 | 150 | $reflection = Reflection::reflectClass($class, $argv, $namespace); 151 | 152 | foreach ($reflection->getMethods() as $method) { 153 | $definition = $this->_buildSignature($method, $class); 154 | $this->addMethodServiceMap($definition); 155 | } 156 | 157 | return $this; 158 | } 159 | 160 | /** 161 | * Indicate fault response. 162 | * 163 | * @param string $fault 164 | * @param int $code 165 | * @param mixed $data 166 | * @return Error 167 | */ 168 | public function fault($fault = null, $code = 404, $data = null) 169 | { 170 | $error = new Error($fault, $code, $data); 171 | $this->getResponse()->setError($error); 172 | return $error; 173 | } 174 | 175 | /** 176 | * Handle request. 177 | * 178 | * @param Request|false $request 179 | * @return null|Response 180 | * @throws Exception\InvalidArgumentException 181 | */ 182 | public function handle($request = false) 183 | { 184 | if ((false !== $request) && ! $request instanceof Request) { 185 | throw new Exception\InvalidArgumentException('Invalid request type provided; cannot handle'); 186 | } 187 | 188 | if ($request) { 189 | $this->setRequest($request); 190 | } 191 | 192 | // Handle request 193 | $this->handleRequest(); 194 | 195 | // Get response 196 | $response = $this->getReadyResponse(); 197 | 198 | // Emit response? 199 | if (! $this->returnResponse) { 200 | echo $response; 201 | return; 202 | } 203 | 204 | // or return it? 205 | return $response; 206 | } 207 | 208 | /** 209 | * Load function definitions 210 | * 211 | * @param array|Definition $definition 212 | * @throws Exception\InvalidArgumentException 213 | * @return void 214 | */ 215 | public function loadFunctions($definition) 216 | { 217 | if (! is_array($definition) && ! $definition instanceof Definition) { 218 | throw new Exception\InvalidArgumentException('Invalid definition provided to loadFunctions()'); 219 | } 220 | 221 | foreach ($definition as $key => $method) { 222 | $this->table->addMethod($method, $key); 223 | $this->addMethodServiceMap($method); 224 | } 225 | } 226 | 227 | /** 228 | * Cache/persist server (unused) 229 | * 230 | * @param int $mode 231 | * @return void 232 | */ 233 | public function setPersistence($mode) 234 | { 235 | } 236 | 237 | /** 238 | * Set request object 239 | * 240 | * @return self 241 | */ 242 | public function setRequest(Request $request) 243 | { 244 | $this->request = $request; 245 | return $this; 246 | } 247 | 248 | /** 249 | * Get JSON-RPC request object. 250 | * 251 | * Lazy-loads an instance if none previously available. 252 | * 253 | * @return Request 254 | */ 255 | public function getRequest() 256 | { 257 | if (null === $this->request) { 258 | $this->setRequest(new Request\Http()); 259 | } 260 | 261 | return $this->request; 262 | } 263 | 264 | /** 265 | * Set response object. 266 | * 267 | * @return self 268 | */ 269 | public function setResponse(Response $response) 270 | { 271 | $this->response = $response; 272 | return $this; 273 | } 274 | 275 | /** 276 | * Get response object. 277 | * 278 | * Lazy-loads an instance if none previously available. 279 | * 280 | * @return Response 281 | */ 282 | public function getResponse() 283 | { 284 | if (null === $this->response) { 285 | $this->setResponse(new Response\Http()); 286 | } 287 | 288 | return $this->response; 289 | } 290 | 291 | /** 292 | * Set return response flag. 293 | * 294 | * If true, {@link handle()} will return the response instead of 295 | * automatically sending it back to the requesting client. 296 | * 297 | * The response is always available via {@link getResponse()}. 298 | * 299 | * @param bool $flag 300 | * @return self 301 | */ 302 | public function setReturnResponse($flag = true) 303 | { 304 | $this->returnResponse = (bool) $flag; 305 | return $this; 306 | } 307 | 308 | /** 309 | * Retrieve return response flag. 310 | * 311 | * @return bool 312 | */ 313 | public function getReturnResponse() 314 | { 315 | return $this->returnResponse; 316 | } 317 | 318 | /** 319 | * Overload to accessors of SMD object. 320 | * 321 | * @param string $method 322 | * @param array $args 323 | * @return mixed 324 | */ 325 | public function __call($method, $args) 326 | { 327 | if (! preg_match('/^(set|get)/', $method, $matches)) { 328 | return; 329 | } 330 | 331 | if (! in_array($method, $this->getSmdMethods())) { 332 | return; 333 | } 334 | 335 | if ('set' === $matches[1]) { 336 | $value = array_shift($args); 337 | $this->getServiceMap()->$method($value); 338 | return $this; 339 | } 340 | 341 | return $this->getServiceMap()->$method(); 342 | } 343 | 344 | /** 345 | * Retrieve SMD object. 346 | * 347 | * Lazy loads an instance if not previously set. 348 | * 349 | * @return Smd 350 | */ 351 | public function getServiceMap() 352 | { 353 | if (null === $this->serviceMap) { 354 | $this->serviceMap = new Smd(); 355 | } 356 | return $this->serviceMap; 357 | } 358 | 359 | /** 360 | * Add service method to service map. 361 | * 362 | * @return void 363 | */ 364 | protected function addMethodServiceMap(Method\Definition $method) 365 | { 366 | $serviceInfo = [ 367 | 'name' => $method->getName(), 368 | 'return' => $this->getReturnType($method), 369 | ]; 370 | 371 | $params = $this->getParams($method); 372 | $serviceInfo['params'] = $params; 373 | $serviceMap = $this->getServiceMap(); 374 | 375 | if (false !== $serviceMap->getService($serviceInfo['name'])) { 376 | $serviceMap->removeService($serviceInfo['name']); 377 | } 378 | 379 | $serviceMap->addService($serviceInfo); 380 | } 381 | 382 | // @codingStandardsIgnoreStart 383 | /** 384 | * Translate PHP type to JSON type. 385 | * 386 | * Method defined in AbstractServer as abstract and implemented here. 387 | * 388 | * @param string $type 389 | * @return string 390 | */ 391 | protected function _fixType($type) 392 | { 393 | return $type; 394 | } 395 | // @codingStandardsIgnoreEnd 396 | 397 | /** 398 | * Get default params from signature. 399 | * 400 | * @param array $args 401 | * @param array $params 402 | * @return array 403 | */ 404 | protected function getDefaultParams(array $args, array $params) 405 | { 406 | if (false === $this->isAssociative($args)) { 407 | $params = array_slice($params, count($args)); 408 | } 409 | 410 | foreach ($params as $param) { 411 | if (isset($args[$param['name']]) || ! array_key_exists('default', $param)) { 412 | continue; 413 | } 414 | 415 | $args[$param['name']] = $param['default']; 416 | } 417 | 418 | return $args; 419 | } 420 | 421 | /** 422 | * Check whether array is associative or not. 423 | * 424 | * @param array $array 425 | * @return bool 426 | */ 427 | private function isAssociative(array $array) 428 | { 429 | $keys = array_keys($array); 430 | return $keys !== array_keys($keys); 431 | } 432 | 433 | /** 434 | * Get method param type. 435 | * 436 | * @return string|array 437 | */ 438 | protected function getParams(Method\Definition $method) 439 | { 440 | $params = []; 441 | foreach ($method->getPrototypes() as $prototype) { 442 | foreach ($prototype->getParameterObjects() as $key => $parameter) { 443 | if (! isset($params[$key])) { 444 | $params[$key] = [ 445 | 'type' => $parameter->getType(), 446 | 'name' => $parameter->getName(), 447 | 'optional' => $parameter->isOptional(), 448 | ]; 449 | 450 | if (null !== ($default = $parameter->getDefaultValue())) { 451 | $params[$key]['default'] = $default; 452 | } 453 | 454 | $description = $parameter->getDescription(); 455 | 456 | if (! empty($description)) { 457 | $params[$key]['description'] = $description; 458 | } 459 | 460 | continue; 461 | } 462 | 463 | $newType = $parameter->getType(); 464 | 465 | if ( 466 | is_array($params[$key]['type']) 467 | && in_array($newType, $params[$key]['type']) 468 | ) { 469 | continue; 470 | } 471 | 472 | if ( 473 | ! is_array($params[$key]['type']) 474 | && $params[$key]['type'] === $newType 475 | ) { 476 | continue; 477 | } 478 | 479 | if (! is_array($params[$key]['type'])) { 480 | $params[$key]['type'] = (array) $params[$key]['type']; 481 | } 482 | 483 | array_push($params[$key]['type'], $parameter->getType()); 484 | } 485 | } 486 | 487 | return $params; 488 | } 489 | 490 | /** 491 | * Set response state. 492 | * 493 | * @return Response 494 | */ 495 | protected function getReadyResponse() 496 | { 497 | $request = $this->getRequest(); 498 | $response = $this->getResponse(); 499 | $response->setServiceMap($this->getServiceMap()); 500 | 501 | if (null !== ($id = $request->getId())) { 502 | $response->setId($id); 503 | } 504 | 505 | if (null !== ($version = $request->getVersion())) { 506 | $response->setVersion($version); 507 | } 508 | 509 | return $response; 510 | } 511 | 512 | /** 513 | * Get method return type. 514 | * 515 | * @return string|array 516 | */ 517 | protected function getReturnType(Method\Definition $method) 518 | { 519 | $return = []; 520 | foreach ($method->getPrototypes() as $prototype) { 521 | $return[] = $prototype->getReturnType(); 522 | } 523 | 524 | if (1 === count($return)) { 525 | return $return[0]; 526 | } 527 | 528 | return $return; 529 | } 530 | 531 | /** 532 | * Retrieve list of allowed SMD methods for proxying. 533 | * 534 | * Lazy-loads the list on first retrieval. 535 | * 536 | * @return array 537 | */ 538 | protected function getSmdMethods() 539 | { 540 | if (null !== $this->smdMethods) { 541 | return $this->smdMethods; 542 | } 543 | 544 | $this->smdMethods = []; 545 | 546 | foreach (get_class_methods(Smd::class) as $method) { 547 | if (! preg_match('/^(set|get)/', $method)) { 548 | continue; 549 | } 550 | 551 | if (strstr($method, 'Service')) { 552 | continue; 553 | } 554 | 555 | $this->smdMethods[] = $method; 556 | } 557 | 558 | return $this->smdMethods; 559 | } 560 | 561 | /** 562 | * Internal method for handling request. 563 | * 564 | * @return Error|null 565 | */ 566 | protected function handleRequest() 567 | { 568 | $request = $this->getRequest(); 569 | 570 | if ($request->isParseError()) { 571 | return $this->fault('Parse error', Error::ERROR_PARSE); 572 | } 573 | 574 | if (! $request->isMethodError() && null === $request->getMethod()) { 575 | return $this->fault('Invalid Request', Error::ERROR_INVALID_REQUEST); 576 | } 577 | 578 | if ($request->isMethodError()) { 579 | return $this->fault('Invalid Request', Error::ERROR_INVALID_REQUEST); 580 | } 581 | 582 | $method = $request->getMethod(); 583 | if (! $this->table->hasMethod($method)) { 584 | return $this->fault('Method not found', Error::ERROR_INVALID_METHOD); 585 | } 586 | 587 | $invokable = $this->table->getMethod($method); 588 | $serviceMap = $this->getServiceMap(); 589 | $service = $serviceMap->getService($method); 590 | $params = $this->validateAndPrepareParams( 591 | $request->getParams(), 592 | $service->getParams(), 593 | $invokable 594 | ); 595 | 596 | if ($params instanceof Error) { 597 | return $params; 598 | } 599 | 600 | try { 601 | $result = $this->_dispatch($invokable, $params); 602 | } catch (PhpException $e) { 603 | return $this->fault($e->getMessage(), $e->getCode(), $e); 604 | } 605 | 606 | $this->getResponse()->setResult($result); 607 | 608 | return null; 609 | } 610 | 611 | /** 612 | * @param array $requestedParams 613 | * @param array $serviceParams 614 | * @return array|Error Array of parameters to use when calling the requested 615 | * method on success, Error if there is a mismatch between request 616 | * parameters and the method signature. 617 | */ 618 | private function validateAndPrepareParams( 619 | array $requestedParams, 620 | array $serviceParams, 621 | Method\Definition $invokable 622 | ) { 623 | return is_string(key($requestedParams)) 624 | ? $this->validateAndPrepareNamedParams($requestedParams, $serviceParams, $invokable) 625 | : $this->validateAndPrepareOrderedParams($requestedParams, $serviceParams); 626 | } 627 | 628 | /** 629 | * Ensures named parameters are passed in the correct order. 630 | * 631 | * @param array $requestedParams 632 | * @param array $serviceParams 633 | * @return array|Error Array of parameters to use when calling the requested 634 | * method on success, Error if any named request parameters do not match 635 | * those of the method requested. 636 | */ 637 | private function validateAndPrepareNamedParams( 638 | array $requestedParams, 639 | array $serviceParams, 640 | Method\Definition $invokable 641 | ) { 642 | if (count($requestedParams) < count($serviceParams)) { 643 | $requestedParams = $this->getDefaultParams($requestedParams, $serviceParams); 644 | } 645 | 646 | $callback = $invokable->getCallback(); 647 | $reflection = 'function' === $callback->getType() 648 | ? new ReflectionFunction($callback->getFunction()) 649 | : new ReflectionMethod($callback->getClass(), $callback->getMethod()); 650 | 651 | $orderedParams = []; 652 | foreach ($reflection->getParameters() as $refParam) { 653 | if (array_key_exists($refParam->getName(), $requestedParams)) { 654 | $orderedParams[$refParam->getName()] = $requestedParams[$refParam->getName()]; 655 | continue; 656 | } 657 | 658 | if ($refParam->isOptional()) { 659 | $orderedParams[$refParam->getName()] = null; 660 | continue; 661 | } 662 | 663 | return $this->fault('Invalid params', Error::ERROR_INVALID_PARAMS); 664 | } 665 | 666 | return $orderedParams; 667 | } 668 | 669 | /** 670 | * @param array $requestedParams 671 | * @param array $serviceParams 672 | * @return array|Error Array of parameters to use when calling the requested 673 | * method on success, Error if the number of request parameters does not 674 | * match the number of parameters required by the requested method. 675 | */ 676 | private function validateAndPrepareOrderedParams(array $requestedParams, array $serviceParams) 677 | { 678 | $requiredParamsCount = array_reduce($serviceParams, static function ($count, $param) { 679 | $count += $param['optional'] ? 0 : 1; 680 | return $count; 681 | }, 0); 682 | 683 | if (count($requestedParams) < $requiredParamsCount) { 684 | return $this->fault('Invalid params', Error::ERROR_INVALID_PARAMS); 685 | } 686 | 687 | return $requestedParams; 688 | } 689 | } 690 | -------------------------------------------------------------------------------- /src/Smd.php: -------------------------------------------------------------------------------- 1 | $value) { 115 | $method = 'set' . ucfirst($key); 116 | 117 | if (method_exists($this, $method)) { 118 | $this->$method($value); 119 | } 120 | } 121 | 122 | return $this; 123 | } 124 | 125 | /** 126 | * Set transport. 127 | * 128 | * @param string $transport 129 | * @return self 130 | * @throws InvalidArgumentException 131 | */ 132 | public function setTransport($transport) 133 | { 134 | if (! in_array($transport, $this->transportTypes)) { 135 | throw new InvalidArgumentException("Invalid transport '{$transport}' specified"); 136 | } 137 | 138 | $this->transport = $transport; 139 | return $this; 140 | } 141 | 142 | /** 143 | * Get transport. 144 | * 145 | * @return string 146 | */ 147 | public function getTransport() 148 | { 149 | return $this->transport; 150 | } 151 | 152 | /** 153 | * Set envelope. 154 | * 155 | * @param string $envelopeType 156 | * @return self 157 | * @throws InvalidArgumentException 158 | */ 159 | public function setEnvelope($envelopeType) 160 | { 161 | if (! in_array($envelopeType, $this->envelopeTypes)) { 162 | throw new InvalidArgumentException("Invalid envelope type '{$envelopeType}'"); 163 | } 164 | 165 | $this->envelope = $envelopeType; 166 | return $this; 167 | } 168 | 169 | /** 170 | * Retrieve envelope. 171 | * 172 | * @return string 173 | */ 174 | public function getEnvelope() 175 | { 176 | return $this->envelope; 177 | } 178 | 179 | /** 180 | * Set content type 181 | * 182 | * @param string $type 183 | * @return self 184 | * @throws InvalidArgumentException 185 | */ 186 | public function setContentType($type) 187 | { 188 | if (! preg_match($this->contentTypeRegex, $type)) { 189 | throw new InvalidArgumentException("Invalid content type '{$type}' specified"); 190 | } 191 | 192 | $this->contentType = $type; 193 | return $this; 194 | } 195 | 196 | /** 197 | * Retrieve content type 198 | * 199 | * Content-Type of response; default to application/json. 200 | * 201 | * @return string 202 | */ 203 | public function getContentType() 204 | { 205 | return $this->contentType; 206 | } 207 | 208 | /** 209 | * Set service target. 210 | * 211 | * @param string $target 212 | * @return self 213 | */ 214 | public function setTarget($target) 215 | { 216 | $this->target = (string) $target; 217 | return $this; 218 | } 219 | 220 | /** 221 | * Retrieve service target. 222 | * 223 | * @return string 224 | */ 225 | public function getTarget() 226 | { 227 | return $this->target; 228 | } 229 | 230 | /** 231 | * Set service ID. 232 | * 233 | * @param string $id 234 | * @return self 235 | */ 236 | public function setId($id) 237 | { 238 | $this->id = (string) $id; 239 | return $this; 240 | } 241 | 242 | /** 243 | * Get service id. 244 | * 245 | * @return string 246 | */ 247 | public function getId() 248 | { 249 | return $this->id; 250 | } 251 | 252 | /** 253 | * Set service description. 254 | * 255 | * @param string $description 256 | * @return self 257 | */ 258 | public function setDescription($description) 259 | { 260 | $this->description = (string) $description; 261 | return $this; 262 | } 263 | 264 | /** 265 | * Get service description. 266 | * 267 | * @return string 268 | */ 269 | public function getDescription() 270 | { 271 | return $this->description; 272 | } 273 | 274 | /** 275 | * Indicate whether or not to generate Dojo-compatible SMD. 276 | * 277 | * @param bool $flag 278 | * @return self 279 | */ 280 | public function setDojoCompatible($flag) 281 | { 282 | $this->dojoCompatible = (bool) $flag; 283 | return $this; 284 | } 285 | 286 | /** 287 | * Is this a Dojo compatible SMD? 288 | * 289 | * @return bool 290 | */ 291 | public function isDojoCompatible() 292 | { 293 | return $this->dojoCompatible; 294 | } 295 | 296 | /** 297 | * Add Service. 298 | * 299 | * @param Smd\Service|array $service 300 | * @return self 301 | * @throws RuntimeException 302 | * @throws InvalidArgumentException 303 | */ 304 | public function addService($service) 305 | { 306 | if (is_array($service)) { 307 | $service = new Smd\Service($service); 308 | } 309 | 310 | if (! $service instanceof Smd\Service) { 311 | throw new InvalidArgumentException('Invalid service passed to addService()'); 312 | } 313 | 314 | $name = $service->getName(); 315 | 316 | if (array_key_exists($name, $this->services)) { 317 | throw new RuntimeException('Attempt to register a service already registered detected'); 318 | } 319 | 320 | $this->services[$name] = $service; 321 | return $this; 322 | } 323 | 324 | /** 325 | * Add many services. 326 | * 327 | * @param array $services 328 | * @return self 329 | */ 330 | public function addServices(array $services) 331 | { 332 | foreach ($services as $service) { 333 | $this->addService($service); 334 | } 335 | 336 | return $this; 337 | } 338 | 339 | /** 340 | * Overwrite existing services with new ones. 341 | * 342 | * @param array $services 343 | * @return self 344 | */ 345 | public function setServices(array $services) 346 | { 347 | $this->services = []; 348 | return $this->addServices($services); 349 | } 350 | 351 | /** 352 | * Get service object. 353 | * 354 | * @param string $name 355 | * @return bool|Smd\Service 356 | */ 357 | public function getService($name) 358 | { 359 | if (! array_key_exists($name, $this->services)) { 360 | return false; 361 | } 362 | 363 | return $this->services[$name]; 364 | } 365 | 366 | /** 367 | * Return services. 368 | * 369 | * @return array 370 | */ 371 | public function getServices() 372 | { 373 | return $this->services; 374 | } 375 | 376 | /** 377 | * Remove service. 378 | * 379 | * @param string $name 380 | * @return bool 381 | */ 382 | public function removeService($name) 383 | { 384 | if (! array_key_exists($name, $this->services)) { 385 | return false; 386 | } 387 | 388 | unset($this->services[$name]); 389 | return true; 390 | } 391 | 392 | /** 393 | * Cast to array. 394 | * 395 | * @return array 396 | */ 397 | public function toArray() 398 | { 399 | if ($this->isDojoCompatible()) { 400 | return $this->toDojoArray(); 401 | } 402 | 403 | $description = $this->getDescription(); 404 | $transport = $this->getTransport(); 405 | $envelope = $this->getEnvelope(); 406 | $contentType = $this->getContentType(); 407 | $smdVersion = static::SMD_VERSION; 408 | assert(is_string($smdVersion)); 409 | $service = [ 410 | 'transport' => $transport, 411 | 'envelope' => $envelope, 412 | 'contentType' => $contentType, 413 | 'SMDVersion' => $smdVersion, 414 | 'description' => $description, 415 | ]; 416 | 417 | if (null !== ($target = $this->getTarget())) { 418 | $service['target'] = $target; 419 | } 420 | if (null !== ($id = $this->getId())) { 421 | $service['id'] = $id; 422 | } 423 | 424 | $services = $this->getServices(); 425 | if (empty($services)) { 426 | return $service; 427 | } 428 | 429 | $service['services'] = []; 430 | foreach ($services as $name => $svc) { 431 | $svc->setEnvelope($envelope); 432 | $service['services'][$name] = $svc->toArray(); 433 | } 434 | $service['methods'] = $service['services']; 435 | 436 | return $service; 437 | } 438 | 439 | /** 440 | * Export to DOJO-compatible SMD array 441 | * 442 | * @return array 443 | */ 444 | public function toDojoArray() 445 | { 446 | $smdVersion = '.1'; 447 | $serviceType = 'JSON-RPC'; 448 | $service = ['SMDVersion' => $smdVersion, 'serviceType' => $serviceType]; 449 | $target = $this->getTarget(); 450 | $services = $this->getServices(); 451 | 452 | if (empty($services)) { 453 | return $service; 454 | } 455 | 456 | $service['methods'] = []; 457 | foreach ($services as $name => $svc) { 458 | $method = [ 459 | 'name' => $name, 460 | 'serviceURL' => $target, 461 | ]; 462 | 463 | $params = []; 464 | foreach ($svc->getParams() as $param) { 465 | $params[] = [ 466 | 'name' => array_key_exists('name', $param) ? $param['name'] : $param['type'], 467 | 'type' => $param['type'], 468 | ]; 469 | } 470 | 471 | if (! empty($params)) { 472 | $method['parameters'] = $params; 473 | } 474 | 475 | $service['methods'][] = $method; 476 | } 477 | 478 | return $service; 479 | } 480 | 481 | /** 482 | * Cast to JSON. 483 | * 484 | * @return string 485 | */ 486 | public function toJson() 487 | { 488 | return Json::encode($this->toArray()); 489 | } 490 | 491 | /** 492 | * Cast to string (JSON) 493 | * 494 | * @return string 495 | */ 496 | public function __toString() 497 | { 498 | return $this->toJson(); 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /src/Smd/Service.php: -------------------------------------------------------------------------------- 1 | 'is_string', 73 | 'optional' => 'is_bool', 74 | 'default' => null, 75 | 'description' => 'is_string', 76 | ]; 77 | 78 | /** 79 | * Service params. 80 | * 81 | * @var array 82 | */ 83 | protected $params = []; 84 | 85 | /** 86 | * Mapping of parameter types to JSON-RPC types. 87 | * 88 | * @var array 89 | */ 90 | protected $paramMap = [ 91 | 'any' => 'any', 92 | 'arr' => 'array', 93 | 'array' => 'array', 94 | 'assoc' => 'object', 95 | 'bool' => 'boolean', 96 | 'boolean' => 'boolean', 97 | 'dbl' => 'float', 98 | 'double' => 'float', 99 | 'false' => 'boolean', 100 | 'float' => 'float', 101 | 'hash' => 'object', 102 | 'integer' => 'integer', 103 | 'int' => 'integer', 104 | 'mixed' => 'any', 105 | 'nil' => 'null', 106 | 'null' => 'null', 107 | 'object' => 'object', 108 | 'string' => 'string', 109 | 'str' => 'string', 110 | 'struct' => 'object', 111 | 'true' => 'boolean', 112 | 'void' => 'null', 113 | ]; 114 | 115 | /** 116 | * Allowed transport types. 117 | * 118 | * @var array 119 | */ 120 | protected $transportTypes = [ 121 | 'POST', 122 | ]; 123 | 124 | /** 125 | * @param string|array $spec 126 | * @throws InvalidArgumentException If no name provided. 127 | */ 128 | public function __construct($spec) 129 | { 130 | if (is_string($spec)) { 131 | $this->setName($spec); 132 | } elseif (is_array($spec)) { 133 | $this->setOptions($spec); 134 | } 135 | 136 | if ('' === $this->getName()) { 137 | throw new InvalidArgumentException('SMD service description requires a name; none provided'); 138 | } 139 | } 140 | 141 | /** 142 | * Set object state. 143 | * 144 | * @param array $options 145 | * @return self 146 | */ 147 | public function setOptions(array $options) 148 | { 149 | $methods = get_class_methods($this); 150 | foreach ($options as $key => $value) { 151 | if ('options' === strtolower($key)) { 152 | continue; 153 | } 154 | 155 | $method = 'set' . ucfirst($key); 156 | if (in_array($method, $methods)) { 157 | $this->$method($value); 158 | } 159 | } 160 | 161 | return $this; 162 | } 163 | 164 | /** 165 | * Set service name. 166 | * 167 | * @return self 168 | * @throws InvalidArgumentException 169 | */ 170 | public function setName(string $name) 171 | { 172 | if (! preg_match($this->nameRegex, $name)) { 173 | throw new InvalidArgumentException(sprintf( 174 | 'Invalid name "%s" provided for service; must follow PHP method naming conventions', 175 | $name 176 | )); 177 | } 178 | 179 | $this->name = $name; 180 | return $this; 181 | } 182 | 183 | /** 184 | * Retrieve name. 185 | * 186 | * @return string 187 | */ 188 | public function getName() 189 | { 190 | return $this->name; 191 | } 192 | 193 | /** 194 | * Set Transport. 195 | * 196 | * Currently limited to POST. 197 | * 198 | * @param string $transport 199 | * @return self 200 | * @throws InvalidArgumentException 201 | */ 202 | public function setTransport($transport) 203 | { 204 | if (! in_array($transport, $this->transportTypes)) { 205 | throw new InvalidArgumentException(sprintf( 206 | 'Invalid transport "%s"; please select one of (%s)', 207 | $transport, 208 | implode(', ', $this->transportTypes) 209 | )); 210 | } 211 | 212 | $this->transport = $transport; 213 | return $this; 214 | } 215 | 216 | /** 217 | * Get transport. 218 | * 219 | * @return string 220 | */ 221 | public function getTransport() 222 | { 223 | return $this->transport; 224 | } 225 | 226 | /** 227 | * Set service target. 228 | * 229 | * @param string $target 230 | * @return self 231 | */ 232 | public function setTarget($target) 233 | { 234 | $this->target = (string) $target; 235 | return $this; 236 | } 237 | 238 | /** 239 | * Get service target. 240 | * 241 | * @return null|string 242 | */ 243 | public function getTarget() 244 | { 245 | return $this->target; 246 | } 247 | 248 | /** 249 | * Set envelope type. 250 | * 251 | * @param string $envelopeType 252 | * @return self 253 | * @throws InvalidArgumentException 254 | */ 255 | public function setEnvelope($envelopeType) 256 | { 257 | if (! in_array($envelopeType, $this->envelopeTypes)) { 258 | throw new InvalidArgumentException(sprintf( 259 | 'Invalid envelope type "%s"; please specify one of (%s)', 260 | $envelopeType, 261 | implode(', ', $this->envelopeTypes) 262 | )); 263 | } 264 | 265 | $this->envelope = $envelopeType; 266 | return $this; 267 | } 268 | 269 | /** 270 | * Get envelope type. 271 | * 272 | * @return string 273 | */ 274 | public function getEnvelope() 275 | { 276 | return $this->envelope; 277 | } 278 | 279 | /** 280 | * Add a parameter to the service. 281 | * 282 | * @param string|array $type 283 | * @param array $options 284 | * @param int|null $order 285 | * @return self 286 | * @throws InvalidArgumentException 287 | */ 288 | public function addParam($type, array $options = [], $order = null) 289 | { 290 | if (! is_string($type) && ! is_array($type)) { 291 | throw new InvalidArgumentException('Invalid param type provided'); 292 | } 293 | 294 | if (is_string($type)) { 295 | $type = $this->validateParamType($type); 296 | } 297 | 298 | if (is_array($type)) { 299 | foreach ($type as $key => $paramType) { 300 | $type[$key] = $this->validateParamType($paramType); 301 | } 302 | } 303 | 304 | $paramOptions = ['type' => $type]; 305 | foreach ($options as $key => $value) { 306 | if (in_array($key, array_keys($this->paramOptionTypes))) { 307 | if (null !== ($callback = $this->paramOptionTypes[$key])) { 308 | if (! $callback($value)) { 309 | continue; 310 | } 311 | } 312 | $paramOptions[$key] = $value; 313 | } 314 | } 315 | 316 | $this->params[] = [ 317 | 'param' => $paramOptions, 318 | 'order' => $order, 319 | ]; 320 | 321 | return $this; 322 | } 323 | 324 | /** 325 | * Add params. 326 | * 327 | * Each param should be an array, and should include the key 'type'. 328 | * 329 | * @param array $params 330 | * @return self 331 | */ 332 | public function addParams(array $params) 333 | { 334 | ksort($params); 335 | 336 | foreach ($params as $options) { 337 | if (! is_array($options)) { 338 | continue; 339 | } 340 | 341 | if (! array_key_exists('type', $options)) { 342 | continue; 343 | } 344 | 345 | $type = $options['type']; 346 | $order = array_key_exists('order', $options) ? $options['order'] : null; 347 | $this->addParam($type, $options, $order); 348 | } 349 | 350 | return $this; 351 | } 352 | 353 | /** 354 | * Overwrite all parameters. 355 | * 356 | * @param array $params 357 | * @return self 358 | */ 359 | public function setParams(array $params) 360 | { 361 | $this->params = []; 362 | return $this->addParams($params); 363 | } 364 | 365 | /** 366 | * Get all parameters. 367 | * 368 | * Returns all params in specified order. 369 | * 370 | * @return array 371 | */ 372 | public function getParams() 373 | { 374 | $params = []; 375 | $index = 0; 376 | 377 | foreach ($this->params as $param) { 378 | if (null === $param['order']) { 379 | if (array_search($index, array_keys($params), true)) { 380 | ++$index; 381 | } 382 | 383 | $params[$index] = $param['param']; 384 | ++$index; 385 | continue; 386 | } 387 | 388 | $params[$param['order']] = $param['param']; 389 | } 390 | 391 | ksort($params); 392 | return $params; 393 | } 394 | 395 | /** 396 | * Set return type. 397 | * 398 | * @param string|array $type 399 | * @return self 400 | * @throws InvalidArgumentException 401 | */ 402 | public function setReturn($type) 403 | { 404 | if (! is_string($type) && ! is_array($type)) { 405 | throw new InvalidArgumentException("Invalid param type provided ('" . gettype($type) . "')"); 406 | } 407 | 408 | if (is_string($type)) { 409 | $type = $this->validateParamType($type, true); 410 | } 411 | 412 | if (is_array($type)) { 413 | foreach ($type as $key => $returnType) { 414 | $type[$key] = $this->validateParamType($returnType, true); 415 | } 416 | } 417 | 418 | $this->return = $type; 419 | return $this; 420 | } 421 | 422 | /** 423 | * Get return type. 424 | * 425 | * @return null|string|array 426 | */ 427 | public function getReturn() 428 | { 429 | return $this->return; 430 | } 431 | 432 | /** 433 | * Cast service description to array. 434 | * 435 | * @return array 436 | */ 437 | public function toArray() 438 | { 439 | $envelope = $this->getEnvelope(); 440 | $target = $this->getTarget(); 441 | $transport = $this->getTransport(); 442 | $parameters = $this->getParams(); 443 | $returns = $this->getReturn(); 444 | $name = $this->getName(); 445 | 446 | if (empty($target)) { 447 | return [ 448 | 'envelope' => $envelope, 449 | 'transport' => $transport, 450 | 'name' => $name, 451 | 'parameters' => $parameters, 452 | 'returns' => $returns, 453 | ]; 454 | } 455 | 456 | return [ 457 | 'envelope' => $envelope, 458 | 'target' => $target, 459 | 'transport' => $transport, 460 | 'name' => $name, 461 | 'parameters' => $parameters, 462 | 'returns' => $returns, 463 | ]; 464 | } 465 | 466 | /** 467 | * Return JSON encoding of service. 468 | * 469 | * @return string 470 | */ 471 | public function toJson() 472 | { 473 | return Json::encode([ 474 | $this->getName() => $this->toArray(), 475 | ]); 476 | } 477 | 478 | /** 479 | * Cast to string. 480 | * 481 | * @return string 482 | */ 483 | public function __toString() 484 | { 485 | return $this->toJson(); 486 | } 487 | 488 | /** 489 | * Validate parameter type. 490 | * 491 | * @param string $type 492 | * @param bool $isReturn 493 | * @return string 494 | * @throws InvalidArgumentException 495 | */ 496 | protected function validateParamType($type, $isReturn = false) 497 | { 498 | if (! is_string($type)) { 499 | throw new InvalidArgumentException(sprintf( 500 | 'Invalid param type provided ("%s")', 501 | $type 502 | )); 503 | } 504 | 505 | if (! array_key_exists($type, $this->paramMap)) { 506 | $type = 'object'; 507 | } 508 | 509 | $paramType = $this->paramMap[$type]; 510 | if (! $isReturn && ('null' === $paramType)) { 511 | throw new InvalidArgumentException(sprintf( 512 | 'Invalid param type provided ("%s")', 513 | $type 514 | )); 515 | } 516 | 517 | return $paramType; 518 | } 519 | } 520 | --------------------------------------------------------------------------------