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