Builder::createData()   F
last analyzed

Complexity

Conditions 29
Paths 934

Size

Total Lines 108
Code Lines 74

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 538.583

Importance

Changes 0
Metric Value
dl 0
loc 108
ccs 10
cts 65
cp 0.1538
rs 2.1641
c 0
b 0
f 0
cc 29
eloc 74
nc 934
nop 8
crap 538.583

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 declare(strict_types=1);
2
3
/*
4
 * This file is part of indragunawan/rest-service package.
5
 *
6
 * (c) Indra Gunawan <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace IndraGunawan\RestService;
13
14
use GuzzleHttp;
15
use GuzzleHttp\Command\CommandInterface;
16
use GuzzleHttp\Exception\BadResponseException as GuzzleBadResponseException;
17
use GuzzleHttp\Psr7\Request;
18
use IndraGunawan\RestService\Exception\BadRequestException;
19
use IndraGunawan\RestService\Exception\BadResponseException;
20
use IndraGunawan\RestService\Exception\CommandException;
21
use IndraGunawan\RestService\Exception\ValidatorException;
22
use IndraGunawan\RestService\Validator\Validator;
23
use Psr\Http\Message\RequestInterface;
24
use Psr\Http\Message\ResponseInterface;
25
26
class Builder
27
{
28
    /**
29
     * @var ServiceInterface
30
     */
31
    private $service;
32
33
    /**
34
     * @var ValueFormatter
35
     */
36
    private $formatter;
37
38
    /**
39
     * @param ServiceInterface $service
40
     */
41 2
    public function __construct(ServiceInterface $service)
42
    {
43 2
        $this->service = $service;
44 2
        $this->formatter = new ValueFormatter();
45 2
    }
46
47
    /**
48
     * Transform command to request.
49
     *
50
     * @return \Closure
51
     */
52
    public function commandToRequestTransformer()
53
    {
54 2
        return function (CommandInterface $command) {
55 2
            if (!$this->service->hasOperation($command->getName())) {
56
                throw new CommandException(
57
                    sprintf(
58
                        'Command "%s" not found',
59
                        $command->getName()
60
                    ),
61
                    $command
62
                );
63
            }
64
65 2
            $operation = $this->service->getOperation($command->getName());
66
67 2
            $result = $this->transformData($command, $command->toArray(), $operation ?: [], 'request');
68
69 2
            $uri = ltrim($operation['requestUri'], '/');
70 2
            if ($result['uri']) {
71
                // replace uri
72
                $patterns = [];
73
                $replacements = [];
74
                foreach ($result['uri'] as $key => $value) {
75
                    $patterns[] = '/{'.$key.'}/';
76
                    $replacements[] = $value;
77
                }
78
79
                $uri = preg_replace($patterns, $replacements, $uri);
80
            }
81
82 2
            $body = null;
83 2
            if ('rest_json' === $operation['requestProtocol']) {
84 2
                $body = GuzzleHttp\json_encode($result['body']);
85 2
                $result['header']['Content-Type'] = 'application/json';
86
            } elseif ('form_params' === $operation['requestProtocol']) {
87
                $body = http_build_query($result['body'], '', '&');
88
                $result['header']['Content-Type'] = 'application/x-www-form-urlencoded';
89
            }
90
91 2
            if ($result['query']) {
92
                $uri .= sprintf('?%s', http_build_query($result['query'], '', '&', PHP_QUERY_RFC3986));
93
            }
94
95
            // GET method has no body, concat to uri
96 2
            if (in_array($operation['httpMethod'], ['GET'], true)) {
97
                // if uri has no query string, append '?' else '&'
98 2
                $uri .= (false === strpos($uri, '?')) ? '?' : '&';
99
100 2
                $uri .= http_build_query($result['body'], '', '&', PHP_QUERY_RFC3986);
101 2
                $result['body'] = null;
102
            }
103
104 2
            return new Request(
105 2
                $operation['httpMethod'],
106 2
                $this->service->getEndpoint().$uri,
107 2
                $result['header'],
108 2
                $body
109
            );
110 2
        };
111
    }
