Completed
Push — master ( 2bc6aa...f63f47 )
by Indra
04:48
created

Builder::createData()   F

Complexity

Conditions 29
Paths 934

Size

Total Lines 108
Code Lines 74

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 870

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 0
loc 108
ccs 0
cts 98
cp 0
rs 2.1641
cc 29
eloc 74
nc 934
nop 8
crap 870

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace IndraGunawan\RestService;
4
5
use GuzzleHttp;
6
use GuzzleHttp\Command\CommandInterface;
7
use GuzzleHttp\Exception\BadResponseException as GuzzleBadResponseException;
8
use GuzzleHttp\Psr7\Request;
9
use IndraGunawan\RestService\Exception\BadRequestException;
10
use IndraGunawan\RestService\Exception\BadResponseException;
11
use IndraGunawan\RestService\Exception\CommandException;
12
use IndraGunawan\RestService\Exception\ValidatorException;
13
use IndraGunawan\RestService\Validator\Validator;
14
use Psr\Http\Message\RequestInterface;
15
use Psr\Http\Message\ResponseInterface;
16
17
class Builder
18
{
19
    /**
20
     * @var ServiceInterface
21
     */
22
    private $service;
23
24
    /**
25
     * @param ServiceInterface $service
26
     */
27
    public function __construct(ServiceInterface $service)
28
    {
29
        $this->service = $service;
30
    }
31
32
    /**
33
     * Transform command to request.
34
     *
35
     * @return \Closure
36
     */
37
    public function commandToRequestTransformer()
38
    {
39
        return function (CommandInterface $command) {
40
            if (!$this->service->hasOperation($command->getName())) {
41
                throw new CommandException(
42
                    sprintf(
43
                        'Command "%s" not found',
44
                        $command->getName()
45
                    ),
46
                    $command
47
                );
48
            }
49
50
            $operation = $this->service->getOperation($command->getName());
51
52
            $result = $this->transformData($command, $command->toArray(), $operation ?: [], 'request');
53
54
            $uri = ltrim($operation['requestUri'], '/');
55
            if ($result['uri']) {
56
                // replace uri
57
                $patterns = [];
58
                $replacements = [];
59
                foreach ($result['uri'] as $key => $value) {
60
                    $patterns[] = '/{'.$key.'}/';
61
                    $replacements[] = $value;
62
                }
63
64
                $uri = preg_replace($patterns, $replacements, $uri);
65
            }
66
67
            $body = null;
68
            if ('rest_json' === $operation['requestProtocol']) {
69
                $body = GuzzleHttp\json_encode($result['body']);
70
                $result['header']['Content-Type'] = 'application/json';
71
            } elseif ('form_params' === $operation['requestProtocol']) {
72
                $body = http_build_query($result['body'], '', '&');
73
                $result['header']['Content-Type'] = 'application/x-www-form-urlencoded';
74
            }
75
76
            if ($result['query']) {
77
                $uri .= sprintf('?%s', http_build_query($result['query'], null, '&', PHP_QUERY_RFC3986));
78
            }
79
80
            // GET method has no body, concat to uri
81
            if (in_array($operation['httpMethod'], ['GET'])) {
82
                // if uri has no query string, append '?' else '&'
83
                $uri .= (false === strpos($uri, '?')) ? '?' : '&';
84
85
                $uri .= http_build_query($result['body'], null, '&', PHP_QUERY_RFC3986);
86
                $result['body'] = null;
87
            }
88
89
            return new Request(
90
                $operation['httpMethod'],
91
                $this->service->getEndpoint().$uri,
92
                $result['header'],
93
                $body
94
            );
95
        };
96
    }
97
98
    /**
99
     * Transform response to result.
100
     *
101
     * @return \Closure
102
     */
103
    public function responseToResultTransformer()
104
    {
105
        return function (
106
            ResponseInterface $response,
107
            RequestInterface $request,
108
            CommandInterface $command
109
        ) {
110
            $operation = $this->service->getOperation($command->getName());
111
            $this->processResponseError($operation ?: [], $request, $response);
112
113
            $body = [];
114
            if ('rest_json' === $operation['responseProtocol']) {
115
                $body = GuzzleHttp\json_decode($response->getBody(), true);
116
            }
117
118
            $result = $this->transformData($command, $body, $operation, 'response');
119
120
            foreach ($response->getHeaders() as $name => $header) {
121
                $result['header'][$name] = is_array($header) ? array_pop($header) : null;
122
            }
123
124
            return new Result($result['body'], $result['header']);
125
        };
126
    }
127
128
    public function badResponseExceptionParser()
129
    {
130
        return function (CommandInterface $command, GuzzleBadResponseException $e) {
131
            $operation = $this->service->getOperation($command->getName());
132
133
            $this->processResponseError(
134
                $operation ?: [],
135
                $e->getRequest(),
136
                $e->getResponse(),
137
                $e
138
            );
139
        };
140
    }
141
142
    /**
143
     * Process response to check is error or not.
144
     *
145
     * @param array                           $operation
146
     * @param RequestInterface                $request
147
     * @param ResponseInterface               $response
148
     * @param GuzzleBadResponseException|null $e
149
     *
150
     * @throws \IndraGunawan\RestService\Exception\BadResponseException|null
151
     */
152
    private function processResponseError(
153
        array $operation,
154
        RequestInterface $request,
155
        ResponseInterface $response = null,
156
        GuzzleBadResponseException $e = null
157
    ) {
158
        $body = null;
159
        if ('rest_json' === $operation['responseProtocol']) {
160
            try {
161
                $body = GuzzleHttp\json_decode((!is_null($response)) ? $response->getBody() : '', true);
0 ignored issues
show
Bug introduced by
It seems like !is_null($response) ? $response->getBody() : '' can also be of type object<Psr\Http\Message\StreamInterface>; however, GuzzleHttp\json_decode() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
162
            } catch (\InvalidArgumentException $ex) {
163
                throw new BadResponseException(
164
                    '',
165
                    $ex->getMessage(),
166
                    ($e ? $e->getMessage() : ''),
167
                    $request,
168
                    $response,
169
                    $e ? $e->getPrevious() : null
170
                );
171
            }
172
        }
173
174
        if ($body) {
175
            foreach ($operation['errors'] as $name => $error) {
176
                if ('field' === $error['type']) {
177
                    $responseCode = $this->parseError($body, $error['codeField']);
178
179
                    if (!$responseCode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $responseCode of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
180
                        continue;
181
                    }
182
183
                    // if no ifCode property then return exception
184
                    if (!$error['ifCode']) {
185
                        $responseMessage = $this->getErrorMessage($body, $error, $response);
186
187
                        throw new BadResponseException(
188
                            $responseCode,
189
                            $responseMessage,
190
                            ($e ? $e->getMessage() : '').' code: '.$responseCode.', message: '.$responseMessage,
191
                            $request,
192
                            $response,
193
                            $e ? $e->getPrevious() : null
194
                        );
195
                    }
196
                } else {
197
                    $responseCode = $response->getStatusCode();
0 ignored issues
show
Bug introduced by
It seems like $response is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
198
                }
199
200
                if ($this->checkResponseCode($responseCode, $error['operator'], $error['ifCode'])) {
201
                    $responseMessage = $this->getErrorMessage($body, $error, $response);
202
203
                    throw new BadResponseException(
204
                        $responseCode,
205
                        $responseMessage,
206
                        ($e ? $e->getMessage() : '').'. code: '.$responseCode.', message: '.$responseMessage,
207
                        $request,
208
                        $response,
209
                        $e ? $e->getPrevious() : null
210
                    );
211
                }
212
            }
213
        }
214
215
        return;
216
    }
217
218
    /**
219
     * Parse error codeField from body.
220
     *
221
     * @param array  $body
222
     * @param string $path
223
     *
224
     * @return string|null
225
     */
226
    private function parseError(array $body, $path)
227
    {
228
        $tmp = $body;
229
        foreach (explode('.', $path) as $key) {
230
            if (!isset($tmp[$key])) {
231
                return;
232
            }
233
234
            $tmp = $tmp[$key];
235
        }
236
237
        return $tmp;
238
    }
239
240
    /**
241
     * Get error message from posible field.
242
     *
243
     * @param array                  $body
244
     * @param array                  $error
245
     * @param ResponseInterface|null $response
246
     *
247
     * @return string
248
     */
249
    private function getErrorMessage(array $body, array $error, ResponseInterface $response = null)
250
    {
251
        $message = $this->parseError($body, $error['messageField']);
252
        if (!$message) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $message of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
253
            $message = $error['defaultMessage'];
254
        }
255
        if (!$message) {
256
            $message = $response->getReasonPhrase();
0 ignored issues
show
Bug introduced by
It seems like $response is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
257
        }
258
259
        return $message;
260
    }
