1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Vectorface\SnappyRouter\Handler; |
4
|
|
|
|
5
|
|
|
use \stdClass; |
6
|
|
|
use \Exception; |
7
|
|
|
use Vectorface\SnappyRouter\Encoder\JsonEncoder; |
8
|
|
|
use Vectorface\SnappyRouter\Exception\ResourceNotFoundException; |
9
|
|
|
use Vectorface\SnappyRouter\Handler\AbstractRequestHandler; |
10
|
|
|
use Vectorface\SnappyRouter\Request\HttpRequest; |
11
|
|
|
use Vectorface\SnappyRouter\Request\JsonRpcRequest; |
12
|
|
|
use Vectorface\SnappyRouter\Response\Response; |
13
|
|
|
use Vectorface\SnappyRouter\Response\JsonRpcResponse; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* Handle JSON-RPC 1/2 requests |
17
|
|
|
* |
18
|
|
|
* @copyright Copyright (c) 2014, VectorFace, Inc. |
19
|
|
|
*/ |
20
|
|
|
class JsonRpcHandler extends AbstractRequestHandler implements BatchRequestHandlerInterface |
21
|
|
|
{ |
22
|
|
|
/** |
23
|
|
|
* Option key for a base path |
24
|
|
|
*/ |
25
|
|
|
const KEY_BASE_PATH = 'basePath'; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Error constants from the JSON-RPC 2.0 spec. |
29
|
|
|
*/ |
30
|
|
|
const ERR_PARSE_ERROR = -32700; |
31
|
|
|
const ERR_INVALID_REQUEST = -32600; |
32
|
|
|
const ERR_METHOD_NOT_FOUND = -32601; |
33
|
|
|
const ERR_INVALID_PARAMS = -32602; |
34
|
|
|
const ERR_INTERNAL_ERROR = -32603; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* The path to the stdin stream. This can be set for testing. |
38
|
|
|
* |
39
|
|
|
* @var string |
40
|
|
|
*/ |
41
|
|
|
private $stdin = 'php://input'; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* An array of HttpRequest objects. |
45
|
|
|
* |
46
|
|
|
* @var array |
47
|
|
|
*/ |
48
|
|
|
private $requests; |
49
|
|
|
|
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* A flag indicating whether the request is a batch request. |
53
|
|
|
* |
54
|
|
|
* @var boolean |
55
|
|
|
*/ |
56
|
|
|
private $batch; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* The encoder instance to be used to encode responses. |
60
|
|
|
* |
61
|
|
|
* @var \Vectorface\SnappyRouter\Encoder\EncoderInterface|\Vectorface\SnappyRouter\Encoder\JsonEncoder |
62
|
|
|
*/ |
63
|
|
|
private $encoder; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Returns true if the handler determines it should handle this request and false otherwise. |
67
|
|
|
* |
68
|
|
|
* @param string $path The URL path for the request. |
69
|
|
|
* @param array $query The query parameters. |
70
|
|
|
* @param array $post The post data. |
71
|
|
|
* @param string $verb The HTTP verb used in the request. |
72
|
|
|
* @return boolean Returns true if this handler will handle the request and false otherwise. |
73
|
|
|
*/ |
74
|
5 |
|
public function isAppropriate($path, $query, $post, $verb) |
75
|
|
|
{ |
76
|
|
|
/* JSON-RPC is POST-only. */ |
77
|
5 |
|
if ($verb !== 'POST' && $verb !== 'OPTIONS') { |
78
|
1 |
|
return false; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/* Try decoding and validating POST data, and skip if it doesn't look like JSON-RPC */ |
82
|
5 |
|
$post = json_decode(file_get_contents($this->stdin)); |
83
|
5 |
|
if ($verb === 'POST' && !is_object($post) && !is_array($post)) { |
84
|
2 |
|
return false; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
// extract the list of requests from the payload and tell the router |
88
|
|
|
// we'll handle this request |
89
|
5 |
|
$service = $this->getServiceFromPath($path); |
90
|
5 |
|
$this->processPayload($service, $verb, $post); |
91
|
5 |
|
return true; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* Determine the service to load via the ServiceProvider based on path |
96
|
|
|
* |
97
|
|
|
* @param string $path The raw path (URI) used to determine the service. |
98
|
|
|
* @return string The name of the service that we should attempt to load. |
99
|
|
|
*/ |
100
|
5 |
|
private function getServiceFromPath($path) |
101
|
|
|
{ |
102
|
|
|
/* Ensure the path is in a standard form, removing empty elements. */ |
103
|
5 |
|
$path = implode('/', array_filter(array_map('trim', explode('/', $path)), 'strlen')); |
104
|
|
|
|
105
|
|
|
/* If using the basePath option, strip the basePath. Otherwise, the path becomes the basename of the URI. */ |
106
|
5 |
|
if (isset($this->options[self::KEY_BASE_PATH])) { |
107
|
1 |
|
$basePathPosition = strpos($path, $this->options[self::KEY_BASE_PATH]); |
108
|
1 |
View Code Duplication |
if (false !== $basePathPosition) { |
|
|
|
|
109
|
1 |
|
$path = substr($path, $basePathPosition + strlen($this->options[self::KEY_BASE_PATH])); |
110
|
|
|
} |
111
|
|
|
|
112
|
1 |
|
return trim(dirname($path), "/") . '/' . basename($path, '.php'); /* For example, x/y/z/FooService */ |
113
|
|
|
} else { |
114
|
4 |
|
return basename($path, '.php'); /* For example: FooService, from /x/y/z/FooService.php */ |
115
|
|
|
} |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Processes the payload POST data and sets up the array of requests. |
120
|
|
|
* @param string $service The service being requested. |
121
|
|
|
* @param string $verb The HTTP verb being used. |
122
|
|
|
* @param array|object $post The raw POST data. |
123
|
|
|
*/ |
124
|
5 |
|
private function processPayload($service, $verb, $post) |
125
|
|
|
{ |
126
|
5 |
|
$this->batch = is_array($post); |
127
|
5 |
|
if (false === $this->batch) { |
128
|
4 |
|
$post = array($post); |
129
|
|
|
} |
130
|
5 |
|
$this->requests = array_map(function ($payload) use ($service, $verb) { |
131
|
4 |
|
return new JsonRpcRequest($service, $payload, $verb); |
132
|
5 |
|
}, $post); |
133
|
5 |
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Returns a request object extracted from the request details (path, query, etc). The method |
137
|
|
|
* isAppropriate() must have returned true, otherwise this method should return null. |
138
|
|
|
* @return HttpRequest Returns a Request object or null if this handler is not appropriate. |
139
|
|
|
*/ |
140
|
4 |
|
public function getRequest() |
141
|
|
|
{ |
142
|
4 |
|
return (!empty($this->requests)) ? $this->requests[0] : null; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* Returns an array of batched requests. |
147
|
|
|
* @return array An array of batched requests. |
148
|
|
|
*/ |
149
|
2 |
|
public function getRequests() |
150
|
|
|
{ |
151
|
2 |
|
return $this->requests; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Performs the actual routing. |
156
|
|
|
* |
157
|
|
|
* @return string Returns the result of the route. |
158
|
|
|
*/ |
159
|
3 |
|
public function performRoute() |
160
|
|
|
{ |
161
|
3 |
|
$this->invokePluginsHook( |
162
|
3 |
|
'beforeServiceSelected', |
163
|
3 |
|
array($this, $this->getRequest()) |
164
|
|
|
); |
165
|
|
|
|
166
|
|
|
/* Check if we can get the service. */ |
167
|
|
|
try { |
168
|
3 |
|
$service = $this->getServiceProvider()->getServiceInstance( |
169
|
3 |
|
$this->getRequest()->getController() |
170
|
|
|
); |
171
|
1 |
|
} catch (Exception $e) { |
172
|
1 |
|
throw new ResourceNotFoundException( |
173
|
1 |
|
'No such service: '.$this->getRequest()->getController() |
174
|
|
|
); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/* Loop through each call in the possible batch. */ |
178
|
2 |
|
$response = array(); |
179
|
2 |
|
foreach ($this->requests as $request) { |
180
|
2 |
|
$callResponse = $this->invokeMethod($service, $request); |
181
|
|
|
|
182
|
|
|
/* Stop here if this isn't a batch. There is only one response. */ |
183
|
2 |
|
if (false === $this->batch) { |
184
|
2 |
|
$response = $callResponse->getResponseObject(); |
185
|
2 |
|
break; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/* Omit empty responses from the batch response. */ |
189
|
1 |
|
if ($callResponse = $callResponse->getResponseObject()) { |
190
|
1 |
|
$response[] = $callResponse; |
191
|
|
|
} |
192
|
|
|
} |
193
|
|
|
|
194
|
2 |
|
@header('Content-type: application/json'); |
|
|
|
|
195
|
2 |
|
$response = new Response($response); |
196
|
2 |
|
return $this->getEncoder()->encode($response); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Invokes a method on a service class, based on the raw JSON-RPC request. |
201
|
|
|
* |
202
|
|
|
* @param mixed $service The service being invoked. |
203
|
|
|
* @param Vectorface\SnappyRouter\Request\JsonRpcRequest $request The request |
204
|
|
|
* to invoke. |
205
|
|
|
* @return JsonRpcResponse A response based on the result of the procedure call. |
206
|
|
|
*/ |
207
|
2 |
|
private function invokeMethod($service, JsonRpcRequest $request) |
208
|
|
|
{ |
209
|
2 |
|
if (false === $request->isValid()) { |
210
|
|
|
/* Note: Method isn't known, so invocation hooks aren't called. */ |
211
|
1 |
|
return new JsonRpcResponse( |
212
|
1 |
|
null, |
213
|
1 |
|
new Exception( |
214
|
1 |
|
'The JSON sent is not a valid Request object', |
215
|
1 |
|
self::ERR_INVALID_REQUEST |
216
|
|
|
) |
217
|
|
|
); |
218
|
|
|
} |
219
|
|
|
|
220
|
2 |
|
$action = $request->getAction(); |
221
|
2 |
|
$this->invokePluginsHook( |
222
|
2 |
|
'afterServiceSelected', |
223
|
2 |
|
array($this, $request, $service, $action) |
224
|
|
|
); |
225
|
|
|
|
226
|
2 |
|
$this->invokePluginsHook( |
227
|
2 |
|
'beforeMethodInvoked', |
228
|
2 |
|
array($this, $request, $service, $action) |
229
|
|
|
); |
230
|
|
|
|
231
|
|
|
try { |
232
|
2 |
|
$response = new JsonRpcResponse( |
233
|
2 |
|
call_user_func_array(array($service, $action), $request->getParameters()), |
234
|
1 |
|
null, |
235
|
1 |
|
$request |
236
|
|
|
); |
237
|
1 |
|
} catch (Exception $e) { |
238
|
1 |
|
$error = new Exception($e->getMessage(), self::ERR_INTERNAL_ERROR); |
239
|
1 |
|
$response = new JsonRpcResponse(null, $error, $request); |
240
|
|
|
} |
241
|
|
|
|
242
|
2 |
|
$this->invokePluginsHook( |
243
|
2 |
|
'afterMethodInvoked', |
244
|
2 |
|
array($this, $request, $service, $action, $response) |
245
|
|
|
); |
246
|
|
|
|
247
|
2 |
|
return $response; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* Returns the active response encoder. |
252
|
|
|
* |
253
|
|
|
* @return \Vectorface\SnappyRouter\Encoder\EncoderInterface Returns the response encoder. |
254
|
|
|
*/ |
255
|
2 |
|
public function getEncoder() |
256
|
|
|
{ |
257
|
2 |
|
if (!isset($this->encoder)) { |
258
|
2 |
|
$this->encoder = new JsonEncoder(); |
259
|
|
|
} |
260
|
2 |
|
return $this->encoder; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* Provides the handler with an opportunity to perform any last minute |
265
|
|
|
* error handling logic. The returned value will be serialized by the |
266
|
|
|
* handler's encoder. |
267
|
|
|
* |
268
|
|
|
* @param Exception $e The exception that was thrown. |
269
|
|
|
* @return mixed Returns a serializable value that will be encoded and returned |
270
|
|
|
* to the client. |
271
|
|
|
*/ |
272
|
1 |
|
public function handleException(Exception $e) |
273
|
|
|
{ |
274
|
1 |
|
if ($e->getCode() > -32000) { |
275
|
|
|
/* Don't pass through internal errors in case there's something sensitive. */ |
276
|
1 |
|
$response = new JsonRpcResponse(null, new Exception("Internal Error", self::ERR_INTERNAL_ERROR)); |
277
|
|
|
} else { |
278
|
|
|
/* JSON-RPC errors (<= -32000) can be passed on. */ |
279
|
1 |
|
$response = new JsonRpcResponse(null, $e, null); |
280
|
|
|
} |
281
|
|
|
|
282
|
1 |
|
return $response->getResponseObject(); |
283
|
|
|
} |
284
|
|
|
} |
285
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.