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

Builder.php (1 issue)

Labels
Severity

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);
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) {
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) {
269
            $message = $error['defaultMessage'];
270
        }
271
        if (!$message) {
272
            $message = $response->getReasonPhrase();
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