JsonProcessor   B
last analyzed

Complexity

Total Complexity 49

Size/Duplication

Total Lines 459
Duplicated Lines 0 %

Test Coverage

Coverage 78.4%

Importance

Changes 0
Metric Value
wmc 49
dl 0
loc 459
ccs 98
cts 125
cp 0.784
rs 8.48
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
B runSingleRequest() 0 66 5
B preprocessJsonRequest() 0 26 9
A packJsonError() 0 16 2
A process() 0 15 2
B run() 0 54 10
A packJsonSuccess() 0 13 2
B checkSignatureMatch() 0 34 8
A checkRequestSustainability() 0 7 2
A preprocessJsonPayload() 0 19 3
A checkRequestConsistence() 0 11 3
A matchParameters() 0 15 2
A __construct() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like JsonProcessor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use JsonProcessor, and based on these observations, apply Extract Interface, too.

1
<?php namespace Comodojo\RpcServer\Request;
2
3
use \Comodojo\RpcServer\Request\Parameters;
4
use \Comodojo\RpcServer\RpcMethod;
5
use \Comodojo\Foundation\Validation\DataValidation as Validator;
6
use \Comodojo\Exception\RpcException;
7
use \Psr\Log\LoggerInterface;
8
use \Exception;
9
10
/**
11
 * The JSONRPC processor
12
 *
13
 * @package     Comodojo Spare Parts
14
 * @author      Marco Giovinazzi <[email protected]>
15
 * @license     MIT
16
 *
17
 * LICENSE:
18
 *
19
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
 * THE SOFTWARE.
26
 */
