Completed
Push — master ( b8a64c...327515 )
by Indra
03:33
created

Builder::commandToRequestTransformer()   C

Complexity

Conditions 10
Paths 1

Size

Total Lines 60
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 0
loc 60
ccs 0
cts 47
cp 0
rs 6.5333
cc 10
eloc 36
nc 1
nop 0
crap 110

How to fix   Long Method    Complexity   

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:

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