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 ( a1e77f...9aca7a )
by Sergey
04:15
created

DataParser::hasValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 2
crap 1
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 19
    public function __construct(MetadataFactoryInterface $factory)
61
    {
62 19
        $this->factory = $factory;
63 19
        $this->accessor = PropertyAccess::createPropertyAccessor();
64
        
65 19
        $this->initPathStack();
66 19
    }
67
68
    /**
69
     * @inheritdoc
70
     */
71 14
    public function setPath($path)
72
    {
73 14
        $this->path->push($this->preparePathSegment($path));
74
75 14
        return $this;
76
    }
77
78
    /**
79
     * @inheritdoc
80
     */
81 14
    public function restorePath()
82
    {
83 14
        $this->path->pop();
84
85 14
        return $this;
86
    }
87
88
    /**
89
     * @inheritdoc
90
     */
91 2
    public function getPath()
92
    {
93 2
        $segments = [];
94 2
        foreach ($this->path as $segment) {
95 1
            $segments[] = $segment;
96 2
        }
97
98 2
        return '/' . implode('/', array_reverse($segments));
99
    }
100
101
    /**
102
     * @inheritdoc
103
     */
104 14
    public function hasValue($data, $path)
105
    {
106 14
        return $this->accessor->isReadable($data, $path);
107
    }
108
109
    /**
110
     * @inheritdoc
111
     */
112 14
    public function getValue($data, $path)
113
    {
114 14
        return $this->accessor->getValue($data, $path);
115
    }
116
117
    /**
118
     * @inheritdoc
119
     */
120 8
    public function parseString($data, $path)
