├── .github └── workflows │ └── build.yml ├── LICENSE ├── composer.json └── lib ├── Dispatcher.php ├── Error.php ├── ErrorCode.php ├── ErrorResponse.php ├── Message.php ├── Notification.php ├── Request.php ├── Response.php └── SuccessResponse.php /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | php: 14 | - 7.1 15 | - 7.2 16 | - 7.3 17 | - 7.4 18 | - 8.0 19 | deps: 20 | - lowest 21 | - highest 22 | include: 23 | - php: 8.1 24 | deps: highest 25 | composer-options: --ignore-platform-reqs 26 | exclude: 27 | # that config currently breaks as older PHPUnit cannot generate coverage on PHP 8 28 | - php: 8 29 | deps: lowest 30 | 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v2 34 | 35 | - name: Setup PHP 36 | uses: shivammathur/setup-php@v2 37 | with: 38 | php-version: ${{ matrix.php }} 39 | 40 | - uses: ramsey/composer-install@v1 41 | with: 42 | dependency-versions: ${{ matrix.deps }} 43 | composer-options: ${{ matrix.composer-options }} 44 | 45 | - run: vendor/bin/phpunit --coverage-clover=coverage.xml --whitelist lib --bootstrap vendor/autoload.php tests 46 | 47 | - uses: codecov/codecov-action@v1 48 | 49 | release: 50 | needs: test 51 | if: github.repository_owner == 'felixfbecker' && github.event_name == 'push' && github.ref == 'refs/heads/master' 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | 56 | - name: Setup Node.js 57 | uses: actions/setup-node@v2 58 | 59 | - name: Install npm dependencies 60 | run: npm ci 61 | 62 | - name: Release 63 | run: npm run semantic-release 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2016, Felix Frederick Becker 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "felixfbecker/advanced-json-rpc", 3 | "description": "A more advanced JSONRPC implementation", 4 | "type": "library", 5 | "license": "ISC", 6 | "authors": [ 7 | { 8 | "name": "Felix Becker", 9 | "email": "felix.b@outlook.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "AdvancedJsonRpc\\": "lib/" 15 | } 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "AdvancedJsonRpc\\Tests\\": "tests/" 20 | } 21 | }, 22 | "require": { 23 | "php": "^7.1 || ^8.0", 24 | "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", 25 | "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^7.0 || ^8.0" 29 | }, 30 | "minimum-stability": "dev", 31 | "prefer-stable": true 32 | } 33 | -------------------------------------------------------------------------------- /lib/Dispatcher.php: -------------------------------------------------------------------------------- 1 | ReflectionMethod[] 28 | * 29 | * @var ReflectionMethod 30 | */ 31 | private $methods; 32 | 33 | /** 34 | * @var \phpDocumentor\Reflection\DocBlockFactory 35 | */ 36 | private $docBlockFactory; 37 | 38 | /** 39 | * @var \phpDocumentor\Reflection\Types\ContextFactory 40 | */ 41 | private $contextFactory; 42 | 43 | /** 44 | * @param object $target The target object that should receive the method calls 45 | * @param string $delimiter A delimiter for method calls on properties, for example someProperty->someMethod 46 | */ 47 | public function __construct($target, $delimiter = '->') 48 | { 49 | $this->target = $target; 50 | $this->delimiter = $delimiter; 51 | $this->docBlockFactory = DocBlockFactory::createInstance(); 52 | $this->contextFactory = new Types\ContextFactory(); 53 | $this->mapper = new JsonMapper(); 54 | } 55 | 56 | /** 57 | * Calls the appropriate method handler for an incoming Message 58 | * 59 | * @param string|object $msg The incoming message 60 | * @return mixed 61 | */ 62 | public function dispatch($msg) 63 | { 64 | if (is_string($msg)) { 65 | $msg = json_decode($msg); 66 | if (json_last_error() !== JSON_ERROR_NONE) { 67 | throw new Error(json_last_error_msg(), ErrorCode::PARSE_ERROR); 68 | } 69 | } 70 | // Find out the object and function that should be called 71 | $obj = $this->target; 72 | $parts = explode($this->delimiter, $msg->method); 73 | // The function to call is always the last part of the method 74 | $fn = array_pop($parts); 75 | // For namespaced methods like textDocument/didOpen, call the didOpen method on the $textDocument property 76 | // For simple methods like initialize, shutdown, exit, this loop will simply not be entered and $obj will be 77 | // the target 78 | foreach ($parts as $part) { 79 | if (!isset($obj->$part)) { 80 | throw new Error("Method {$msg->method} is not implemented", ErrorCode::METHOD_NOT_FOUND); 81 | } 82 | $obj = $obj->$part; 83 | } 84 | if (!isset($this->methods[$msg->method])) { 85 | try { 86 | $method = new ReflectionMethod($obj, $fn); 87 | $this->methods[$msg->method] = $method; 88 | } catch (ReflectionException $e) { 89 | throw new Error($e->getMessage(), ErrorCode::METHOD_NOT_FOUND, null, $e); 90 | } 91 | } 92 | $method = $this->methods[$msg->method]; 93 | $parameters = $method->getParameters(); 94 | if ($method->getDocComment()) { 95 | $docBlock = $this->docBlockFactory->create( 96 | $method->getDocComment(), 97 | $this->contextFactory->createFromReflector($method->getDeclaringClass()) 98 | ); 99 | $paramTags = $docBlock->getTagsByName('param'); 100 | } 101 | $args = []; 102 | if (isset($msg->params)) { 103 | // Find out the position 104 | if (is_array($msg->params)) { 105 | $args = $msg->params; 106 | } else if (is_object($msg->params)) { 107 | foreach ($parameters as $pos => $parameter) { 108 | $value = null; 109 | foreach(get_object_vars($msg->params) as $key => $val) { 110 | if ($parameter->name === $key) { 111 | $value = $val; 112 | break; 113 | } 114 | } 115 | $args[$pos] = $value; 116 | } 117 | } else { 118 | throw new Error('Params must be structured or omitted', ErrorCode::INVALID_REQUEST); 119 | } 120 | foreach ($args as $position => $value) { 121 | try { 122 | // If the type is structured (array or object), map it with JsonMapper 123 | if (is_object($value)) { 124 | // Does the parameter have a type hint? 125 | $param = $parameters[$position]; 126 | if ($param->hasType()) { 127 | $paramType = $param->getType(); 128 | if ($paramType instanceof ReflectionNamedType) { 129 | // We have object data to map and want the class name. 130 | // This should not include the `?` if the type was nullable. 131 | $class = $paramType->getName(); 132 | } else { 133 | // Fallback for php 7.0, which is still supported (and doesn't have nullable). 134 | $class = (string)$paramType; 135 | } 136 | $value = $this->mapper->map($value, new $class()); 137 | } 138 | } else if (is_array($value) && isset($docBlock)) { 139 | // Get the array type from the DocBlock 140 | $type = $paramTags[$position]->getType(); 141 | // For union types, use the first one that is a class array (often it is SomeClass[]|null) 142 | if ($type instanceof Types\Compound) { 143 | for ($i = 0; $t = $type->get($i); $i++) { 144 | if ( 145 | $t instanceof Types\Array_ 146 | && $t->getValueType() instanceof Types\Object_ 147 | && (string)$t->getValueType() !== 'object' 148 | ) { 149 | $class = (string)$t->getValueType()->getFqsen(); 150 | $value = $this->mapper->mapArray($value, [], $class); 151 | break; 152 | } 153 | } 154 | } else if ($type instanceof Types\Array_) { 155 | $class = (string)$type->getValueType()->getFqsen(); 156 | $value = $this->mapper->mapArray($value, [], $class); 157 | } else { 158 | throw new Error('Type is not matching @param tag', ErrorCode::INVALID_PARAMS); 159 | } 160 | } 161 | } catch (JsonMapper_Exception $e) { 162 | throw new Error($e->getMessage(), ErrorCode::INVALID_PARAMS, null, $e); 163 | } 164 | $args[$position] = $value; 165 | } 166 | } 167 | ksort($args); 168 | $result = $obj->$fn(...$args); 169 | return $result; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /lib/Error.php: -------------------------------------------------------------------------------- 1 | data = $data; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/ErrorCode.php: -------------------------------------------------------------------------------- 1 | id) && isset($msg->error); 29 | } 30 | 31 | /** 32 | * @param int|string $id 33 | * @param \AdvancedJsonRpc\Error $error 34 | */ 35 | public function __construct($id, Error $error) 36 | { 37 | parent::__construct($id); 38 | $this->error = $error; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/Message.php: -------------------------------------------------------------------------------- 1 | method, $decoded->params ?? null); 32 | } else if (Request::isRequest($decoded)) { 33 | $obj = new Request($decoded->id, $decoded->method, $decoded->params ?? null); 34 | } else if (SuccessResponse::isSuccessResponse($decoded)) { 35 | $obj = new SuccessResponse($decoded->id, $decoded->result); 36 | } else if (ErrorResponse::isErrorResponse($decoded)) { 37 | $obj = new ErrorResponse($decoded->id, new Error($decoded->error->message, $decoded->error->code, $decoded->error->data ?? null)); 38 | } else { 39 | throw new Error('Invalid message', ErrorCode::INVALID_REQUEST); 40 | } 41 | return $obj; 42 | } 43 | 44 | public function __toString(): string 45 | { 46 | $encoded = json_encode($this); 47 | if ($encoded === false) { 48 | throw new Error(json_last_error_msg(), ErrorCode::INTERNAL_ERROR); 49 | } 50 | return $encoded; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/Notification.php: -------------------------------------------------------------------------------- 1 | method); 45 | } 46 | 47 | /** 48 | * @param string $method 49 | * @param mixed $params 50 | */ 51 | public function __construct(string $method, $params = null) 52 | { 53 | $this->method = $method; 54 | $this->params = $params; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/Request.php: -------------------------------------------------------------------------------- 1 | method); 50 | } 51 | 52 | /** 53 | * @param string|int $id 54 | * @param string $method 55 | * @param object|array $params 56 | */ 57 | public function __construct($id, string $method, $params = null) 58 | { 59 | $this->id = $id; 60 | $this->method = $method; 61 | $this->params = $params; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/Response.php: -------------------------------------------------------------------------------- 1 | error)); 29 | } 30 | 31 | /** 32 | * @param int|string $id 33 | * @param mixed $result 34 | * @param ResponseError $error 35 | */ 36 | public function __construct($id) 37 | { 38 | $this->id = $id; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/SuccessResponse.php: -------------------------------------------------------------------------------- 1 | result = $result; 39 | } 40 | } 41 | --------------------------------------------------------------------------------