Completed
Push — master ( 327515...051294 )
by Indra
02:26
created

Builder.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\StreamResult;
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
            if ('rest_json' === $operation['responseProtocol']) {
120
121
                $body = GuzzleHttp\json_decode($response->getBody(), true);
122
123
                $result = $this->transformData($command, $body, $operation, 'response');
124
125
                foreach ($response->getHeaders() as $name => $header) {
126
                    $result['header'][$name] = is_array($header) ? array_pop($header) : null;
127
                }
128
129
                return new Result($result['body'], $result['header']);
130
            } elseif ('stream' === $operation['responseProtocol']) {
131
                $streamResponse = new StreamResult(
132
                    $response->getStatusCode(),
133
                    $response->getHeaders(),
134
                    $response->getBody(),
135
                    $response->getProtocolVersion(),
136
                    $response->getReasonPhrase()
137
                );
138
139
                return $streamResponse;
140
            }
141
        };
142
    }
143
144
    public function badResponseExceptionParser()
145
    {
146
        return function (CommandInterface $command, GuzzleBadResponseException $e) {
147
            $operation = $this->service->getOperation($command->getName());
148
149
            $this->processResponseError(
150
                $operation ?: [],
151
                $e->getRequest(),
152
                $e->getResponse(),
153
                $e
154
            );
155
        };
156
    }
157
158
    /**
159
     * Process response to check is error or not.
160
     *
161
     * @param array                           $operation
162
     * @param RequestInterface                $request
163
     * @param ResponseInterface               $response
164
     * @param GuzzleBadResponseException|null $e
165
     *
166
     * @throws \IndraGunawan\RestService\Exception\BadResponseException|null
167
     */
168
    private function processResponseError(
169
        array $operation,
170
        RequestInterface $request,
171
        ResponseInterface $response = null,
172
        GuzzleBadResponseException $e = null
173
    ) {
174
        $body = null;
175
        try {
176
            if ('rest_json' === $operation['responseProtocol']) {
177
                $body = GuzzleHttp\json_decode((!is_null($response)) ? $response->getBody() : '', true);
0 ignored issues
show
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...
178
            } elseif ('stream' === $operation['responseProtocol']) {
179
                $body = $response->getBody();
0 ignored issues
show
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...
180
            }
181
        } catch (\InvalidArgumentException $ex) {
182
            throw new BadResponseException(
183
                '',
184
                $ex->getMessage(),
185
                ($ex ? $ex->getMessage() : ''),
186
                $request,
187
                $response,
188
                $ex ? $ex->getPrevious() : null
189
            );
190
        }
191
192
        if ($body) {
193
            foreach ($operation['errors'] as $name => $error) {
194
                if ('field' === $error['type']) {
195
                    $responseCode = $this->parseError($body, $error['codeField']);
196
197
                    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...
198
                        continue;
199
                    }
200
201
                    // if no ifCode property then return exception
202
                    if (!$error['ifCode']) {
203
                        $responseMessage = $this->getErrorMessage($body, $error, $response);
204
205
                        throw new BadResponseException(
206
                            $responseCode,
207
                            $responseMessage,
208
                            ($e ? $e->getMessage() : '').' code: '.$responseCode.', message: '.$responseMessage,
209
                            $request,
210
                            $response,
211
                            $e ? $e->getPrevious() : null
212
                        );
213
                    }
214
                } else {
215
                    $responseCode = $response->getStatusCode();
216
                }
217
218
                if ($this->checkResponseCode($responseCode, $error['operator'], $error['ifCode'])) {
219
                    $responseMessage = $this->getErrorMessage($body, $error, $response);
220
221
                    throw new BadResponseException(
222
                        $responseCode,
223
                        $responseMessage,
224
                        ($e ? $e->getMessage() : '').'. code: '.$responseCode.', message: '.$responseMessage,
225
                        $request,
226
                        $response,
227
                        $e ? $e->getPrevious() : null
228
                    );
229
                }
230
            }
231
        }
232
    }
233
234
    /**
235
     * Parse error codeField from body.
236
     *
237
     * @param array  $body
238
     * @param string $path
239
     *
240
     * @return string|null
241
     */
242
    private function parseError(array $body, $path)
243
    {
244
        $tmp = $body;
245
        foreach (explode('.', $path) as $key) {
246
            if (!isset($tmp[$key])) {
247
                return;
248
            }
249
250
            $tmp = $tmp[$key];
251
        }
252
253
        return $tmp;
254
    }
255
256
    /**
257
     * Get error message from posible field.
258
     *
259
     * @param array                  $body
260
     * @param array                  $error
261
     * @param ResponseInterface|null $response
262
     *
263
     * @return string
264
     */
265
    private function getErrorMessage(array $body, array $error, ResponseInterface $response = null)
266
    {
267
        $message = $this->parseError($body, $error['messageField']);
268
        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...
269
            $message = $error['defaultMessage'];
270
        }
271
        if (!$message) {
272
            $message = $response->getReasonPhrase();
0 ignored issues
show
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...
273
        }
274
275
        return $message;
276
    }
277
278
    /**
279
     * Transform and match data with shape.
280
     *
281
     * @param CommandInterface $command
282
     * @param array            $datas
283
     * @param array            $operation
284
     * @param string           $action
285
     *
286
     * @return array
287
     */
288
    private function transformData(CommandInterface $command, array $datas, array $operation, $action)