112
113
    /**
114
     * Transform response to result.
115
     *
116
     * @return \Closure
117
     */
118
    public function responseToResultTransformer()
119
    {
120 2
        return function (
121
            ResponseInterface $response,
122
            RequestInterface $request,
123
            CommandInterface $command
124
        ) {
125 2
            $operation = $this->service->getOperation($command->getName());
126 2
            $this->processResponseError($operation ?: [], $request, $response);
127 1
            if ('rest_json' === $operation['responseProtocol']) {
128 1
                $body = GuzzleHttp\json_decode($response->getBody(), true);
129
130 1
                $result = $this->transformData($command, $body, $operation, 'response');
131
132 1
                foreach ($response->getHeaders() as $name => $header) {
133 1
                    $result['header'][$name] = is_array($header) ? array_pop($header) : null;
134
                }
135
136 1
                return new Result($result['body'], $result['header']);
137 1
            } elseif ('stream' === $operation['responseProtocol']) {
138 1
                $streamResponse = new StreamResult(
139 1
                    $response->getStatusCode(),
140 1
                    $response->getHeaders(),
141 1
                    $response->getBody(),
142 1
                    $response->getProtocolVersion(),
143 1
                    $response->getReasonPhrase()
144
                );
145
146 1
                return $streamResponse;
147
            }
148 2
        };
149
    }
150
151
    public function badResponseExceptionParser()
152
    {
153 2
        return function (CommandInterface $command, GuzzleBadResponseException $e) {
154 1
            $operation = $this->service->getOperation($command->getName());
155
156 1
            $this->processResponseError(
157 1
                $operation ?: [],
158 1
                $e->getRequest(),
159 1
                $e->getResponse(),
160 1
                $e
161
            );
162 2
        };
163
    }
164
165
    /**
166
     * Process response to check is error or not.
167
     *
168
     * @param array                           $operation
169
     * @param RequestInterface                $request
170
     * @param ResponseInterface               $response
171
     * @param GuzzleBadResponseException|null $e
172
     *
173
     * @throws \IndraGunawan\RestService\Exception\BadResponseException|null
174
     */
175 2
    private function processResponseError(
176
        array $operation,
177
        RequestInterface $request,
178
        ResponseInterface $response = null,
179
        GuzzleBadResponseException $e = null
180
    ) {
181 2
        $body = null;
182
        try {
183 2
            if ('rest_json' === $operation['responseProtocol']) {
184 2
                $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...
185 1
            } elseif ('stream' === $operation['responseProtocol']) {
186 1
                $body = $response->getBody();
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...
187
            }
188 1
        } catch (\InvalidArgumentException $ex) {
189 1
            throw new BadResponseException(
190 1
                '',
191 1
                $ex->getMessage(),
192 1
                ($ex ? $ex->getMessage() : ''),
193 1
                $request,
194 1
                $response,
195 1
                $ex ? $ex->getPrevious() : null
196
            );
197
        }
198
199 1
        if ($body) {
200 1
            foreach ($operation['errors'] as $name => $error) {
201
                if ('field' === $error['type']) {
202
                    $responseCode = $this->parseError($body, $error['codeField']);
203
204
                    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...
205
                        continue;
206
                    }
207
208
                    // if no ifCode property then return exception
209
                    if (!$error['ifCode']) {
210
                        $responseMessage = $this->getErrorMessage($body, $error, $response);
211
212
                        throw new BadResponseException(
213
                            $responseCode,
214
                            $responseMessage,
215
                            ($e ? $e->getMessage() : '').' code: '.$responseCode.', message: '.$responseMessage,
216
                            $request,
217
                            $response,
218
                            $e ? $e->getPrevious() : null
219
                        );
220
                    }
221
                } else {
222
                    $responseCode = $response->getStatusCode();
223
                }
224
225
                if ($this->checkResponseCode($responseCode, $error['operator'], $error['ifCode'])) {
226
                    $responseMessage = $this->getErrorMessage($body, $error, $response);
227
228
                    throw new BadResponseException(
229
                        $responseCode,
230
                        $responseMessage,
231
                        ($e ? $e->getMessage() : '').'. code: '.$responseCode.', message: '.$responseMessage,
232
                        $request,
233
                        $response,
234
                        $e ? $e->getPrevious() : null
235
                    );
236
                }
237
            }
238
        }
