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 ( 9ee3c2...6cf3d3 )
by Sergey
05:55
created

DataParser::parseScalarValue()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 8.512

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 22
ccs 12
cts 15
cp 0.8
rs 6.6037
cc 8
eloc 15
nc 8
nop 3
crap 8.512
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\Contracts\Encoder\Parameters\EncodingParametersInterface;
14
use Neomerx\JsonApi\Document\Error;
15
use Neomerx\JsonApi\Exceptions\JsonApiException;
16
use Reva2\JsonApi\Contracts\Decoders\DataParserInterface;
17
use Reva2\JsonApi\Contracts\Decoders\Mapping\ClassMetadataInterface;
18
use Reva2\JsonApi\Contracts\Decoders\Mapping\DocumentMetadataInterface;
19
use Reva2\JsonApi\Contracts\Decoders\Mapping\Factory\MetadataFactoryInterface;
20
use Reva2\JsonApi\Contracts\Decoders\Mapping\ObjectMetadataInterface;
21
use Reva2\JsonApi\Contracts\Decoders\Mapping\PropertyMetadataInterface;
22
use Reva2\JsonApi\Contracts\Decoders\Mapping\ResourceMetadataInterface;
23
use Symfony\Component\PropertyAccess\PropertyAccess;
24
use Symfony\Component\PropertyAccess\PropertyAccessor;
25
26
/**
27
 * Data parser
28
 *
29
 * @package Reva2\JsonApi\Decoders
30
 * @author Sergey Revenko <[email protected]>
31
 */