27
28
class JsonProcessor {
29
30
    /**
31
     * A parameters object
32
     *
33
     * @var Parameters
34
     */
35
    private $parameters;
36
37
    /**
38
     * Array of requests
39
     *
40
     * @var array
41
     */
42
    private $requests = [];
43
44
    /**
45
     * Array of results
46
     *
47
     * @var array
48
     */
49
    private $results = [];
50
51
    /**
52
     * Internal flag to identify a batch request
53
     *
54
     * @var bool
55
     */
56
    private $is_batch_request = false;
57
58
    /**
59
     * Current logger
60
     *
61
     * @var LoggerInterface
62
     */
63
    private $logger = null;
64
65
    /**
66
     * Class constructor
67
     *
68
     * @param array|object $payload
69
     * @param Parameters $parameters
70
     * @param LoggerInterface $logger
71
     */
72 45
    public function __construct($payload, Parameters $parameters, LoggerInterface $logger) {
73
74 45
        $this->logger = $logger;
75
76 45
        $this->logger->debug("Starting JSON processor");
77
78 45
        $this->parameters = $parameters;
79
80 45
        list($this->is_batch_request, $this->requests) = self::preprocessJsonPayload($payload);
81
82 45
    }
83
84
    /**
85
     * Run the processor and exec callback(s)
86
     *
87
     * @return mixed
88
     * @throws Exception
89
     */
90 45
    public function run() {
91
92 45
        foreach ( $this->requests as $request ) {
93
94 45
            if ( isset($request['ERROR_CODE']) && isset($request['ERROR_MESSAGE']) ) {
95
96
                $this->logger->warning("Invalid request ".$request['ID']);
97
98
                $result = self::packJsonError($request['ERROR_CODE'], $request['ERROR_MESSAGE'], $request['ID']);
99
100
                if ( !is_null($result) ) $this->results[] = $result;
101
102
            } else {
103
104
                try {
105
106 45
                    $this->logger->debug("Serving request ".$request['METHOD']."(".$request['ID'].")");
107
108 45
                    $response = $this->runSingleRequest($request['METHOD'], $request['PARAMETERS']);
109
110 36
                    $result = self::packJsonSuccess($response, $request['ID']);
111
112 12
                } catch (RpcException $re) {
113
114 12
                    $this->logger->warning("Error handling request ".$request['ID'].": ".$re->getMessage());
115
116 12
                    $result = self::packJsonError($re->getCode(), $re->getMessage(), $request['ID']);
117
118
                } catch (Exception $e) {
119
120
                    $this->logger->error($e->getMessage());
121
122
                    throw $e;
123
124
                }
125
126 45
                if ( !is_null($result) ) $this->results[] = $result;
127
128
            }
129
130
131
        }
132
133 45
        if ( empty($this->results) ) {
134
135 3
            return null;
136
137 42
        } else if ( $this->is_batch_request ) {
138
139 9
            return $this->results;
140
141
        } else {
142
143 33
            return $this->results[0];
144
145
        }
146
147
    }
148
149
    /**
150
     * Static constructor - start processor
151
     *
152
     * @param array|object $payload
153
     * @param Parameters $parameters
154
     * @param LoggerInterface $logger
155
     *
156
     * @return mixed
157
     * @throws Exception
158
     */
159 45
    public static function process($payload, Parameters $parameters, LoggerInterface $logger) {
160
161
        try {
162
163 45
            $processor = new JsonProcessor($payload, $parameters, $logger);
164
165 45
            $return = $processor->run();
166
167
        } catch (Exception $e) {
168
169
            throw $e;
170
171
        }
172
173 45
        return $return;
174
175
    }
176
177
    /**
178
     * Preprocess json payload
179
     *
180
     * @param array|object $payload
181
     *
182
     * @return array
183
     */
184 45
    private static function preprocessJsonPayload($payload) {
185
186 45
        $requests = [];
187
188 45
        $is_batch = false;
189
190 45
        if ( is_array($payload) ) {
191
192 9
            $is_batch = true;
193
194 9
            foreach ( $payload as $request ) $requests[] = self::preprocessJsonRequest($request);
195
196
        } else {
197
198 36
            $requests[] = self::preprocessJsonRequest($payload);
199
200
        }
201
202 45
        return array($is_batch, $requests);
203
204
    }
205
206
    /**
207
     * Preprocess a single json request
208
     *
209
     * @param array|object $request
210
     *
211
     * @return array
212
     */
213 45
    private static function preprocessJsonRequest($request) {
214
215
        // check for required parameters
216
217
        if (
218 45
            !is_object($request) ||
219 45
            !property_exists($request, 'jsonrpc') ||
220 45
            !property_exists($request, 'method') ||
221 45
            $request->jsonrpc != '2.0' ||
222 45
            empty($request->method)
223
        ) {
224
225
            return array(
226
                'ERROR_CODE' => -32600,
227
                'ERROR_MESSAGE' => 'Invalid Request',
228
                'ID' => !isset($request['id']) ? null : $request['id']
229
            );
230
231
        }
232
233
        // parse request's components
234
235
        return array(
236 45
            'METHOD' => $request->method,
237 45
            'PARAMETERS' => property_exists($request, 'params') ? $request->params : [],
238 45
            'ID' => property_exists($request, 'id') ? $request->id : null
239
        );
240
241
    }
242
243
    /**
244
     * Exec a single request
245
     *
246
     * @param string $request_method
247
     * @param array  $parameters
248
     *
249
     * @return mixed
250
     * @throws RpcException
251
     */
252 45
    private function runSingleRequest($request_method, $parameters) {
253
254
        try {
255
256 45
            $registered_method = $this->checkRequestSustainability($request_method);
257
258 42
            $selected_signature = $this->checkRequestConsistence($registered_method, $parameters);
259
260 42
            if ( is_array($parameters) ) $parameters = self::matchParameters($parameters, $registered_method, $selected_signature);
0 ignored issues
show
introduced by
The condition is_array($parameters) is always true.
Loading history...
261
262 42
            $this->parameters->setParameters($parameters);
263
264 42
            $callback = $registered_method->getCallback();
265
266 42
            $attributes = $registered_method->getArguments();
267
268 42
            array_unshift($attributes, $this->parameters);
269
270 6
        } catch (RpcException $re) {
271
272 6
            throw $re;
273
274
        }
275
276 42
        set_error_handler(
277
278 42
            function($severity, $message, $file, $line) {
279
280
                $this->logger->error($message, array(
281
                    "FILE" => $file,
282
                    "LINE" => $line
283
                ));
284
285
                throw new RpcException('Internal error', -32603);
286
287 42
            }
288
289
        );
290
291
        try {
292
293
            // $return = call_user_func($callback, $this->parameters);
294 42
            $return = call_user_func_array($callback, $attributes);
295
296 6
        } catch (RpcException $re) {
297
298 6
            restore_error_handler();
299
300 6
            throw $re;
301
302
        } catch (Exception $e) {
303
304
            restore_error_handler();
305
306
            $this->logger->error($e->getMessage(), array(
307
                "FILE" => $e->getFile(),
308
                "LINE" => $e->getLine()
309
            ));
310
311
            throw new RpcException('Internal error', -32603);
312
313
        }
314
315 36
        restore_error_handler();
316
317 36
        return $return;
318
319
    }
320
321
    /**
322
     * Pack a json error response
323
     *
324
     * @param integer $code
325
     * @param string  $message
326
     * @param integer $id
327
     *
328
     * @return array|null
329
     */
330 12
    private static function packJsonError($code, $message, $id) {
331
332 12
        if ( !is_null($id) ) {
0 ignored issues
show
introduced by
The condition is_null($id) is always false.
Loading history...
333
334
            return array(
335 12
                'jsonrpc' => '2.0',
336
                'error' => array(
337 12
                    'code' => $code,
338 12
                    'message' => $message
339
                ),
340 12
                'id' => $id
341
            );
342
343
        } else {
344
345
            return null;
346
347
        }
348
349
    }
350
351
    /**
352
     * Pack a json success response
353
     *
354
     * @param mixed   $result
355
     * @param integer $id
356
     *
357
     * @return array
358
     */
359 36
    private static function packJsonSuccess($result, $id) {
360
361 36
        if ( !is_null($id) ) {
0 ignored issues
show
introduced by
The condition is_null($id) is always false.
Loading history...
362
363
            return array(
364 33
                'jsonrpc' => '2.0',
365 33
                'result' => $result,
366 33
                'id' => $id
367
            );
368
369
        } else {
370
371 6
            return null;
372
373
        }
374
375
    }
376
377
    /**
378
     * Create an associative array of $name => $parameter from current signature
379
     *
380
     * @param array $provided
381
     * @param RpcMethod $method
382
     * @param integer $selected_signature
383
     *
384
     * @return array
385
     */
386 42
    private static function matchParameters(array $provided, RpcMethod $method, $selected_signature) {
387
388 42
        $parameters = [];
389
390 42
        $requested_parameters = $method->selectSignature($selected_signature)->getParameters();
391
392 42
        $requested_parameters_keys = array_keys($requested_parameters);
393
394 42
        foreach ( $provided as $index => $parameter ) {
395
396 27
            $parameters[$requested_parameters_keys[$index]] = $parameter;
397
398
        }
399
400 42
        return $parameters;
401
402
    }
403
404
    /**
405
     * Check if a request is sustainable (i.e. if method is registered)
406
     *
407
     * @param string $request_method
408
     *
409
     * @return RpcMethod
410
     * @throws RpcException
411
     */
412 45
    private function checkRequestSustainability($request_method) {
413
414 45
        $method = $this->parameters->methods()->get($request_method);
0 ignored issues
show
Deprecated Code introduced by
The function Comodojo\RpcServer\Request\Parameters::methods() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

414
        $method = /** @scrutinizer ignore-deprecated */ $this->parameters->methods()->get($request_method);
Loading history...
415
416 45
        if ( is_null($method) ) throw new RpcException("Method not found", -32601);
417
418 42
        return $method;
419
420
    }
421
422
    /**
423
     * Check if a request is consistent (i.e. if it matches one of method's signatures)
424
     *
425
     * @param RpcMethod $registered_method
426
     * @param array $parameters
427
     *
428
     * @return int
429
     * @throws RpcException
430
     */
431 42
    private function checkRequestConsistence(RpcMethod $registered_method, array $parameters) {
432
433 42
        $signatures = $registered_method->getSignatures(false);
434
435 42
        foreach ( $signatures as $num => $signature ) {
436
437 42
            if ( self::checkSignatureMatch($parameters, $signature["PARAMETERS"]) === true ) return $num;
438
439
        }
440
441
        throw new RpcException("Invalid params", -32602);
442
443
    }
444
445
    /**
446
     * Check if call match a signature
447
     *
448
     * @param array|object $provided
449
     * @param array|object $requested
450
     *
451
     * @return bool
452
     */
453 42
    private static function checkSignatureMatch($provided, $requested) {
454
455 42
        if ( is_object($provided) ) {
456
457
            foreach ( $provided as $parameter=>$value ) {
458
459
                if (
460
                    !isset($requested[$parameter]) ||
461
                    !Validator::validate($value, $requested[$parameter])
462
                ) return false;
463
464
            }
465
466
        } else {
467
468 42
            $provided_parameters_count = count($provided);
469
470 42
            $requested_parameters_count = count($requested);
471
472 42
            if ( $provided_parameters_count != $requested_parameters_count ) return false;
473
474 42
            $index = 0;
475
476 42
            foreach ( $requested as $parameter => $type ) {
477
478 27
                if ( !Validator::validate($provided[$index], $type) ) return false;
479
480 27
                $index += 1;
481
482
            }
483
484
        }
485
486 42
        return true;
487
488
    }
489
490
}
491