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