32
class DataParser implements DataParserInterface
33
{
34
    const ERROR_CODE = 'ee2c1d49-ba40-4077-a6bb-b06baceb3e97';
35
36
    /**
37
     * Current path
38
     *
39
     * @var \SplStack
40
     */
41
    protected $path;
42
43
    /**
44
     * Resource decoders factory
45
     *
46
     * @var MetadataFactoryInterface
47
     */
48
    protected $factory;
49
50
    /**
51
     * @var PropertyAccessor
52
     */
53
    protected $accessor;
54
55
    /**
56
     * Constructor
57
     *
58
     * @param MetadataFactoryInterface $factory
59
     */
60 17
    public function __construct(MetadataFactoryInterface $factory)
61
    {
62 17
        $this->factory = $factory;
63 17
        $this->accessor = PropertyAccess::createPropertyAccessor();
64
        
65 17
        $this->initPathStack();
66 17
    }
67
68
    /**
69
     * @inheritdoc
70
     */
71 13
    public function setPath($path)
72
    {
73 13
        $this->path->push($this->preparePathSegment($path));
74
75 13
        return $this;
76
    }
77
78
    /**
79
     * @inheritdoc
80
     */
81 13
    public function restorePath()
82
    {
83 13
        $this->path->pop();
84
85 13
        return $this;
86
    }
87
88
    /**
89
     * @inheritdoc
90
     */
91 1
    public function getPath()
92
    {
93 1
        $segments = [];
94 1
        foreach ($this->path as $segment) {
95 1
            $segments[] = $segment;
96 1
        }
97
98 1
        return '/' . implode('/', array_reverse($segments));
99
    }
100
101
    /**
102
     * @inheritdoc
103
     */
104 13
    public function hasValue($data, $path)
105
    {
106 13
        return $this->accessor->isReadable($data, $path);
107
    }
108
109
    /**
110
     * @inheritdoc
111
     */
112 13
    public function getValue($data, $path)
113
    {
114 13
        return $this->accessor->getValue($data, $path);
115
    }
116
117
    /**
118
     * @inheritdoc
119
     */
120 7
    public function parseString($data, $path)
121
    {
122 7
        $this->setPath($path);
123
124 7
        $pathValue = null;
125 7
        if ($this->hasValue($data, $path)) {
126 7
            $value = $this->getValue($data, $path);
127 7
            if ((null === $value) || (is_string($value))) {
128 7
                $pathValue = $value;
129 7
            } else {
130 1
                throw new \InvalidArgumentException(
131 1
                    sprintf("Value expected to be a string, but %s given", gettype($value)),
132
                    400
133 1
                );
134 1
            }
135 7
        }
136 7
        $this->restorePath();
137
138 7
        return $pathValue;
139
    }
140
141
    /**
142
     * @inheritdoc
143
     */
144 4
    public function parseInt($data, $path)
145
    {
146 4
        return $this->parseNumeric($data, $path, 'int');
147
    }
148
149
    /**
150
     * @inheritdoc
151
     */
152 2
    public function parseFloat($data, $path)
153
    {
154 2
        return $this->parseNumeric($data, $path, 'float');
155
    }
156
157
    /**
158
     * @inheritdoc
159
     */
160 1
    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...
161
    {
162 1
        $this->setPath($path);
163
164 1
        $pathValue = null;
165 1
        if ($this->hasValue($data, $path)) {
166 1
            $pathValue = $this->getValue($data, $path);
167 1
        }
168
169 1
        $this->restorePath();
170
171 1
        return $pathValue;
172
    }
173
174
    /**
175
     * @inheritdoc
176
     */
177 4
    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...
178
    {
179 4
        $this->setPath($path);
180
181 4
        $pathValue = null;
182 4
        if ($this->hasValue($data, $path)) {
183 4
            $pathValue = call_user_func($callback, $this->getValue($data, $path));
184 4
        }
185
186 4
        $this->restorePath();
187
188 4
        return $pathValue;
189
    }
190
191
    /**
192
     * @inheritdoc
193
     */
194 2
    public function parseBool($data, $path)
195
    {
196 2
        $this->setPath($path);
197
198 2
        $pathValue = null;
199 2
        if ($this->hasValue($data, $path)) {
200 2
            $value = $this->getValue($data, $path);
201 2
            if ((null === $value) || (is_bool($value))) {
202 1
                $pathValue = $value;
203 2
            } elseif (is_string($value)) {
204 2
                $pathValue = (in_array($value, ['true', 'yes', 'y', 'on', 'enabled'])) ? true : false;
205 2
            } elseif (is_numeric($value)) {
206 1
                $pathValue = (bool) $value;
207 1
            } else {
208 1
                throw new \InvalidArgumentException(
209 1
                    sprintf("Value expected to be a boolean, but %s given", gettype($value)),
210
                    400
211 1
                );
212
            }
213 2
        }
214
215 2
        $this->restorePath();
216
217 2
        return $pathValue;
218
    }
219
220
    /**
221
     * @inheritdoc
222
     */
223 2
    public function parseDateTime($data, $path, $format = 'Y-m-d')
224
    {
225 2
        $this->setPath($path);
226
227 2
        $pathValue = null;
228 2
        if ($this->hasValue($data, $path)) {
229 2
            $value = $this->getValue($data, $path);
230 2
            if (null !== $value) {
231 2
                if (is_string($value)) {
232 2
                    $pathValue = \DateTimeImmutable::createFromFormat($format, $value);
233 2
                }
234
235 2
                if (!$pathValue instanceof \DateTimeImmutable) {
236 1
                    throw new \InvalidArgumentException(
237 1
                        sprintf("Value expected to be a date/time string in '%s' format", $format),
238
                        400
239 1
                    );
240
                }
241 2
            }
242 2
        }
243
244 2
        $this->restorePath();
245
246 2
        return $pathValue;
247
    }
248
249
    /**
250
     * @inheritdoc
251
     */
252 5
    public function parseArray($data, $path, \Closure $itemsParser)
253
    {
254 4
        $this->setPath($path);
255
256 4
        $pathValue = null;
257 4
        if ($this->hasValue($data, $path)) {
258 4
            $value = $this->getValue($data, $path);
259 4
            if ((null !== $value) && (false === is_array($value))) {
260 1
                throw new \InvalidArgumentException(
261 1
                    sprintf("Value expected to be an array, but %s given", gettype($value)),
262
                    400
263 1
                );
264 4
            } elseif (is_array($value)) {
265 5
                $pathValue = [];
266 4
                $keys = array_keys($value);
267 4
                foreach ($keys as $key) {
268 4
                    $arrayPath = sprintf("[%s]", $key);
269
270 4
                    $pathValue[$key] = $itemsParser($value, $arrayPath, $this);
271 3
                }
272 3
            }
273 3
        }
274
275 3
        $this->restorePath();
276
277 3
        return $pathValue;
278
    }
279
280
    /**
281
     * Parse data object value at specified path as object of specified class
282
     *
283
     * @param array|object $data
284
     * @param string $path
285
     * @param string $objType
286
     * @return null
287
     */
288 1
    public function parseObject($data, $path, $objType)
289
    {
290 1
        $this->setPath($path);
291
292 1
        $pathValue = null;
293 1
        if ((true === $this->hasValue($data, $path)) &&
294 1
            (null !== ($value = $this->getValue($data, $path)))
295 1
        ) {
296 1
            $this->restorePath();
297
298 1
            $pathValue = $this->parseObjectValue($value, $objType);
299 1
        }
300
301 1
        return $pathValue;
302
    }
303
304
    /**
305
     * @inheritdoc
306
     */
307 4
    public function parseResource($data, $path, $resType)
308
    {
309 4
        $this->setPath($path);
310
311 4
        $pathValue = null;
312 4
        if ((true === $this->hasValue($data, $path)) &&
313 4
            (null !== ($value = $this->getValue($data, $path)))
314 4
        ) {
315 4
            $metadata = $this->factory->getMetadataFor($resType);
316
            /* @var $metadata ResourceMetadataInterface */
317
318 4
            $discClass = $this->getClassByDiscriminator($metadata, $value);
319 4
            if ((null !== $discClass) && ($discClass !== $resType)) {
320 4
                $this->restorePath();
321
322 4
                return $this->parseResource($data, $path, $discClass);
323
            }
324
325 4
            $name = $this->parseString($value, 'type');
326 4
            if ($name !== $metadata->getName()) {
327 1
                throw new \InvalidArgumentException(
328 1
                    sprintf("Value must contain resource of type '%s'", $metadata->getName()),
329
                    409
330 1
                );
331
            }
332
333 4
            $objClass = $metadata->getClassName();
334 4
            $pathValue = new $objClass();
335
336 4
            $this->parseProperty($value, $pathValue, $metadata->getIdMetadata());
337
338 4
            foreach ($metadata->getAttributes() as $attribute) {
339 4
                $this->parseProperty($value, $pathValue, $attribute);
340 4
            }
341
342 4
            foreach ($metadata->getRelationships() as $relationship) {
343 4
                $this->parseProperty($value, $pathValue, $relationship);
344 3
            }
345 3
        }
346
347 3
        $this->restorePath();
348
349 3
        return $pathValue;
350
    }
351
352
    /**
353
     * @inheritdoc
354
     */
355 3
    public function parseDocument($data, $docType)
356
    {
357
        try {
358 3
            $this->initPathStack();
359
360 3
            $metadata = $this->factory->getMetadataFor($docType);
361
            /* @var $metadata DocumentMetadataInterface */
362
363 3
            $docClass = $metadata->getClassName();
364 3
            $doc = new $docClass();
365
366 3
            $this->parseProperty($data, $doc, $metadata->getContentMetadata());
367
368 2
            return $doc;
369 1
        } catch (JsonApiException $e) {
370
            throw $e;
371 1
        } catch (\Exception $e) {
372 1
            throw  $this->convertToApiException($e, 'document');
373
        }
374
    }
375
376
    /**
377
     * @inheritdoc
378
     */
379 3
    public function parseQueryParams($data, $paramsType)
380
    {
381
        try {
382 3
            $this->initPathStack();
383
384 3
            $query = $this->parseObjectValue($data, $paramsType);
385 3
            if (!$query instanceof EncodingParametersInterface) {
386
                throw new \InvalidArgumentException(sprintf(
387
                    "Query parameters object must implement interface %s",
388
                    EncodingParametersInterface::class
389
                ));
390
            }
391
392 3
            return $query;
393
        } catch (JsonApiException $e) {
394
            throw $e;
395
        } catch (\Exception $e) {
396
            throw  $this->convertToApiException($e, 'query');
397
        }
398
    }
399
400
    /**
401
     * Prepare path segment
402
     *
403
     * @param string $path
404
     * @return string
405
     */
406 13
    protected function preparePathSegment($path)
407
    {
408 13
        return trim(preg_replace('~[\/]+~si', '/', str_replace(['.', '[', ']'], '/', (string) $path)), '/');
409
    }
410
411
    /**
412
     * Initialize stack that store current path
413
     */
414 17
    protected function initPathStack()
415
    {
416 17
        $this->path = new \SplStack();
417 17
    }
418
419
    /**
420
     * Parse numeric value
421
     *
422
     * @param mixed $data
423
     * @param string $path
424
     * @param string $type
425
     * @return float|int|null
426
     */
427 5
    protected function parseNumeric($data, $path, $type)
428
    {
429 5
        $this->setPath($path);
430
431 5
        $pathValue = null;
432 5
        if ($this->hasValue($data, $path)) {
433 5
            $value = $this->getValue($data, $path);
434 5
            $rightType = ('int' === $type) ? is_int($value) : is_float($value);
435 5
            if ($rightType) {
436 3
                $pathValue = $value;
437 5
            } elseif (is_numeric($value)) {
438 4
                $pathValue = ('int' === $type) ? (int) $value : (float) $value;
439 5
            } elseif (null !== $value) {
440 2
                throw new \InvalidArgumentException(
441 2
                    sprintf("Value expected to be %s, but %s given", $type, gettype($value)),
442
                    400
443 2
                );
444
            }
445 5
        }
446
447 5
        $this->restorePath();
448
449 5
        return $pathValue;
450
    }
451
452
    /**
453
     * Parse property of specified object
454
     *
455
     * @param object|array $data
456
     * @param object $obj
457
     * @param PropertyMetadataInterface $metadata
458
     */
459 7
    private function parseProperty($data, $obj, PropertyMetadataInterface $metadata)
460
    {
461 7
        $path = $metadata->getDataPath();
462
463 7
        if (false === $this->hasValue($data, $path)) {
464 4
            return;
465
        }
466
467 7
        if ('custom' === $metadata->getDataType()) {
468 4
            $value = $this->parseCallback($data, $path, [$obj, $metadata->getDataTypeParams()]);
469 4
        } else {
470 7
            $value = $this->parsePropertyValue($data, $path, $metadata);
471
        }
472
473 7
        $setter = $metadata->getSetter();
474 7
        if (null !== $setter) {
475 6
            $obj->{$setter}($value);
476 6
        } else {
477 5
            $setter = $metadata->getPropertyName();
478 5
            $obj->{$setter} = $value;
479
        }
480 7
    }
481
482
    /**
483
     * Parse value of specified property
484
     *
485
     * @param object|array $data
486
     * @param string $path
487
     * @param PropertyMetadataInterface $metadata
488
     * @return mixed|null
489
     */
490 7
    private function parsePropertyValue($data, $path, PropertyMetadataInterface $metadata)
491
    {
492 7
        switch ($metadata->getDataType()) {
493 7
            case 'scalar':
494 7
                return $this->parseScalarValue($data, $path, $metadata->getDataTypeParams());
495
496 5
            case 'datetime':
497 1
                $format = $metadata->getDataTypeParams();
498 1
                if (empty($format)) {
499 1
                    $format = 'Y-m-d';
500 1
                }
501
502 1
                return $this->parseDateTime($data, $path, $format);
503
504 5
            case 'array':
505 3
                return $this->parseArrayValue($data, $path, $metadata->getDataTypeParams());
506
507 5
            case 'object':
508 5
                return $this->parseResourceOrObject($data, $path, $metadata->getDataTypeParams());
509
510 1
            case 'raw':
511 1
                return $this->parseRaw($data, $path);
512
513
            default:
514
                throw new \InvalidArgumentException(sprintf(
515
                    "Unsupported property data type '%s'",
516
                    $metadata->getDataType()
517
                ));
518
        }
519
    }
520
521
    /**
522
     * Parse value as JSON API resource or object
523
     *
524
     * @param object|array $data
525
     * @param string $path
526
     * @param string $objClass
527
     * @return mixed|null
528
     */
529 5
    public function parseResourceOrObject($data, $path, $objClass)
530
    {
531 5
        $metadata = $this->factory->getMetadataFor($objClass);
532
533 5
        if ($metadata instanceof ResourceMetadataInterface) {
534 4
            return $this->parseResource($data, $path, $objClass);
535
        } else {
536 1
            return $this->parseObject($data, $path, $objClass);
537
        }
538
    }
539
540
    /**
541
     * Parse value that contains JSON API object
542
     *
543
     * @param object|array $data
544
     * @param string $objType
545
     * @return mixed
546
     */
547 4
    public function parseObjectValue($data, $objType)
548
    {
549 4
        $metadata = $this->factory->getMetadataFor($objType);
550 4
        if (!$metadata instanceof ObjectMetadataInterface) {
551
            throw new \InvalidArgumentException('Invalid object metadata');
552
        }
553
554 4
        $discClass = $this->getClassByDiscriminator($metadata, $data);
555 4
        if ((null !== $discClass) && ($discClass !== $objType)) {
556 1
            return $this->parseObjectValue($data, $discClass);
557
        }
558
559 4
        $objClass = $metadata->getClassName();
560 4
        $obj = new $objClass();
561
562 4
        $properties = $metadata->getProperties();
563 4
        foreach ($properties as $property) {
564 4
            $this->parseProperty($data, $obj, $property);
565 4
        }
566
567 4
        return $obj;
568
    }
569
570
    /**
571
     * Parse value that contains array
572
     *
573
     * @param object|array $data
574
     * @param string $path
575
     * @param array $params
576
     * @return array|null
577
     */
578 3
    public function parseArrayValue($data, $path, array $params)
579
    {
580 3
        $type = $params[0];
581 3
        $typeParams = $params[1];
582
583
        switch ($type) {
584 3
            case 'scalar':
585 1
                return $this->parseArray(
586 1
                    $data,
587 1
                    $path,
588
                    function ($data, $path, DataParser $parser) use ($typeParams) {
589 1
                        return $parser->parseScalarValue($data, $path, $typeParams);
590
                    }
591 1
                );
592
593 3
            case 'datetime':
594 1
                $format = (!empty($typeParams)) ? $typeParams : 'Y-m-d';
595 1
                return $this->parseArray(
596 1
                    $data,
597 1
                    $path,
598
                    function ($data, $path, DataParser $parser) use ($format) {
599 1
                        return $parser->parseDateTime($data, $path, $format);
600
                    }
601 1
                );
602
603 3
            case 'object':
604 3
                return $this->parseArray(
605 3
                    $data,
606 3
                    $path,
607
                    function ($data, $path, DataParser $parser) use ($typeParams) {
608 3
                        return $parser->parseResourceOrObject($data, $path, $typeParams);
609
                    }
610 3
                );
611
612 1
            case 'array':
613 1
                return $this->parseArray(
614 1
                    $data,
615 1
                    $path,
616
                    function ($data, $path, DataParser $parser) use ($typeParams) {
617 1
                        return $parser->parseArrayValue($data, $path, $typeParams);
618
                    }
619 1
                );
620
621 1
            case 'raw':
622 1
                return $this->parseArray(
623 1
                    $data,
624 1
                    $path,
625 1
                    function ($data, $path, DataParser $parser) {
626 1
                        return $parser->parseRaw($data, $path);
627
                    }
628 1
                );
629
630
            default:
631
                throw new \InvalidArgumentException(sprintf(
632
                    "Unsupported array item type '%s' specified",
633
                    $type
634
                ));
635
        }
636
    }
637
638
    /**
639
     * Parse scalar value
640
     *
641
     * @param object|array $data
642
     * @param string $path
643
     * @param string $type
644
     * @return bool|float|int|null|string
645
     */
646 7
    public function parseScalarValue($data, $path, $type)
647
    {
648
        switch ($type) {
649 7
            case 'string':
650 5
                return $this->parseString($data, $path);
651
652 3
            case 'bool':
653 3
            case 'boolean':
654 1
                return $this->parseBool($data, $path);
655
656 3
            case 'int':
657 3
            case 'integer':
658 3
                return $this->parseInt($data, $path);
659
660 1
            case 'float':
661 1
            case 'double':
662 1
                return $this->parseFloat($data, $path);
663
664
            default:
665
                throw new \InvalidArgumentException(sprintf("Unsupported scalar type '%s' specified", $type));
666
        }
667
    }
668
669
    /**
670
     * Convert any exception to JSON API exception
671
     *
672
     * @param \Exception $e
673
     * @param string $objType
674
     * @return JsonApiException
675
     */
676 1
    private function convertToApiException(\Exception $e, $objType)
677
    {
678 1
        $status = $e->getCode();
679 1
        $message = 'Failed to parse request';
680 1
        if (empty($status)) {
681
            $message = 'Internal server error';
682
            $status = 500;
683
        }
684
685 1
        $source = null;
686
        switch ($objType) {
687 1
            case 'document':
688 1
                $source = ['pointer' => $this->getPath()];
689 1
                break;
690
691
            case 'query':
692
                $source = ['parameter' => $this->getPath()];
693
                break;
694
        }
695
696 1
        $error = new Error(rand(), null, $status, self::ERROR_CODE, $message, $e->getMessage(), $source);
697
698 1
        return new JsonApiException($error, $status, $e);
699
    }
700
701
    /**
702
     * Returns appropriate discriminator class for specified data
703
     *
704
     * @param ClassMetadataInterface $metadata
705
     * @param array|object $data
706
     * @return string|null
707
     */
708 7
    private function getClassByDiscriminator(ClassMetadataInterface $metadata, $data)
709
    {
710 7
        if (null === ($discField = $metadata->getDiscriminatorField())) {
711 7
            return null;
712
        }
713
714 5
        $discValue = $this->parseString($data, $discField->getDataPath());
715 5
        if (null === $discValue) {
716
            return null;
717
        }
718
719 5
        return $metadata->getDiscriminatorClass($discValue);
720
    }
721
}
722