Builder   F
last analyzed

Complexity

Total Complexity 83

Size/Duplication

Total Lines 462
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 42.92%

Importance

Changes 0
Metric Value
wmc 83
lcom 1
cbo 13
dl 0
loc 462
ccs 94
cts 219
cp 0.4292
rs 3.4814
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
C commandToRequestTransformer() 0 60 10
B responseToResultTransformer() 0 32 6
A badResponseExceptionParser() 0 13 2
C processResponseError() 0 65 17
A parseError() 0 13 3
A getErrorMessage() 0 12 3
B transformData() 0 38 4
F createData() 0 108 29
B checkResponseCode() 0 21 8

How to fix   Complexity   

Complex Class

Complex classes like Builder 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Builder, and based on these observations, apply Extract Interface, too.

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