239 1
    }
240
241
    /**
242
     * Parse error codeField from body.
243
     *
244
     * @param array  $body
245
     * @param string $path
246
     *
247
     * @return string|null
248
     */
249
    private function parseError(array $body, $path)
250
    {
251
        $tmp = $body;
252
        foreach (explode('.', $path) as $key) {
253
            if (!isset($tmp[$key])) {
254
                return;
255
            }
256
257
            $tmp = $tmp[$key];
258
        }
259
260
        return $tmp;
261
    }
262
263
    /**
264
     * Get error message from posible field.
265
     *
266
     * @param array                  $body
267
     * @param array                  $error
268
     * @param ResponseInterface|null $response
269
     *
270
     * @return string
271
     */
272
    private function getErrorMessage(array $body, array $error, ResponseInterface $response = null)
273
    {
274
        $message = $this->parseError($body, $error['messageField']);
275
        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...
276
            $message = $error['defaultMessage'];
277
        }
278
        if (!$message) {
279
            $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...
280
        }
281
282
        return $message;
283
    }
284
285
    /**
286
     * Transform and match data with shape.
287
     *
288
     * @param CommandInterface $command
289
     * @param array            $datas
290
     * @param array            $operation
291
     * @param string           $action
292
     *
293
     * @return array
294
     */
295 2
    private function transformData(CommandInterface $command, array $datas, array $operation, $action)
296
    {
297
        $result = [
298 2
            'header' => [],
299
            'uri' => [],
300
            'query' => [],
301
            'body' => [],
302
        ];
303
304 2
        $validator = new Validator();
305
306 2
        if (isset($operation[$action])) {
307
            $shape = $operation[$action];
308
        } else {
309
            $shape = [
310 2
                'type' => null,
311
                'members' => [],
312
            ];
313
        }
314
315 2
        $result['body'] = $this->createData(
316 2
            $validator,
317 2
            $datas,
318 2
            $shape,
319 2
            $command->getName(),
320 2
            $action,
321 2
            $result,
322 2
            ('request' === $action) ? $operation['strictRequest'] : $operation['strictResponse'],
323 2
            $operation['sentEmptyField']
324
        );
325
326 2
        if (!$validator->validate([$command->getName() => $datas])) {
327
            // validation failed
328
            throw $validator->createValidatorException();
329
        }
330
331 2
        return $result;
332
    }
333
334
    /**
335
     * Create request/response data and add validation rule.
336
     *
337
     * @param Validator $validator
338
     * @param array     $datas
339
     * @param array     $shape
340
     * @param string    $path
341
     * @param string    $action
342
     * @param array     &$result
343
     * @param bool      $isStrict
344
     * @param bool      $isSentEmptyField
345
     *
346
     * @return array
347
     */
