Completed
Push — master ( 63bd6b...2bc6aa )
by Indra
02:23
created

Builder::checkResponseCode()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 21
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

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