289
    {
290
        $result = [
291
            'header' => [],
292
            'uri' => [],
293
            'query' => [],
294
            'body' => [],
295
        ];
296
297
        $validator = new Validator();
298
299
        if (isset($operation[$action])) {
300
            $shape = $operation[$action];
301
        } else {
302
            $shape = [
303
                'type' => null,
304
                'members' => [],
305
            ];
306
        }
307
308
        $result['body'] = $this->createData(
309
            $validator,
310
            $datas,
311
            $shape,
312
            $command->getName(),
313
            $action,
314
            $result,
315
            ('request' === $action) ? $operation['strictRequest'] : $operation['strictResponse'],
316
            $operation['sentEmptyField']
317
        );
318
319
        if (!$validator->validate([$command->getName() => $datas])) {
320
            // validation failed
321
            throw $validator->createValidatorException();
322
        }
323
324
        return $result;
325
    }
326
327
    /**
328
     * Create request/response data and add validation rule.
329
     *
330
     * @param Validator $validator
331
     * @param array     $datas
332
     * @param array     $shape
333
     * @param string    $path
334
     * @param string    $action
335
     * @param array     &$result
336
     * @param bool      $isStrict
337
     * @param bool      $isSentEmptyField
338
     *
339
     * @return array
340
     */
341
    private function createData(
342
        Validator $validator,
343
        array &$datas,
344
        array $shape,
345
        $path,
346
        $action,
347
        array &$result,
348
        $isStrict = false,
349
        $isSentEmptyField = true
350
    ) {
351
        $tmpData = $datas;
352
        $bodyResult = [];
353
        if ('list' === $shape['type']) {
354
            $path .= '[*]';
355
        }
356
357
        foreach ($shape['members'] as $name => $parameter) {
358
            $tmpPath = $path.'['.$name.']';
359
            $value = isset($datas[$name]) ? $datas[$name] : null;
360
            if (!$value) {
361
                $datas[$name] = $parameter['defaultValue'];
362
            }
363
364
            // set validator
365
            $validator->add($tmpPath, $parameter, $value);
366
367
            // if nested children
368
            if (isset($parameter['members']) && count($parameter['members']) > 0) {
369
                if (!is_null($value) && !is_array($value)) {
370
                    throw new ValidatorException($tmpPath, sprintf(
371
                        'Expected "%s", but got "%s"',
372
                        $parameter['type'],
373
                        gettype($value)
374
                    ));
375
                }
376
                if ('list' === $parameter['type']) {
377
                    $children = $value ?: [];
378
                    foreach ($children as $idx => $child) {
379
                        if (!is_array($child)) {
380
                            throw new ValidatorException($tmpPath, 'Expected "list", but got "map"');
381
                        }
382
383
                        $bodyResult[$parameter['locationName']][] = $this->createData(
384
                            $validator,
385
                            $datas[$name][$idx],
386
                            $parameter,
387
                            $tmpPath,
388
                            $action,
389
                            $result,
390
                            $isStrict,
391
                            $isSentEmptyField
392
                        );
393
                    }
394
                } elseif ('map' === $parameter['type']) {
395
                    if (is_null($value)) {
396
                        if (array_key_exists($name, $datas)) {
397
                            unset($tmpData[$name]);
398
                        }
399
                        continue;
400
                    }
401
                    $children = $value ?: [];
402
                    foreach (array_keys($parameter['members']) as $key) {
403
                        if (!array_key_exists($key, $children)) {
404
                            $children[$key] = null;
405
                            $datas[$name][$key] = null;
406
                        }
407
                    }
408
409
                    $bodyResult[$parameter['locationName']] = $this->createData(
410
                        $validator,
411
                        $datas[$name],
412
                        $parameter,
413
                        $tmpPath,
414
                        $action,
415
                        $result,
416
                        $isStrict,
417
                        $isSentEmptyField
418
                    );
419
                }
420
            }
421
422
            $formattedValue = $this->formatter->format($parameter['type'], $parameter['format'], $value, $parameter['defaultValue']);
423
            if ('body' !== $parameter['location']) {
424
                $result[$parameter['location']][$parameter['locationName']] = $formattedValue;
425
            } else {
426
                if (!array_key_exists($parameter['locationName'], $bodyResult)) {
427
                    if (!$value && !is_numeric($value)) {
428
                        $value = $parameter['defaultValue'];
429
                    }
430
                    if ($isSentEmptyField || ($value || is_numeric($value))) {
431
                        $bodyResult[$parameter['locationName']] = $formattedValue;
432
                    }
433
                }
434
            }
435
            unset($tmpData[$name]);
436
        }
437
438
        if (count($tmpData) > 0) {
439
            if ($isStrict) {
440
                throw new BadRequestException(ucwords($action), 'Undefined parameters "'.implode('", "', array_keys($tmpData)).'"');
441
            }
442
            foreach ($tmpData as $name => $child) {
443
                $bodyResult[$name] = $child;
444
            }
445
        }
446
447
        return $bodyResult;
448
    }
449
450
    /**
451
     * Check is responsecode is match with code.
452
     *
453
     * @param string $responseCode
454
     * @param string $operator
455
     * @param string $code
456
     *
457
     * @return bool
458
     */
459
    public function checkResponseCode($responseCode, $operator, $code)
460
    {
461
        switch ($operator) {
462
            case '===':
463
                return $responseCode === $code;
464
            case '!==':
465
                return $responseCode !== $code;
466
            case '!=':
467
                return $responseCode != $code;
468
            case '<':
469
                return $responseCode < $code;
470
            case '<=':
471
                return $responseCode <= $code;
472
            case '>=':
473
                return $responseCode >= $code;
474
            case '>':
475
                return $responseCode > $code;
476
            default:
477
                return $responseCode == $code;
478
        }
479
    }
480
}
481