261
262
    /**
263
     * Transform and match data with shape.
264
     *
265
     * @param CommandInterface $command
266
     * @param array            $datas
267
     * @param array            $operation
268
     * @param string           $action
269
     *
270
     * @return array
271
     */
272
    private function transformData(CommandInterface $command, array $datas, array $operation, $action)
273
    {
274
        $result = [
275
            'header' => [],
276
            'uri' => [],
277
            'query' => [],
278
            'body' => [],
279
        ];
280
281
        $validator = new Validator();
282
283
        if (isset($operation[$action])) {
284
            $shape = $operation[$action];
285
        } else {
286
            $shape = [
287
                'type' => null,
288
                'members' => [],
289
            ];
290
        }
291
292
        $result['body'] = $this->createData(
293
            $validator,
294
            $datas,
295
            $shape,
296
            $command->getName(),
297
            $action,
298
            $result,
299
            ('request' === $action) ? $operation['strictRequest'] : $operation['strictResponse'],
300
            $operation['sentEmptyField']
301
        );
302
303
        if (!$validator->validate([$command->getName() => $datas])) {
304
            // validation failed
305
            throw $validator->createValidatorException();
306
        }
307
308
        return $result;
309
    }
310
311
    /**
312
     * Create request/response data and add validation rule.
313
     *
314
     * @param Validator $validator
315
     * @param array     $datas
316
     * @param array     $shape
317
     * @param string    $path
318
     * @param string    $action
319
     * @param array     &$result
320
     * @param bool      $isStrict
321
     * @param bool      $isSentEmptyField
322
     *
323
     * @return array
324
     */
