Completed
Push — master ( 2ed0f3...63bd6b )
by Indra
02:29
created

Builder.php (3 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\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
            return $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
     * @return BadResponseException|null
141
     */
142
    private function processResponseError(
143
        array $operation,
144
        RequestInterface $request,
145
        ResponseInterface $response = null,
146
        GuzzleBadResponseException $e = null
147
    ) {
148
        $body = null;
149
        if ('rest_json' === $operation['responseProtocol']) {
150
            try {
151
                $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...
152
            } catch (\InvalidArgumentException $ex) {
153
                return new BadResponseException(
154
                    '',
155
                    $ex->getMessage(),
156
                    ($e ? $e->getMessage() : ''),
157
                    $request,
158
                    $response,
159
                    $e ? $e->getPrevious() : null
160
                );
161
            }
162
        }
163
164
        if ($body) {
165
            foreach ($operation['errors'] as $name => $error) {
166
                if ('field' === $error['type']) {
167
                    $responseCode = $this->parseError($body, $error['codeField']);
168
169
                    if (!$responseCode) {
170
                        continue;
171
                    }
172
173
                    // if no ifCode property then return exception
174
                    if (!$error['ifCode']) {
175
                        $responseMessage = $this->getErrorMessage($body, $error, $response);
176
177
                        return new BadResponseException(
178
                            $responseCode,
179
                            $responseMessage,
180
                            ($e ? $e->getMessage() : '').' code: '.$responseCode.', message: '.$responseMessage,
181
                            $request,
182
                            $response,
183
                            $e ? $e->getPrevious() : null
184
                        );
185
                    }
186
                } else {
187
                    $responseCode = $response->getStatusCode();
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...
188
                }
189
190
                if ($error['ifCode'] == $responseCode) {
191
                    $responseMessage = $this->getErrorMessage($body, $error, $response);
192
193
                    return 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
            }
203
        }
204
205
        return;
206
    }
207
208
    /**
209
     * Parse error codeField from body.
210
     *
211
     * @param array  $body
212
     * @param string $path
213
     *
214
     * @return string|null
215
     */
216
    private function parseError(array $body, $path)
217
    {
218
        $tmp = $body;
219
        foreach (explode('.', $path) as $key) {
220
            if (!isset($tmp[$key])) {
221
                return;
222
            }
223
224
            $tmp = $tmp[$key];
225
        }
226
227
        return $tmp;
228
    }
229
230
    /**
231
     * Get error message from posible field.
232
     *
233
     * @param array                  $body
234
     * @param array                  $error
235
     * @param ResponseInterface|null $response
236
     *
237
     * @return string
238
     */
239
    private function getErrorMessage(array $body, array $error, ResponseInterface $response = null)
240
    {
241
        $message = $this->parseError($body, $error['messageField']);
242
        if (!$message) {
243
            $message = $error['defaultMessage'];
244
        }
245
        if (!$message && $response->getStatusCode() >= 400) {
246
            $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...
247
        }
248
249
        return $message;
250
    }
251
252
    /**
253
     * Transform and match data with shape.
254
     *
255
     * @param CommandInterface $command
256
     * @param array            $datas
257
     * @param array            $operation
258
     * @param string           $action
259
     *
260
     * @return array
261
     */
262
    private function transformData(CommandInterface $command, array $datas, array $operation, $action)
263
    {
264
        $result = [
265
            'header' => [],
266
            'uri' => [],
267
            'query' => [],
268
            'body' => [],
269
        ];
270
271
        $validator = new Validator();
272
273
        if (isset($operation[$action])) {
274
            $shape = $operation[$action];
275
        } else {
276
            $shape = [
277
                'type' => null,
278
                'members' => [],
279
            ];
280
        }
281
282
        $result['body'] = $this->createData(
283
            $validator,
284
            $datas,
285
            $shape,
286
            $command->getName(),
287
            $action,
288
            $result
289
        );
290
291
        if (!$validator->validate([$command->getName() => $datas])) {
292
            // validation failed
293
            throw $validator->createValidatorException();
294
        }
295
296
        return $result;
297
    }
298
299
    /**
300
     * Create request/response data and add validation rule.
301
     *
302
     * @param Validator $validator
303
     * @param array     $datas
304
     * @param array     $shape
305
     * @param string    $path
306
     * @param string    $action
307
     * @param array     &$result
308
     *
309
     * @return array
310
     */
311
    private function createData(
312
        Validator $validator,
313
        array $datas,
314
        array $shape,
315
        $path,
316
        $action,
317
        array &$result
318
    ) {
319
        $bodyResult = [];
320
        if ('list' === $shape['type']) {
321
            $path .= '[*]';
322
        }
323
324
        foreach ($shape['members'] as $name => $parameter) {
325
            $tmpPath = $path.'['.$name.']';
326
            $value = isset($datas[$name]) ? $datas[$name] : null;
327
            // set validator
328
            $validator->add($tmpPath, $parameter, $value);
329
330
            // if nested children
331
            if (isset($parameter['members']) && count($parameter['members']) > 0) {
332
                if (!is_null($value) && !is_array($value)) {
333
                    throw new ValidatorException($tmpPath, sprintf(
334
                        'Expected "%s", but got "%s"',
335
                        $parameter['type'],
336
                        gettype($value)
337
                    ));
338
                }
339
                if ('list' === $parameter['type']) {
340
                    $children = $value ?: [];
341
                    foreach ($children as $idx => $child) {
342
                        if (!is_array($child)) {
343
                            throw new ValidatorException($tmpPath, 'Expected "list", but got "map"');
344
                        }
345
346
                        $bodyResult[$parameter['locationName']][] = $this->createData($validator, $child, $parameter, $tmpPath, $action, $result);
347
                    }
348
                } elseif ('map' === $parameter['type']) {
349
                    if (is_null($value)) {
350
                        continue;
351
                    }
352
                    $children = $value ?: [];
353
                    foreach (array_keys($parameter['members']) as $key) {
354
                        if (!array_key_exists($key, $children)) {
355
                            $children[$key] = null;
356
                        }
357
                    }
358
359
                    foreach ($children as $parameterName => $child) {
360
                        if (is_array($child)) {
361
                            $bodyResult[$parameter['locationName']][$parameterName] = $this->createData(
362
                                $validator,
363
                                $child,
364
                                $parameter,
365
                                $tmpPath,
366
                                $action,
367
                                $result
368
                            );
369
                        }
370
                    }
371
                    $bodyResult[$parameter['locationName']] = $this->createData(
372
                        $validator,
373
                        $children,
374
                        $parameter,
375
                        $tmpPath,
376
                        $action,
377
                        $result
378
                    );
379
                }
380
            }
381
382
            $value = $this->getFormatedValue($value, $parameter, $action);
383
            if ('body' !== $parameter['location']) {
384
                $result[$parameter['location']][$parameter['locationName']] = $value;
385
            } else {
386
                if (!array_key_exists($parameter['locationName'], $bodyResult)) {
387
                    $bodyResult[$parameter['locationName']] = $value;
388
                }
389
            }
390
            unset($datas[$name]);
391
        }
392
393
        if (count($datas) > 0) {
394
            foreach ($datas as $name => $child) {
395
                $bodyResult[$name] = $child;
396
            }
397
        }
398
399
        return $bodyResult;
400
    }
401
402
    /**
403
     * Get formatted value.
404
     *
405
     * @param mixed  $value     [description]
406
     * @param array  $parameter [description]
407
     * @param string $action    request/response
408
     *
409
     * @return mixed
410
     */
411
    private function getFormatedValue($value, array $parameter, $action)
412
    {
413
        if (!$value) {
414
            $value = $parameter['defaultValue'];
415
        }
416
417
        switch ($parameter['type']) {
418
            case 'integer':
419
                return (int) (string) $value;
420
            case 'float':
421
                return (float) (string) $value;
422
            case 'string':
423
                $result = (string) $value;
424
425
                return sprintf($parameter['format'] ?: '%s', $result);
426
            case 'boolean':
427
                return ($value === 'true' || true === $value) ? true : false;
428
            case 'number':
429
                if ($parameter['format']) {
430
                    $format = explode('|', $parameter['format']);
431
                    $decimal = isset($format[0]) ? $format[0] : 0;
432
                    $decimalPoint = isset($format[1]) ? $format[1] : '.';
433
                    $thousandsSeparator = isset($format[2]) ? $format[2] : ',';
434
435
                    return number_format((float) (string) $value, $decimal, $decimalPoint, $thousandsSeparator);
436
                }
437
438
                return (string) $value;
439
            case 'datetime':
440
                if ('request' === $action) {
441
                    if (!$value) {
442
                        return;
443
                    }
444
445
                    if (!($value instanceof \DateTime)) {
446
                        $value = new \DateTime($value);
447
                    }
448
449
                    if ($parameter['format']) {
450
                        return $value->format($parameter['format']);
451
                    }
452
453
                    return $value->format('Y-m-d\TH:i:s\Z');
454
                } elseif ('response' === $action) {
455
                    if ($parameter['format']) {
456
                        return \DateTime::createFromFormat($parameter['format'], $value);
457
                    } else {
458
                        return new \DateTime($value);
459
                    }
460
                }
461
                //
462
            default:
463
                return;
464
        }
465
    }
466
}
467