Completed
Push — master ( aa1927...26941e )
by Indra
03:57
created

Builder.php (15 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 Request
35
     */
36
    public function commandToRequestTransformer()
37
    {
38
        return function (CommandInterface $command) {
39
            if (!$this->service->hasOperation($command->getName())) {
40
                throw new CommandException(sprintf(
0 ignored issues
show
The call to CommandException::__construct() misses a required argument $command.

This check looks for function calls that miss required arguments.

Loading history...
41
                    'Command "%s" not found',
42
                    $command->getName()
43
                ));
44
            }
45
46
            $operation = $this->service->getOperation($command->getName());
47
48
            $result = $this->transformData($command, $command->toArray(), $operation, 'request');
49
50
            $uri = ltrim($operation['requestUri'], '/');
51
            if ($result['uri']) {
52
                // replace uri
53
                $patterns = [];
54
                $replacements = [];
55
                foreach ($result['uri'] as $key => $value) {
56
                    $patterns[] = '/{'.$key.'}/';
57
                    $replacements[] = $value;
58
                }
59
60
                $uri = preg_replace($patterns, $replacements, $uri);
61
            }
62
63
            $body = null;
64
            if ('rest_json' === $operation['requestProtocol']) {
65
                $body = GuzzleHttp\json_encode($result['body']);
66
                $result['header']['Content-Type'] = 'application/json';
67
            } elseif ('form_params' === $operation['requestProtocol']) {
68
                $body = http_build_query($result['body'], '', '&');
69
                $result['header']['Content-Type'] = 'application/x-www-form-urlencoded';
70
            }
71
72
            if ($result['query']) {
73
                $uri .= sprintf('?%s', http_build_query($result['query'], null, '&', PHP_QUERY_RFC3986));
74
            }
75
76
            return new Request(
77
                $operation['httpMethod'],
78
                $this->service->getEndpoint().$uri,
79
                $result['header'],
80
                $body
81
            );
82
        };
83
    }
84
85
    /**
86
     * Transform response to result.
87
     *
88
     * @return Result
89
     */
90
    public function responseToResultTransformer()
91
    {
92
        return function (
93
            ResponseInterface $response,
94
            RequestInterface $request,
95
            CommandInterface $command
96
        ) {
97
            $operation = $this->service->getOperation($command->getName());
98
            $this->processResponseError($operation, $request, $response);
99
100
            $body = [];
101
            if ('rest_json' === $operation['responseProtocol']) {
102
                $body = GuzzleHttp\json_decode($response->getBody(), true);
103
            }
104
105
            $result = $this->transformData($command, $body, $operation, 'response');
106
107
            foreach ($response->getHeaders() as $name => $header) {
108
                $result['header'][$name] = is_array($header) ? array_pop($header) : null;
109
            }
110
111
            return new Result($result['body'], $result['header']);
112
        };
113
    }
114
115
    public function badResponseExceptionParser()
116
    {
117
        return function (CommandInterface $command, GuzzleBadResponseException $e) {
118
            $operation = $this->service->getOperation($command->getName());
119
120
            return $this->processResponseError(
121
                $operation,
122
                $e->getRequest(),
123
                $e->getResponse(),
0 ignored issues
show
It seems like $e->getResponse() can be null; however, processResponseError() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
124
                $e
125
            );
126
        };
127
    }
128
129
    /**
130
     * Process response to check is error or not.
131
     *
132
     * @param array                           $operation
133
     * @param RequestInterface                $request
134
     * @param ResponseInterface               $response
135
     * @param GuzzleBadResponseException|null $e
136
     *
137
     * @return BadResponseException|null
138
     */
139
    private function processResponseError(
140
        array $operation,
141
        RequestInterface $request,
142
        ResponseInterface $response,
143
        GuzzleBadResponseException $e = null
144
    ) {
145
        $body = null;
146
        if ('rest_json' === $operation['responseProtocol']) {
147
            try {
148
                $body = GuzzleHttp\json_decode($response->getBody(), true);
149
            } catch (\InvalidArgumentException $ex) {
150
                return new BadResponseException(
151
                    '',
152
                    $ex->getMessage(),
153
                    ($e ? $e->getMessage() : ''),
154
                    $request,
155
                    $response,
156
                    $e ? $e->getPrevious() : null
157
                );
158
            }
159
        }
160
161
        $responseCode = 0;
0 ignored issues
show
$responseCode is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
162
        $responseMessage = '';
0 ignored issues
show
$responseMessage is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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 View Code Duplication
                    if (!$error['ifCode']) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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();
188
                }
189
190 View Code Duplication
                if ($error['ifCode'] == $responseCode) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 $response
236
     *
237
     * @return string
238
     */
239
    private function getErrorMessage(array $body, array $error, ResponseInterface $response)
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();
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
        $shape = [];
0 ignored issues
show
$shape is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
274
        if (isset($operation[$action])) {
275
            $shape = $operation[$action];
276
        } else {
277
            $shape = [
278
                'type' => null,
279
                'members' => [],
280
            ];
281
        }
282
283
        $result['body'] = $this->createData(
284
            $validator,
285
            $datas,
286
            $shape,
287
            $command->getName(),
288
            $action,
289
            $result
290
        );
291
292
        if (!$validator->validate([$command->getName() => $datas])) {
293
            // validation failed
294
            throw $validator->createValidatorException();
295
        }
296
297
        return $result;
298
    }