121
    {
122 8
        $this->setPath($path);
123
124 8
        $pathValue = null;
125 8
        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 8
        $this->restorePath();
137
138 8
        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 5
    public function parseResource($data, $path, $resType)
308
    {
309 5
        $this->setPath($path);
310
311 5
        $pathValue = null;
312 5
        if ((true === $this->hasValue($data, $path)) &&
313 5
            (null !== ($value = $this->getValue($data, $path)))
314 5
        ) {
315 5
            $metadata = $this->factory->getMetadataFor($resType);
316
            /* @var $metadata ResourceMetadataInterface */
317
318 5
            $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 4
    public function parseDocument($data, $docType)
356
    {
357
        try {
358 4
            $this->initPathStack();
359
360 4
            $metadata = $this->factory->getMetadataFor($docType);
361 4
            if (!$metadata instanceof DocumentMetadataInterface) {
362 1
                throw new \InvalidArgumentException(sprintf("Failed to parse %s as JSON API document", $docType));
363
            }
364
365
            /* @var $metadata \Reva2\JsonApi\Contracts\Decoders\Mapping\DocumentMetadataInterface */
366
367 3
            $docClass = $metadata->getClassName();
368 3
            $doc = new $docClass();
369
370 3
            $this->parseProperty($data, $doc, $metadata->getContentMetadata());
371
372 2
            return $doc;
373 2
        } catch (JsonApiException $e) {
374
            throw $e;
375 2
        } catch (\Exception $e) {
376 2
            throw  $this->convertToApiException($e, 'document');
377
        }
378
    }
379
380
    /**
381
     * @inheritdoc
382
     */
383 3
    public function parseQueryParams($data, $paramsType)
384
    {
385
        try {
386 3
            $this->initPathStack();
387
388 3
            $query = $this->parseObjectValue($data, $paramsType);
389 3
            if (!$query instanceof EncodingParametersInterface) {
390
                throw new \InvalidArgumentException(sprintf(
391
                    "Query parameters object must implement interface %s",
392
                    EncodingParametersInterface::class
393
                ));
394
            }
395
396 3
            return $query;
397
        } catch (JsonApiException $e) {
398
            throw $e;
399
        } catch (\Exception $e) {
400
            throw  $this->convertToApiException($e, 'query');
401
        }
402
    }
403
404
    /**
405
     * Prepare path segment
406
     *
407
     * @param string $path
408
     * @return string
409
     */
410 14
    protected function preparePathSegment($path)
411
    {
412 14
        return trim(preg_replace('~[\/]+~si', '/', str_replace(['.', '[', ']'], '/', (string) $path)), '/');
413
    }
414
415
    /**
416
     * Initialize stack that store current path
417
     */
418 19
    protected function initPathStack()
419
    {
420 19
        $this->path = new \SplStack();
421 19
    }
422
423
    /**
424
     * Parse numeric value
425
     *
426
     * @param mixed $data
427
     * @param string $path
428
     * @param string $type
429
     * @return float|int|null
430
     */
431 5
    protected function parseNumeric($data, $path, $type)
432
    {
433 5
        $this->setPath($path);
434
435 5
        $pathValue = null;
436 5
        if ($this->hasValue($data, $path)) {
437 5
            $value = $this->getValue($data, $path);
438 5
            $rightType = ('int' === $type) ? is_int($value) : is_float($value);
439 5
            if ($rightType) {
440 3
                $pathValue = $value;
441 5
            } elseif (is_numeric($value)) {
442 4
                $pathValue = ('int' === $type) ? (int) $value : (float) $value;
443 5
            } elseif (null !== $value) {
444 2
                throw new \InvalidArgumentException(
445 2
                    sprintf("Value expected to be %s, but %s given", $type, gettype($value)),
446
                    400
447 2
                );
448
            }
449 5
        }
450
451 5
        $this->restorePath();
452
453 5
        return $pathValue;
454
    }
455
456
    /**
457
     * Parse property of specified object
458
     *
459
     * @param object|array $data
460
     * @param object $obj
461
     * @param PropertyMetadataInterface $metadata
462
     */
463 7
    private function parseProperty($data, $obj, PropertyMetadataInterface $metadata)
464
    {
465 7
        $path = $metadata->getDataPath();
466
467 7
        if (false === $this->hasValue($data, $path)) {
468 4
            return;
469
        }
470
471 7
        if ('custom' === $metadata->getDataType()) {
472 4
            $value = $this->parseCallback($data, $path, [$obj, $metadata->getDataTypeParams()]);
473 4
        } else {
474 7
            $value = $this->parsePropertyValue($data, $path, $metadata);
475
        }
476
477 7
        $setter = $metadata->getSetter();
478 7
        if (null !== $setter) {
479 6
            $obj->{$setter}($value);
480 6
        } else {
481 5
            $setter = $metadata->getPropertyName();
482 5
            $obj->{$setter} = $value;
483
        }
484 7
    }
485
486
    /**
487
     * Parse value of specified property
488
     *
489
     * @param object|array $data
490
     * @param string $path
491
     * @param PropertyMetadataInterface $metadata
492
     * @return mixed|null
493
     */
494 7
    private function parsePropertyValue($data, $path, PropertyMetadataInterface $metadata)
495
    {
496 7
        switch ($metadata->getDataType()) {
497 7
            case 'scalar':
498 7
                return $this->parseScalarValue($data, $path, $metadata->getDataTypeParams());
499
500 5
            case 'datetime':
501 1
                $format = $metadata->getDataTypeParams();
502 1
                if (empty($format)) {
503 1
                    $format = 'Y-m-d';
504 1
                }
505
506 1
                return $this->parseDateTime($data, $path, $format);
507
508 5
            case 'array':
509 3
                return $this->parseArrayValue($data, $path, $metadata->getDataTypeParams());
510
511 5
            case 'object':
512 5
                return $this->parseResourceOrObject($data, $path, $metadata->getDataTypeParams());
513
514 1
            case 'raw':
515 1
                return $this->parseRaw($data, $path);
516
517
            default:
518
                throw new \InvalidArgumentException(sprintf(
519
                    "Unsupported property data type '%s'",
520
                    $metadata->getDataType()
521
                ));
522
        }
523
    }
524
525
    /**
526
     * Parse value as JSON API resource or object
527
     *
528
     * @param object|array $data
529
     * @param string $path
530
     * @param string $objClass
531
     * @return mixed|null
532
     */
533 5
    public function parseResourceOrObject($data, $path, $objClass)
534
    {
535 5
        $metadata = $this->factory->getMetadataFor($objClass);
536
537 5
        if ($metadata instanceof ResourceMetadataInterface) {
538 4
            return $this->parseResource($data, $path, $objClass);
539
        } else {
540 1
            return $this->parseObject($data, $path, $objClass);
541
        }
542
    }
543
544
    /**
545
     * Parse value that contains JSON API object
546
     *
547
     * @param object|array $data
548
     * @param string $objType
549
     * @return mixed
550
     */
551 4
    public function parseObjectValue($data, $objType)
552
    {
553 4
        $metadata = $this->factory->getMetadataFor($objType);
554 4
        if (!$metadata instanceof ObjectMetadataInterface) {
555
            throw new \InvalidArgumentException('Invalid object metadata');
556
        }
557
558 4
        $discClass = $this->getClassByDiscriminator($metadata, $data);
559 4
        if ((null !== $discClass) && ($discClass !== $objType)) {
560 1
            return $this->parseObjectValue($data, $discClass);
561
        }
562
563 4
        $objClass = $metadata->getClassName();
564 4
        $obj = new $objClass();
565
566 4
        $properties = $metadata->getProperties();
567 4
        foreach ($properties as $property) {
568 4
            $this->parseProperty($data, $obj, $property);
569 4
        }
570
571 4
        return $obj;
572
    }
573
574
    /**
575
     * Parse value that contains array
576
     *
577
     * @param object|array $data
578
     * @param string $path
579
     * @param array $params
580
     * @return array|null
581
     */
582 3
    public function parseArrayValue($data, $path, array $params)
583
    {
584 3
        $type = $params[0];
585 3
        $typeParams = $params[1];
586
587
        switch ($type) {
588 3
            case 'scalar':
589 1
                return $this->parseArray(
590 1
                    $data,
591 1
                    $path,
592
                    function ($data, $path, DataParser $parser) use ($typeParams) {
593 1
                        return $parser->parseScalarValue($data, $path, $typeParams);
594
                    }
595 1
                );
596
597 3
            case 'datetime':
598 1
                $format = (!empty($typeParams)) ? $typeParams : 'Y-m-d';
599 1
                return $this->parseArray(
600 1
                    $data,
601 1
                    $path,
602
                    function ($data, $path, DataParser $parser) use ($format) {
603 1
                        return $parser->parseDateTime($data, $path, $format);
604
                    }
605 1
                );
606
607 3
            case 'object':
608 3
                return $this->parseArray(
609 3
                    $data,
610 3
                    $path,
611
                    function ($data, $path, DataParser $parser) use ($typeParams) {
612 3
                        return $parser->parseResourceOrObject($data, $path, $typeParams);
613
                    }
614 3
                );
615
616 1
            case 'array':
617 1
                return $this->parseArray(
618 1
                    $data,
619 1
                    $path,
620
                    function ($data, $path, DataParser $parser) use ($typeParams) {
621 1
                        return $parser->parseArrayValue($data, $path, $typeParams);
622
                    }
623 1
                );
624
625 1
            case 'raw':
626 1
                return $this->parseArray(
627 1
                    $data,
628 1
                    $path,
629 1
                    function ($data, $path, DataParser $parser) {
630 1
                        return $parser->parseRaw($data, $path);
631
                    }
632 1
                );
633
634
            default:
635
                throw new \InvalidArgumentException(sprintf(
636
                    "Unsupported array item type '%s' specified",
637
                    $type
638
                ));
639
        }
640
    }
641
642
    /**
643
     * Parse scalar value
644
     *
645
     * @param object|array $data
646
     * @param string $path
647
     * @param string $type
648
     * @return bool|float|int|null|string
649
     */
650 7
    public function parseScalarValue($data, $path, $type)
651
    {
652
        switch ($type) {
653 7
            case 'string':
654 5
                return $this->parseString($data, $path);
655
656 3
            case 'bool':
657 3
            case 'boolean':
658 1
                return $this->parseBool($data, $path);
659
660 3
            case 'int':
661 3
            case 'integer':
662 3
                return $this->parseInt($data, $path);
663
664 1
            case 'float':
665 1
            case 'double':
666 1
                return $this->parseFloat($data, $path);
667
668
            default:
669
                throw new \InvalidArgumentException(sprintf("Unsupported scalar type '%s' specified", $type));
670
        }
671
    }
672
673
    /**
674
     * Convert any exception to JSON API exception
675
     *
676
     * @param \Exception $e
677
     * @param string $objType
678
     * @return JsonApiException
679
     */
680 2
    private function convertToApiException(\Exception $e, $objType)
681
    {
682 2
        $status = $e->getCode();
683 2
        $message = 'Failed to parse request';
684 2
        if (empty($status)) {
685 1
            $message = 'Internal server error';
686 1
            $status = 500;
687 1
        }
688
689 2
        $source = null;
690
        switch ($objType) {
691 2
            case 'document':
692 2
                $source = ['pointer' => $this->getPath()];
693 2
                break;
694
695
            case 'query':
696
                $source = ['parameter' => $this->getPath()];
697
                break;
698
        }
699
700 2
        $error = new Error(rand(), null, $status, self::ERROR_CODE, $message, $e->getMessage(), $source);
701
702 2
        return new JsonApiException($error, $status, $e);
703
    }
704
705
    /**
706
     * Returns appropriate discriminator class for specified data
707
     *
708
     * @param ClassMetadataInterface $metadata
709
     * @param array|object $data
710
     * @return string|null
711
     */
712 8
    private function getClassByDiscriminator(ClassMetadataInterface $metadata, $data)
713
    {
714 8
        if (null === ($discField = $metadata->getDiscriminatorField())) {
715 7
            return null;
716
        }
717
718 6
        $discValue = $this->parseString($data, $discField->getDataPath());
719 6
        if (empty($discValue)) {
720 1
            $this->setPath($discField->getDataPath());
721
722 1
            throw new \InvalidArgumentException("Field value required and can not be empty", 422);
723
        }
724
725 5
        return $metadata->getDiscriminatorClass($discValue);
726
    }
727
}
728