Completed
Push — master ( f63f47...49e847 )
by Indra
05:36
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\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);
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) {
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();
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) {
260
            $message = $error['defaultMessage'];
261
        }
262
        if (!$message) {
263
            $message = $response->getReasonPhrase();
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']) {
0 ignored issues
show
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_IF
Loading history...
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