Completed
Push — master ( 49e847...b8a64c )
by Indra
05:31
created

Builder::responseToResultTransformer()   B

Complexity

Conditions 5
Paths 1

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 24
ccs 0
cts 19
cp 0
rs 8.5125
cc 5
eloc 14
nc 1
nop 0
crap 30
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