GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 721b3b...eac8ca )
by Sergey
09:42
created

DataParser::parseProperty()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 22
ccs 0
cts 0
cp 0
rs 8.9197
cc 4
eloc 14
nc 5
nop 3
crap 20
1
<?php
2
/*
3
 * This file is part of the reva2/jsonapi.
4
 *
5
 * (c) Sergey Revenko <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Reva2\JsonApi\Decoders;
12
13
use Neomerx\JsonApi\Document\Error;
14
use Neomerx\JsonApi\Exceptions\JsonApiException;
15
use Reva2\JsonApi\Contracts\Decoders\DataParserInterface;
16
use Reva2\JsonApi\Contracts\Decoders\Mapping\DocumentMetadataInterface;
17
use Reva2\JsonApi\Contracts\Decoders\Mapping\Factory\MetadataFactoryInterface;
18
use Reva2\JsonApi\Contracts\Decoders\Mapping\ObjectMetadataInterface;
19
use Reva2\JsonApi\Contracts\Decoders\Mapping\PropertyMetadataInterface;
20
use Reva2\JsonApi\Contracts\Decoders\Mapping\ResourceMetadataInterface;
21
use Symfony\Component\PropertyAccess\PropertyAccess;
22
use Symfony\Component\PropertyAccess\PropertyAccessor;
23
24
/**
25
 * Data parser
26
 *
27
 * @package Reva2\JsonApi\Decoders
28
 * @author Sergey Revenko <[email protected]>
29
 */