325
    private function createData(
326
        Validator $validator,
327
        array &$datas,
328
        array $shape,
329
        $path,
330
        $action,
331
        array &$result,
332
        $isStrict = false,
333
        $isSentEmptyField = true
334
    ) {
335
        $tmpData = $datas;
336
        $bodyResult = [];
337
        if ('list' === $shape['type']) {
338
            $path .= '[*]';
339
        }
340
341
        foreach ($shape['members'] as $name => $parameter) {
342
            $tmpPath = $path.'['.$name.']';
343
            $value = isset($datas[$name]) ? $datas[$name] : null;
344
            if (!$value) {
345
                $datas[$name] = $parameter['defaultValue'];
346
            }
347
348
            // set validator
349
            $validator->add($tmpPath, $parameter, $value);
350
351
            // if nested children
352
            if (isset($parameter['members']) && count($parameter['members']) > 0) {
353
                if (!is_null($value) && !is_array($value)) {
354
                    throw new ValidatorException($tmpPath, sprintf(
355
                        'Expected "%s", but got "%s"',
356
                        $parameter['type'],
357
                        gettype($value)
358
                    ));
359
                }
360
                if ('list' === $parameter['type']) {
361
                    $children = $value ?: [];
362
                    foreach ($children as $idx => $child) {
363
                        if (!is_array($child)) {
364
                            throw new ValidatorException($tmpPath, 'Expected "list", but got "map"');
365
                        }
366
367
                        $bodyResult[$parameter['locationName']][] = $this->createData(
368
                            $validator,
369
                            $datas[$name][$idx],
370
                            $parameter,
371
                            $tmpPath,
372
                            $action,
373
                            $result,
374
                            $isStrict,
375
                            $isSentEmptyField
376
                        );
377
                    }
378
                } elseif ('map' === $parameter['type']) {
379
                    if (is_null($value)) {
380
                        if (array_key_exists($name, $datas)) {
381
                            unset($tmpData[$name]);
382
                        }
383
                        continue;
384
                    }
385
                    $children = $value ?: [];
386
                    foreach (array_keys($parameter['members']) as $key) {
387
                        if (!array_key_exists($key, $children)) {
388
                            $children[$key] = null;
389
                            $datas[$name][$key] = null;
390
                        }
391
                    }
392
393
                    $bodyResult[$parameter['locationName']] = $this->createData(
394
                        $validator,
395
                        $datas[$name],
396
                        $parameter,
397
                        $tmpPath,
398
                        $action,
399
                        $result,
400
                        $isStrict,
401
                        $isSentEmptyField
402
                    );
403
                }
404
            }
405
406
            $formattedValue = $this->getFormatedValue($value, $parameter, $action);
407
            if ('body' !== $parameter['location']) {
408
                $result[$parameter['location']][$parameter['locationName']] = $formattedValue;
409
            } else {
410
                if (!array_key_exists($parameter['locationName'], $bodyResult)) {
411
                    if (!$value && !is_numeric($value)) {
412
                        $value = $parameter['defaultValue'];
413
                    }
414
                    if ($isSentEmptyField || ($value || is_numeric($value))) {
415
                        $bodyResult[$parameter['locationName']] = $formattedValue;
416
                    }
417
                }
418
            }
419
            unset($tmpData[$name]);
420
        }
421
422
        if (count($tmpData) > 0) {
423
            if ($isStrict) {
424
                throw new BadRequestException(ucwords($action), 'Undefined parameters "'.implode('", "', array_keys($tmpData)).'"');
425
            }
426
            foreach ($tmpData as $name => $child) {
427
                $bodyResult[$name] = $child;
428
            }
429
        }
430
431
        return $bodyResult;
432
    }