299
300
    /**
301
     * Create request/response data and add validation rule.
302
     *
303
     * @param Validator $validator
304
     * @param array     $datas
305
     * @param array     $shape
306
     * @param string    $path
307
     * @param string    $action
308
     * @param array     &$result
309
     *
310
     * @return array
311
     */
312
    private function createData(
313
        Validator $validator,
314
        array $datas,
315
        array $shape,
316
        $path,
317
        $action,
318
        array &$result
319
    ) {
320
        $bodyResult = [];
321
        if ('list' === $shape['type']) {
322
            $path .= '[*]';
323
        }
324
325
        foreach ($shape['members'] as $name => $parameter) {
326
            $tmpPath = $path.'['.$name.']';
327
            $value = isset($datas[$name]) ? $datas[$name] : null;
328
            // set validator
329
            $validator->add($tmpPath, $parameter, $value);
330
331
            // if nested children
332
            if (isset($parameter['members']) && count($parameter['members']) > 0) {
333
                if (!is_null($value) && !is_array($value)) {
334
                    throw new ValidatorException($tmpPath, sprintf(
335
                        'Expected "%s", but got "%s"',
336
                        $parameter['type'],
337
                        gettype($value)
338
                    ));
339
                }
340
                if ('list' === $parameter['type']) {
341
                    $children = $value ?: [];
342 View Code Duplication
                    foreach ($children as $idx => $child) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
343
                        if (!is_array($child)) {
344
                            throw new ValidatorException($tmpPath, 'Expected "list", but got "map"');
345
                        }
346
347
                        $bodyResult[$parameter['locationName']][] = $this->createData($validator, $child, $parameter, $tmpPath, $action, $result);
348
                    }
349
                } elseif ('map' === $parameter['type']) {
350
                    if (is_null($value)) {
351
                        continue;
352
                    }
353
                    $children = $value ?: [];
354
                    foreach (array_keys($parameter['members']) as $key) {
355
                        if (!array_key_exists($key, $children)) {
356
                            $children[$key] = null;
357
                        }
358
                    }
359
360 View Code Duplication
                    foreach ($children as $parameterName => $child) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
361
                        if (is_array($child)) {
362
                            $bodyResult[$parameter['locationName']][$parameterName] = $this->createData(
363
                                $validator,
364
                                $child,
365
                                $parameter,
366
                                $tmpPath,
367
                                $action,
368
                                $result
369
                            );
370
                        }
371
                    }
372
                    $bodyResult[$parameter['locationName']] = $this->createData(
373
                        $validator,
374
                        $children,
375
                        $parameter,
376
                        $tmpPath,
377
                        $action,
378
                        $result
379
                    );
380
                }
381
            }
382
383
            $value = $this->getFormatedValue($value, $parameter, $action);
384
            if ('body' !== $parameter['location']) {
385
                $result[$parameter['location']][$parameter['locationName']] = $value;
386
            } else {
387
                if (!array_key_exists($parameter['locationName'], $bodyResult)) {
388
                    $bodyResult[$parameter['locationName']] = $value;
389
                }
390
            }
391
            unset($datas[$name]);
392
        }
393
394
        if (count($datas) > 0) {
395
            foreach ($datas as $name => $child) {
396
                $bodyResult[$name] = $child;
397
            }
398
        }
399
400
        return $bodyResult;
401
    }
402
403
    /**
404
     * Get formatted value.
405
     *
406
     * @param mixed  $value     [description]
407
     * @param array  $parameter [description]
408
     * @param string $action    request/response
409
     *
410
     * @return mixed
411
     */
412
    private function getFormatedValue($value, array $parameter, $action)
413
    {
414
        if (!$value) {
415
            $value = $parameter['defaultValue'];
416
        }
417
418
        switch ($parameter['type']) {
419
            case 'integer':
420
                return (int) (string) $value;
421
                break;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
422
            case 'float':
423
                return (float) (string) $value;
424
                break;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
425
            case 'string':
426
                $result = (string) $value;
427
428
                return sprintf($parameter['format'] ?: '%s', $result);
429
                break;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
430
            case 'boolean':
431
                return ($value === 'true' || true === $value) ? true : false;
432
                break;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
433
            case 'number':
434
                if ($parameter['format']) {
435
                    $format = explode('|', $parameter['format']);
436
                    $decimal = isset($format[0]) ? $format[0] : 0;
437
                    $decimalPoint = isset($format[1]) ? $format[1] : '.';
438
                    $thousandsSeparator = isset($format[2]) ? $format[2] : ',';
439
440
                    return number_format((float) (string) $value, $decimal, $decimalPoint, $thousandsSeparator);
441
                }
442
443
                return (string) $value;
444
                break;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
445
            case 'datetime':
446
                if ('request' === $action) {
447
                    if (!$value) {
448
                        return;
449
                    }
450
451
                    if (!($value instanceof \DateTime)) {
452
                        $value = new \DateTime($value);
453
                    }
454
455
                    if ($parameter['format']) {
456
                        return $value->format($parameter['format']);
457
                    }
458
459
                    return $value->format('Y-m-d\TH:i:s\Z');
460
                } elseif ('response' === $action) {
461
                    if ($parameter['format']) {
462
                        return \DateTime::createFromFormat($parameter['format'], $value);
463
                    } else {
464
                        return new \DateTime($value);
465
                    }
466
                }
467
                break;
468
            default:
469
                return;
470
                break;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
471
        }
472
    }
473
}
474