30
class DataParser implements DataParserInterface
31
{
32
    const ERROR_CODE = 'ee2c1d49-ba40-4077-a6bb-b06baceb3e97';
33
34
    /**
35
     * Current path
36
     *
37
     * @var \SplStack
38
     */
39
    protected $path;
40
41
    /**
42
     * Resource decoders factory
43
     *
44
     * @var MetadataFactoryInterface
45
     */
46
    protected $factory;
47
48
    /**
49
     * @var PropertyAccessor
50
     */
51
    protected $accessor;
52
53
    /**
54
     * Constructor
55 14
     *
56
     * @param MetadataFactoryInterface $factory
57 14
     */
58 14
    public function __construct(MetadataFactoryInterface $factory)
59
    {
60 14
        $this->factory = $factory;
61 14
        $this->accessor = PropertyAccess::createPropertyAccessor();
62
        
63
        $this->initPathStack();
64
    }
65
66 13
    /**
67
     * @inheritdoc
68 13
     */
69
    public function setPath($path)
70 12
    {
71
        $this->path->push($this->preparePathSegment($path));
72
73
        return $this;
74
    }
75
76 12
    /**
77
     * @inheritdoc
78 12
     */
79
    public function restorePath()
80 12
    {
81
        $this->path->pop();
82
83
        return $this;
84
    }
85
86 1
    /**
87
     * @inheritdoc
88 1
     */
89 1
    public function getPath()
90
    {
91 1
        $segments = [];
92
        foreach ($this->path as $segment) {
93 1
            $segments[] = $segment;
94
        }
95
96
        return '/' . implode('/', array_reverse($segments));
97
    }
98
99 12
    /**
100
     * @inheritdoc
101 12
     */
102
    public function hasValue($data, $path)
103
    {
104
        return $this->accessor->isReadable($data, $path);
105
    }
106
107 12
    /**
108
     * @inheritdoc
109 12
     */
110
    public function getValue($data, $path)
111
    {
112
        return $this->accessor->getValue($data, $path);
113
    }
114
115 7
    /**
116
     * @inheritdoc
117 7
     */
118
    public function parseString($data, $path)
119 7
    {
120 7
        $this->setPath($path);
121 7
122 7
        $pathValue = null;
123 7
        if ($this->hasValue($data, $path)) {
124 7
            $value = $this->getValue($data, $path);
125 1
            if ((null === $value) || (is_string($value))) {
126 1
                $pathValue = $value;
127
            } else {
128 1
                throw new \InvalidArgumentException(
129
                    sprintf("Value expected to be a string, but %s given", gettype($value)),
130 7
                    400
131 7
                );
132
            }
133 7
        }
134
        $this->restorePath();
135
136
        return $pathValue;
137
    }
138
139 2
    /**
140
     * @inheritdoc
141 2
     */
142
    public function parseInt($data, $path)
143
    {
144
        return $this->parseNumeric($data, $path, 'int');
145
    }
146
147 2
    /**
148
     * @inheritdoc
149 2
     */
150
    public function parseFloat($data, $path)
151
    {
152
        return $this->parseNumeric($data, $path, 'float');
153
    }
154
155 2
    /**
156
     * @inheritdoc
157 2
     */
158 View Code Duplication
    public function parseRaw($data, $path)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
159 2
    {
160 2
        $this->setPath($path);
161 2
162 2
        $pathValue = null;
163 2
        if ($this->hasValue($data, $path)) {
164 2
            $pathValue = $this->getValue($data, $path);
165 1
        }
166 1
167 1
        $this->restorePath();
168 1
169 1
        return $pathValue;
170 1
    }
171
172 1
    /**
173
     * @inheritdoc
174 2
     */
175 View Code Duplication
    public function parseCallback($data, $path, $callback)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
176 2
    {
177
        $this->setPath($path);
178 2
179
        $pathValue = null;
180
        if ($this->hasValue($data, $path)) {
181
            $pathValue = call_user_func($callback, $this->getValue($data, $path));
182
        }
183
184 2
        $this->restorePath();
185
186 2
        return $pathValue;
187
    }
188 2
189 2
    /**
190 2
     * @inheritdoc
191 2
     */
192
    public function parseBool($data, $path)
193
    {
194 2
        $this->setPath($path);
195 2
196 2
        $pathValue = null;
197
        if ($this->hasValue($data, $path)) {
198 2
            $value = $this->getValue($data, $path);
199 1
            if ((null === $value) || (is_bool($value))) {
200 1
                $pathValue = $value;
201
            } elseif (is_string($value)) {
202 1
                $pathValue = (in_array($value, ['true', 'yes', 'y', 'on', 'enabled'])) ? true : false;
203
            } elseif (is_numeric($value)) {
204
                $pathValue = (bool) $value;
205 2
            } else {
206
                throw new \InvalidArgumentException(
207 2
                    sprintf("Value expected to be a boolean, but %s given", gettype($value)),
208
                    400
209 2
                );
210
            }
211
        }
212
213
        $this->restorePath();
214
215 1
        return $pathValue;
216
    }
217 1
218
    /**
219 1
     * @inheritdoc
220 1
     */
221 1
    public function parseDateTime($data, $path, $format = 'Y-m-d')
222 1
    {
223 1
        $this->setPath($path);
224 1
225
        $pathValue = null;
226 1
        if ($this->hasValue($data, $path)) {
227 1
            $value = $this->getValue($data, $path);
228 1
            if (null !== $value) {
229 1
                if (is_string($value)) {
230 1
                    $pathValue = \DateTimeImmutable::createFromFormat($format, $value);
231 1
                }
232
233 1
                if (!$pathValue instanceof \DateTimeImmutable) {
234
                    throw new \InvalidArgumentException(
235 1
                        sprintf("Value expected to be a date/time string in '%s' format", $format),
236 1
                        400
237 1
                    );
238 1
                }
239
            }
240 1
        }
241
242 1
        $this->restorePath();
243
244
        return $pathValue;
245
    }
246
247
    /**
248 1
     * @inheritdoc
249
     */
250 1
    public function parseArray($data, $path, \Closure $itemsParser)
251
    {
252 1
        $this->setPath($path);
253 1
254 1
        $pathValue = null;
255
        if ($this->hasValue($data, $path)) {
256 1
            $value = $this->getValue($data, $path);
257 1
            if ((null !== $value) && (false === is_array($value))) {
258
                throw new \InvalidArgumentException(
259 1
                    sprintf("Value expected to be an array, but %s given", gettype($value)),
260 1
                    400
261
                );
262 1
            } elseif (is_array($value)) {
263
                $pathValue = [];
264
                $keys = array_keys($value);
265
                foreach ($keys as $key) {
266
                    $arrayPath = sprintf("[%s]", $key);
267
268 2
                    $pathValue[$key] = $itemsParser($value, $arrayPath, $this);
269
                }
270
            }
271 2
        }
272
273 2
        $this->restorePath();
274
275 2
        return $pathValue;
276 1
    }
277
278 1
    /**
279 1
     * Parse data object value at specified path as object of specified class
280 1
     *
281 1
     * @param array|object $data
282 1
     * @param string $path
283 1
     * @param string $objType
284 1
     * @return null
285
     */
286 1
    public function parseObject($data, $path, $objType)
287 1
    {
288 1
        $this->setPath($path);
289 1
290 1
        $pathValue = null;
291 1
        if ((true === $this->hasValue($data, $path)) &&
292 1
            (null !== ($value = $this->getValue($data, $path)))
293 1
        ) {
294 1
            $metadata = $this->factory->getMetadataFor($objType);
295
            /* @var $metadata ObjectMetadataInterface */
296 1
297 View Code Duplication
            if (null !== ($discField = $metadata->getDiscriminatorField())) {
0 ignored issues
show
Duplication introduced by
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...
298
                $discValue = $this->parseString($value, $discField->getDataPath());
299
                $discClass = $metadata->getDiscriminatorClass($discValue);
300
                if ($discClass !== $objType) {
301
                    $this->restorePath();
302
303
                    return $this->parseObject($data, $path, $discClass);
304
                }
305
            }
306
307
            $objClass = $metadata->getClassName();
308
            $pathValue = new $objClass();
309
310
            $properties = $metadata->getProperties();
311
            foreach ($properties as $property) {
312
                $this->parseProperty($value, $pathValue, $property);
313
            }
314 12
        }
315
316 12
        $this->restorePath();
317
318
        return $pathValue;
319
    }
320
321
    /**
322 14
     * @inheritdoc
323
     */
324 14
    public function parseResource($data, $path, $resType)
325 14
    {
326
        $this->setPath($path);
327
328
        $pathValue = null;
329
        if ((true === $this->hasValue($data, $path)) &&
330
            (null !== ($value = $this->getValue($data, $path)))
331
        ) {
332
            $metadata = $this->factory->getMetadataFor($resType);
333
            /* @var $metadata ResourceMetadataInterface */
334
335 1 View Code Duplication
            if ((null !== ($discField = $metadata->getDiscriminatorField()))) {
0 ignored issues
show
Duplication introduced by
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...
336
                $discValue = $this->parseString($value, $discField->getDataPath());
337 1
                $discClass = $metadata->getDiscriminatorClass($discValue);
338
                if ($discClass !== $resType) {
339 1
                    $this->restorePath();
340
341 1
                    return $this->parseResource($data, $path, $discClass);
342 1
                }
343
            }
344 1
345 1
            $name = $this->parseString($value, 'type');
346 1
            if ($name !== $metadata->getName()) {
347
                throw new \InvalidArgumentException(
348 1
                    sprintf("Value must contain resource of type '%s'", $metadata->getName()),
349 1
                    409
350 1
                );
351
            }
352 1
353 1
            $objClass = $metadata->getClassName();
354 1
            $pathValue = new $objClass();
355
356 1
            $this->parseProperty($value, $pathValue, $metadata->getIdMetadata());
357 1
358
            foreach ($metadata->getAttributes() as $attribute) {
359 1
                $this->parseProperty($value, $pathValue, $attribute);
360 1
            }
361
362 1
            foreach ($metadata->getRelationships() as $relationship) {
363 1
                $this->parseProperty($value, $pathValue, $relationship);
364
            }
365
        }
366
367
        $this->restorePath();
368
369
        return $pathValue;
370
    }
371 1
372 1
    /**
373
     * @inheritdoc
374
     */
375
    public function parseDocument($data, $docType)
376
    {
377
        try {
378
            $this->initPathStack();
379
380
            $metadata = $this->factory->getMetadataFor($docType);
381
            /* @var $metadata DocumentMetadataInterface */
382
383
            $docClass = $metadata->getClassName();
384
            $doc = new $docClass();
385
386
            $this->parseProperty($data, $doc, $metadata->getContentMetadata());
387
388
            return $doc;
389
        } catch (JsonApiException $e) {
390
            throw $e;
391
        } catch (\Exception $e) {
392 3
            $status = $e->getCode();
393
            $message = 'Failed to parse document';
394 3
            if (empty($status)) {
395
                $message = 'Internal server error';
396 3
                $status = 500;
397 3
            }
398 3
399 3
            $error = new Error(
400 3
                rand(),
401 3
                null,
402 3
                $status,
403
                self::ERROR_CODE,
404 2
                $message,
405 2
                $e->getMessage(),
406 2
                ['pointer' => $this->getPath()]
407
            );
408 2
409
            throw new JsonApiException($error, $status, $e);
410 3
        }
411
    }
412 3
413
    /**
414 3
     * @inheritdoc
415
     */
416
    public function parseQueryParams($data, $paramsType)
417
    {
418
        throw new \RuntimeException('Not implemented');
419
    }
420
421
    /**
422
     * Prepare path segment
423
     *
424
     * @param string $path
425
     * @return string
426
     */
427
    protected function preparePathSegment($path)
428
    {
429
        return trim(preg_replace('~[\/]+~si', '/', str_replace(['.', '[', ']'], '/', (string) $path)), '/');
430
    }
431
432
    /**
433
     * Initialize stack that store current path
434
     */
435
    protected function initPathStack()
436
    {
437
        $this->path = new \SplStack();
438
    }
439
440
    /**
441
     * Parse numeric value
442
     *
443
     * @param mixed $data
444
     * @param string $path
445
     * @param string $type
446
     * @return float|int|null
447
     */
448
    protected function parseNumeric($data, $path, $type)
449
    {
450
        $this->setPath($path);
451
452
        $pathValue = null;
453
        if ($this->hasValue($data, $path)) {
454
            $value = $this->getValue($data, $path);
455
            $rightType = ('int' === $type) ? is_int($value) : is_float($value);
456
            if ($rightType) {
457
                $pathValue = $value;
458
            } elseif (is_numeric($value)) {
459
                $pathValue = ('int' === $type) ? (int) $value : (float) $value;
460
            } elseif (null !== $value) {
461
                throw new \InvalidArgumentException(
462
                    sprintf("Value expected to be %s, but %s given", $type, gettype($value)),
463
                    400
464
                );
465
            }
466
        }
467
468
        $this->restorePath();
469
470
        return $pathValue;
471
    }
472
473
    /**
474
     * Parse property of specified object
475
     *
476
     * @param object|array $data
477
     * @param object $obj
478
     * @param PropertyMetadataInterface $metadata
479
     */
480
    private function parseProperty($data, $obj, PropertyMetadataInterface $metadata)
481
    {
482
        $path = $metadata->getDataPath();
483
484
        if (false === $this->hasValue($data, $path)) {
485
            return;
486
        }
487
488
        if ('custom' === $metadata->getDataType()) {
489
            $value = $this->parseCallback($data, $path, [$obj, $metadata->getDataTypeParams()]);
490
        } else {
491
            $value = $this->parsePropertyValue($data, $path, $metadata);
492
        }
493
494
        $setter = $metadata->getSetter();
495
        if (null !== $setter) {
496
            $obj->{$setter}($value);
497
        } else {
498
            $setter = $metadata->getPropertyName();
499
            $obj->{$setter} = $value;
500
        }
501
    }
502
503
    /**
504
     * Parse value of specified property
505
     *
506
     * @param object|array $data
507
     * @param string $path
508
     * @param PropertyMetadataInterface $metadata
509
     * @return mixed|null
510
     */
511
    private function parsePropertyValue($data, $path, PropertyMetadataInterface $metadata)
512
    {
513
        switch ($metadata->getDataType()) {
514
            case 'scalar':
515
                return $this->parseScalarValue($data, $path, $metadata->getDataTypeParams());
0 ignored issues
show
Bug introduced by
It seems like $metadata->getDataTypeParams() targeting Reva2\JsonApi\Contracts\...ce::getDataTypeParams() can also be of type array; however, Reva2\JsonApi\Decoders\D...ser::parseScalarValue() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
516
517
            case 'datetime':
518
                $format = $metadata->getDataTypeParams();
519
                if (empty($format)) {
520
                    $format = 'Y-m-d';
521
                }
522
523
                return $this->parseDateTime($data, $path, $format);
0 ignored issues
show
Bug introduced by
It seems like $format defined by $metadata->getDataTypeParams() on line 518 can also be of type array; however, Reva2\JsonApi\Decoders\DataParser::parseDateTime() 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...
524
525
            case 'array':
526
                return $this->parseArrayValue($data, $path, $metadata->getDataTypeParams());
0 ignored issues
show
Bug introduced by
It seems like $metadata->getDataTypeParams() targeting Reva2\JsonApi\Contracts\...ce::getDataTypeParams() can also be of type string; however, Reva2\JsonApi\Decoders\D...rser::parseArrayValue() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
527
528
            case 'object':
529
                return $this->parseObjectValue($data, $path, $metadata->getDataTypeParams());
0 ignored issues
show
Bug introduced by
It seems like $metadata->getDataTypeParams() targeting Reva2\JsonApi\Contracts\...ce::getDataTypeParams() can also be of type array; however, Reva2\JsonApi\Decoders\D...ser::parseObjectValue() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
530
531
            case 'raw':
532
                return $this->parseRaw($data, $path);
533
534
            default:
535
                throw new \InvalidArgumentException(sprintf(
536
                    "Unsupported property data type '%s'",
537
                    $metadata->getDataType()
538
                ));
539
        }
540
    }
541
542
    /**
543
     * Parse value as JSON API resource or object
544
     *
545
     * @param object|array $data
546
     * @param string $path
547
     * @param string $objClass
548
     * @return mixed|null
549
     */
550
    public function parseObjectValue($data, $path, $objClass)
551
    {
552
        $metadata = $this->factory->getMetadataFor($objClass);
553
554
        if ($metadata instanceof ResourceMetadataInterface) {
555
            return $this->parseResource($data, $path, $objClass);
556
        } else {
557
            return $this->parseObject($data, $path, $objClass);
558
        }
559
    }
560
561
    /**
562
     * Parse value that contains array
563
     *
564
     * @param $data
565
     * @param $path
566
     * @param array $params
567
     * @return array|null
568
     */
569
    public function parseArrayValue($data, $path, array $params)
570
    {
571
        $type = $params[0];
572
        $typeParams = $params[1];
573
574
        switch ($type) {
575
            case 'scalar':
576
                return $this->parseArray(
577
                    $data,
578
                    $path,
579
                    function ($data, $path, DataParser $parser) use ($typeParams) {
580
                        return $parser->parseScalarValue($data, $path, $typeParams);
581
                    }
582
                );
583
584
            case 'datetime':
585
                $format = (!empty($typeParams)) ? $typeParams : 'Y-m-d';
586
                return $this->parseArray(
587
                    $data,
588
                    $path,
589
                    function ($data, $path, DataParser $parser) use ($format) {
590
                        return $parser->parseDateTime($data, $path, $format);
591
                    }
592
                );
593
594
            case 'object':
595
                return $this->parseArray(
596
                    $data,
597
                    $path,
598
                    function ($data, $path, DataParser $parser) use ($typeParams) {
599
                        return $parser->parseObjectValue($data, $path, $typeParams);
600
                    }
601
                );
602
603
            case 'array':
604
                return $this->parseArray(
605
                    $data,
606
                    $path,
607
                    function ($data, $path, DataParser $parser) use ($typeParams) {
608
                        return $parser->parseArrayValue($data, $path, $typeParams);
609
                    }
610
                );
611
612
            case 'raw':
613
                return $this->parseArray(
614
                    $data,
615
                    $path,
616
                    function ($data, $path, DataParser $parser) {
617
                        return $parser->parseRaw($data, $path);
618
                    }
619
                );
620
621
            default:
622
                throw new \InvalidArgumentException(sprintf(
623
                    "Unsupported array item type '%s' specified",
624
                    $type
625
                ));
626
        }
627
    }
628
629
    /**
630
     * Parse scalar value
631
     *
632
     * @param object|array $data
633
     * @param string $path
634
     * @param string $type
635
     * @return bool|float|int|null|string
636
     */
637
    public function parseScalarValue($data, $path, $type)
638
    {
639
        switch ($type) {
640
            case 'string':
641
                return $this->parseString($data, $path);
642
643
            case 'bool':
644
            case 'boolean':
645
                return $this->parseBool($data, $path);
646
647
            case 'int':
648
            case 'integer':
649
                return $this->parseInt($data, $path);
650
651
            case 'float':
652
            case 'double':
653
                return $this->parseFloat($data, $path);
654
655
            default:
656
                throw new \InvalidArgumentException(sprintf("Unsupported scalar type '%s' specified", $type));
657
        }
658
    }
659
}