433
434
    /**
435
     * Get formatted value.
436
     *
437
     * @param mixed  $value     [description]
438
     * @param array  $parameter [description]
439
     * @param string $action    request/response
440
     *
441
     * @return mixed
442
     */
443
    private function getFormatedValue($value, array $parameter, $action)
444
    {
445
        if (!$value && !is_numeric($value)) {
446
            $value = $parameter['defaultValue'];
447
        }
448
449
        switch ($parameter['type']) {
450
            case 'integer':
451
                return (int) (string) $value;
452
            case 'float':
453
                return (float) (string) $value;
454
            case 'string':
455
                $result = (string) $value;
456
457
                return sprintf($parameter['format'] ?: '%s', $result);
458
            case 'boolean':
459
                return ($value === 'true' || true === $value) ? true : false;
460
            case 'number':
461
                if ($parameter['format']) {
462
                    $format = explode('|', $parameter['format']);
463
                    $decimal = isset($format[0]) ? $format[0] : 0;
464
                    $decimalPoint = isset($format[1]) ? $format[1] : '.';
465
                    $thousandsSeparator = isset($format[2]) ? $format[2] : ',';
466
467
                    return number_format((float) (string) $value, $decimal, $decimalPoint, $thousandsSeparator);
468
                }
469
470
                return (string) $value;
471
            case 'datetime':
472
                if ('request' === $action) {
473
                    if (!$value) {
474
                        return;
475
                    }
476
477
                    if (!($value instanceof \DateTime)) {
478
                        $value = new \DateTime($value);
479
                    }
480
481
                    if ($parameter['format']) {
482
                        return $value->format($parameter['format']);
483
                    }
484
485
                    return $value->format('Y-m-d\TH:i:s\Z');
486
                } elseif ('response' === $action) {
487
                    if ($parameter['format']) {
488
                        return \DateTime::createFromFormat($parameter['format'], $value);
489
                    } else {
490
                        return new \DateTime($value);
491
                    }
492
                }
493
                //
494
            default:
495
                return;
496
        }
497
    }
498
499
    /**
500
     * Check is responsecode is match with code.
501
     *
502
     * @param string $responseCode
503
     * @param string $operator
504
     * @param string $code
505
     *
506
     * @return bool
507
     */
508
    public function checkResponseCode($responseCode, $operator, $code)
509
    {
510
        switch ($operator) {
511
            case '===':
512
                return $responseCode === $code;
513
            case '!==':
514
                return $responseCode !== $code;
515
            case '!=':
516
                return $responseCode != $code;
517
            case '<':
518
                return $responseCode < $code;
519
            case '<=':
520
                return $responseCode <= $code;
521
            case '>=':
522
                return $responseCode >= $code;
523
            case '>':
524
                return $responseCode > $code;
525
            default:
526
                return $responseCode == $code;
527
        }
528
    }
529
}
530