348 2
    private function createData(
349
        Validator $validator,
350
        array &$datas,
351
        array $shape,
352
        $path,
353
        $action,
354
        array &$result,
355
        $isStrict = false,
356
        $isSentEmptyField = true
357
    ) {
358 2
        $tmpData = $datas;
359 2
        $bodyResult = [];
360 2
        if ('list' === $shape['type']) {
361
            $path .= '[*]';
362
        }
363
364 2
        foreach ($shape['members'] as $name => $parameter) {
365
            $tmpPath = $path.'['.$name.']';
366
            $value = isset($datas[$name]) ? $datas[$name] : null;
367
            if (!$value) {
368
                $datas[$name] = $parameter['defaultValue'];
369
            }
370
371
            // set validator
372
            $validator->add($tmpPath, $parameter, $value);
373
374
            // if nested children
375
            if (isset($parameter['members']) && count($parameter['members']) > 0) {
376
                if (!is_null($value) && !is_array($value)) {
377
                    throw new ValidatorException($tmpPath, sprintf(
378
                        'Expected "%s", but got "%s"',
379
                        $parameter['type'],
380
                        gettype($value)
381
                    ));
382
                }
383
                if ('list' === $parameter['type']) {
384
                    $children = $value ?: [];
385
                    foreach ($children as $idx => $child) {
386
                        if (!is_array($child)) {
387
                            throw new ValidatorException($tmpPath, 'Expected "list", but got "map"');
388
                        }
389
390
                        $bodyResult[$parameter['locationName']][] = $this->createData(
391
                            $validator,
392
                            $datas[$name][$idx],
393
                            $parameter,
394
                            $tmpPath,
395
                            $action,
396
                            $result,
397
                            $isStrict,
398
                            $isSentEmptyField
399
                        );
400
                    }
401
                } elseif ('map' === $parameter['type']) {
402
                    if (is_null($value)) {
403
                        if (array_key_exists($name, $datas)) {
404
                            unset($tmpData[$name]);
405
                        }
406
                        continue;
407
                    }
408
                    $children = $value ?: [];
409
                    foreach (array_keys($parameter['members']) as $key) {
410
                        if (!array_key_exists($key, $children)) {
411
                            $children[$key] = null;
412
                            $datas[$name][$key] = null;
413
                        }
414
                    }
415
416
                    $bodyResult[$parameter['locationName']] = $this->createData(
417
                        $validator,
418
                        $datas[$name],
419
                        $parameter,
420
                        $tmpPath,
421
                        $action,
422
                        $result,
423
                        $isStrict,
424
                        $isSentEmptyField
425
                    );
426
                }
427
            }
428
429
            $formattedValue = $this->formatter->format($parameter['type'], $parameter['format'], $value, $parameter['defaultValue']);
430
            if ('body' !== $parameter['location']) {
431
                $result[$parameter['location']][$parameter['locationName']] = $formattedValue;
432
            } else {
433
                if (!array_key_exists($parameter['locationName'], $bodyResult)) {
434
                    if (!$value && !is_numeric($value)) {
435
                        $value = $parameter['defaultValue'];
436
                    }
437
                    if ($isSentEmptyField || ($value || is_numeric($value))) {
438
                        $bodyResult[$parameter['locationName']] = $formattedValue;
439
                    }
440
                }
441
            }
442
            unset($tmpData[$name]);
443
        }
444
445 2
        if (count($tmpData) > 0) {
446 1
            if ($isStrict) {
447
                throw new BadRequestException(ucwords($action), 'Undefined parameters "'.implode('", "', array_keys($tmpData)).'"');
448
            }
449 1
            foreach ($tmpData as $name => $child) {
450 1
                $bodyResult[$name] = $child;
451
            }
452
        }
453
454 2
        return $bodyResult;
455
    }
456
457
    /**
458
     * Check is responsecode is match with code.
459
     *
460
     * @param string $responseCode
461
     * @param string $operator
462
     * @param string $code
463
     *
464
     * @return bool
465
     */
466
    public function checkResponseCode($responseCode, $operator, $code)
467
    {
468
        switch ($operator) {
469
            case '===':
470
                return $responseCode === $code;
471
            case '!==':
472
                return $responseCode !== $code;
473
            case '!=':
474
                return $responseCode !== $code;
475
            case '<':
476
                return $responseCode < $code;
477
            case '<=':
478
                return $responseCode <= $code;
479
            case '>=':
480
                return $responseCode >= $code;
481
            case '>':
482
                return $responseCode > $code;
483
            default:
484
                return $responseCode === $code;
485
        }
486
    }
487
}
488