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
Pull Request — master (#10)
by Sergey
01:44
created

DataParser   F

Complexity

Total Complexity 155

Size/Duplication

Total Lines 1006
Duplicated Lines 5.37 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 91.63%

Importance

Changes 0
Metric Value
wmc 155
lcom 1
cbo 13
dl 54
loc 1006
ccs 405
cts 442
cp 0.9163
rs 1.576
c 0
b 0
f 0

38 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A setPath() 0 6 1
A restorePath() 0 6 1
A getSerializationGroups() 0 4 1
A setSerializationGroups() 0 6 1
A getPath() 0 9 2
A hasValue() 0 4 1
A getValue() 0 4 1
A parseString() 0 20 4
A parseInt() 0 4 1
A parseFloat() 0 4 1
A parseRaw() 0 13 2
A parseCallback() 0 13 2
B parseBool() 0 25 7
A parseDateTime() 0 25 5
A parseArray() 6 27 4
A parseObject() 0 15 3
F parseResource() 0 73 19
A parseQueryParams() 0 21 4
A preparePathSegment() 0 4 1
A initPathStack() 0 4 1
A initContext() 0 4 1
B parseNumeric() 6 24 7
B parseProperty() 0 26 6
B parsePropertyValue() 0 30 7
A parseResourceOrObject() 0 16 4
A parseObjectValue() 0 22 5
A getClassByDiscriminator() 0 15 3
A isExcludedProperty() 0 11 3
B parseLinkedResources() 0 29 10
B parseRelationship() 18 39 8
B parseArrayValue() 0 59 7
B parseScalarValue() 0 22 8
A convertToApiException() 0 24 4
A parseDocument() 0 34 5
B parseArrayRelationship() 24 48 8
A setProperty() 0 10 2
A parseErrors() 0 28 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DataParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DataParser, and based on these observations, apply Extract Interface, too.

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\CallbackResolverInterface;
17
use Reva2\JsonApi\Contracts\Decoders\DataParserInterface;
18
use Reva2\JsonApi\Contracts\Decoders\Mapping\ClassMetadataInterface;
19
use Reva2\JsonApi\Contracts\Decoders\Mapping\DocumentMetadataInterface;
20
use Reva2\JsonApi\Contracts\Decoders\Mapping\Factory\MetadataFactoryInterface;
21
use Reva2\JsonApi\Contracts\Decoders\Mapping\ObjectMetadataInterface;
22
use Reva2\JsonApi\Contracts\Decoders\Mapping\PropertyMetadataInterface;
23
use Reva2\JsonApi\Contracts\Decoders\Mapping\ResourceMetadataInterface;
24
use Symfony\Component\PropertyAccess\PropertyAccess;
25
use Symfony\Component\PropertyAccess\PropertyAccessor;
26
27
/**
28
 * Data parser
29
 *
30
 * @package Reva2\JsonApi\Decoders
31
 * @author Sergey Revenko <[email protected]>
32
 */
33
class DataParser implements DataParserInterface
34
{
35
    const ERROR_CODE = 'ee2c1d49-ba40-4077-a6bb-b06baceb3e97';
36
37
    /**
38
     * Current path
39
     *
40
     * @var \SplStack
41
     */
42
    protected $path;
43
44
    /**
45
     * @var Context
46
     */
47
    protected $context;
48
49
    /**
50
     * Resource decoders factory
51
     *
52
     * @var MetadataFactoryInterface
53
     */
54
    protected $factory;
55
56
    /**
57
     * @var PropertyAccessor
58
     */
59
    protected $accessor;
60
61
    /**
62
     * @var CallbackResolverInterface
63
     */
64
    protected $callbackResolver;
65
66
    /**
67
     * Serialization groups
68
     *
69
     * @var string[]
70
     */
71
    protected $serializationGroups = ['Default'];
72
73
    /**
74
     * Constructor
75
     *
76
     * @param MetadataFactoryInterface $factory
77
     * @param CallbackResolverInterface $callbackResolver
78
     */
79 21
    public function __construct(MetadataFactoryInterface $factory, CallbackResolverInterface $callbackResolver)
80
    {
81 21
        $this->factory = $factory;
82 21
        $this->callbackResolver = $callbackResolver;
83 21
        $this->accessor = PropertyAccess::createPropertyAccessorBuilder()
84 21
            ->enableExceptionOnInvalidIndex()
85 21
            ->getPropertyAccessor();
86
87 21
        $this->initPathStack();
88 21
        $this->initContext();
89 21
    }
90
91
    /**
92
     * @inheritdoc
93
     */
94 16
    public function setPath($path)
95
    {
96 16
        $this->path->push($this->preparePathSegment($path));
97
98 16
        return $this;
99
    }
100
101
    /**
102
     * @inheritdoc
103
     */
104 16
    public function restorePath()
105
    {
106 16
        $this->path->pop();
107
108 16
        return $this;
109
    }
110
111
    /**
112
     * @return string[]
113
     */
114 3
    public function getSerializationGroups()
115
    {
116 3
        return $this->serializationGroups;
117
    }
118
119
    /**
120
     * @param string[] $serializationGroups
121
     * @return $this
122
     */
123 3
    public function setSerializationGroups(array $serializationGroups)
124
    {
125 3
        $this->serializationGroups = $serializationGroups;
126
127 3
        return $this;
128
    }
129
130
    /**
131
     * @inheritdoc
132
     */
133 4
    public function getPath()
134
    {
135 4
        $segments = [];
136 4
        foreach ($this->path as $segment) {
137 3
            $segments[] = $segment;
138
        }
139
140 4
        return '/' . implode('/', array_reverse($segments));
141
    }
142
143
    /**
144
     * @inheritdoc
145
     */
146 17
    public function hasValue($data, $path)
147
    {
148 17
        return $this->accessor->isReadable($data, $path);
149
    }
150
151
    /**
152
     * @inheritdoc
153
     */
154 16
    public function getValue($data, $path)
155
    {
156 16
        return $this->accessor->getValue($data, $path);
157
    }
158
159
    /**
160
     * @inheritdoc
161
     */
162 10
    public function parseString($data, $path)
163
    {
164 10
        $this->setPath($path);
165
166 10
        $pathValue = null;
167 10
        if ($this->hasValue($data, $path)) {
168 10
            $value = $this->getValue($data, $path);
169 10
            if ((null === $value) || (is_string($value))) {
170 10
                $pathValue = $value;
171
            } else {
172 1
                throw new \InvalidArgumentException(
173 1
                    sprintf("Value expected to be a string, but %s given", gettype($value)),
174 1
                    400
175
                );
176
            }
177
        }
178 10
        $this->restorePath();
179
180 10
        return $pathValue;
181
    }
182
183
    /**
184
     * @inheritdoc
185
     */
186 3
    public function parseInt($data, $path)
187
    {
188 3
        return $this->parseNumeric($data, $path, 'int');
189
    }
190
191
    /**
192
     * @inheritdoc
193
     */
194 2
    public function parseFloat($data, $path)
195
    {
196 2
        return $this->parseNumeric($data, $path, 'float');
197
    }
198
199
    /**
200
     * @inheritdoc
201
     */
202 2
    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...
203
    {
204 2
        $this->setPath($path);
205
206 2
        $pathValue = null;
207 2
        if ($this->hasValue($data, $path)) {
208 2
            $pathValue = $this->getValue($data, $path);
209
        }
210
211 2
        $this->restorePath();
212
213 2
        return $pathValue;
214
    }
215
216
    /**
217
     * @inheritdoc
218
     */
219 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...
220
    {
221 4
        $this->setPath($path);
222
223 4
        $pathValue = null;
224 4
        if ($this->hasValue($data, $path)) {
225 4
            $pathValue = call_user_func($callback, $this->getValue($data, $path));
226
        }
227
228 4
        $this->restorePath();
229
230 4
        return $pathValue;
231
    }
232
233
    /**
234
     * @inheritdoc
235
     */
236 2
    public function parseBool($data, $path)
237
    {
238 2
        $this->setPath($path);
239
240 2
        $pathValue = null;
241 2
        if ($this->hasValue($data, $path)) {
242 2
            $value = $this->getValue($data, $path);
243 2
            if ((null === $value) || (is_bool($value))) {
244 1
                $pathValue = $value;
245 2
            } elseif (is_string($value)) {
246 2
                $pathValue = (in_array($value, ['true', 'yes', 'y', 'on', 'enabled'])) ? true : false;
247 1
            } elseif (is_numeric($value)) {
248 1
                $pathValue = (bool) $value;
249
            } else {
250 1
                throw new \InvalidArgumentException(
251 1
                    sprintf("Value expected to be a boolean, but %s given", gettype($value)),
252 1
                    400
253
                );
254
            }
255
        }
256
257 2
        $this->restorePath();
258
259 2
        return $pathValue;
260
    }
261
262
    /**
263
     * @inheritdoc
264
     */
265 2
    public function parseDateTime($data, $path, $format = 'Y-m-d')
266
    {
267 2
        $this->setPath($path);
268
269 2
        $pathValue = null;
270 2
        if ($this->hasValue($data, $path)) {
271 2
            $value = $this->getValue($data, $path);
272 2
            if (null !== $value) {
273 2
                if (is_string($value)) {
274 2
                    $pathValue = \DateTimeImmutable::createFromFormat($format, $value);
275
                }
276
277 2
                if (!$pathValue instanceof \DateTimeImmutable) {
278 1
                    throw new \InvalidArgumentException(
279 1
                        sprintf("Value expected to be a date/time string in '%s' format", $format),
280 1
                        400
281
                    );
282
                }
283
            }
284
        }
285
286 2
        $this->restorePath();
287
288 2
        return $pathValue;
289
    }
290
291
    /**
292
     * @inheritdoc
293
     */
294 8
    public function parseArray($data, $path, \Closure $itemsParser)
295
    {
296 8
        $this->setPath($path);
297
298 8
        $pathValue = null;
299 8
        if ($this->hasValue($data, $path)) {
300 6
            $value = $this->getValue($data, $path);
301 6 View Code Duplication
            if (false === is_array($value)) {
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...
302 1
                throw new \InvalidArgumentException(
303 1
                    sprintf("Value expected to be an array, but %s given", gettype($value)),
304 1
                    400
305
                );
306
            }
307
308 6
            $pathValue = [];
309 6
            $keys = array_keys($value);
310 6
            foreach ($keys as $key) {
311 6
                $arrayPath = sprintf("[%s]", $key);
312
313 6
                $pathValue[$key] = $itemsParser($value, $arrayPath, $this);
314
            }
315
        }
316
317 7
        $this->restorePath();
318
319 7
        return $pathValue;
320
    }
321
322
    /**
323
     * Parse data object value at specified path as object of specified class
324
     *
325
     * @param array|object $data
326
     * @param string $path
327
     * @param string $objType
328
     * @return null
329
     */
330 1
    public function parseObject($data, $path, $objType)
331
    {
332 1
        $this->setPath($path);
333
334 1
        $pathValue = null;
335 1
        if ((true === $this->hasValue($data, $path)) &&
336 1
            (null !== ($value = $this->getValue($data, $path)))
337
        ) {
338 1
            $this->restorePath();
339
340 1
            $pathValue = $this->parseObjectValue($value, $objType);
341
        }
342
343 1
        return $pathValue;
344
    }
345
346
    /**
347
     * @inheritdoc
348
     */
349 6
    public function parseResource($data, $path, $resType, $loader = null)
350
    {
351 6
        $this->setPath($path);
352
353 6
        $pathValue = null;
354 6
        if ((true === $this->hasValue($data, $path)) &&
355 6
            (null !== ($value = $this->getValue($data, $path)))
356
        ) {
357 6
            $metadata = $this->factory->getMetadataFor($resType);
358
            /* @var $metadata ResourceMetadataInterface */
359
360 6
            $name = $this->parseString($value, 'type');
361 6
            if ($name !== $metadata->getName()) {
362 1
                throw new \InvalidArgumentException(
363 1
                    sprintf("Value must contain resource of type '%s'", $metadata->getName()),
364 1
                    409
365
                );
366
            }
367
368 6
            $id = ($this->hasValue($value, 'id')) ? $this->getValue($value, 'id') : null;
369
370 6
            if (null !== $id) {
371 6
                $pathValue = $this->context->getResource($name, $id);
372 6
                if (null !== $pathValue) {
373
                    $id = null;
374
                }
375
            }
376
377 6
            if (null === $pathValue && $loader === null) {
378 6
                $loader = $metadata->getLoader();
379
            }
380
381 6
            if ((null !== $loader) && (null !== $id)) {
382
                $callback = $this->callbackResolver->resolveCallback($loader);
383
                $pathValue = call_user_func($callback, $id, $metadata);
384
            }
385
386 6
            if (null === $pathValue) {
387 6
                $discClass = $this->getClassByDiscriminator($metadata, $value);
388 5
                if ((null !== $discClass) && ($discClass !== $resType)) {
389 4
                    $metadata = $this->factory->getMetadataFor($discClass);
390
                }
391
392 5
                $objClass = $metadata->getClassName();
393 5
                $pathValue = new $objClass();
394
395 5
                if (null !== ($idMetadata = $metadata->getIdMetadata())) {
396 5
                    $this->parseProperty($value, $pathValue, $idMetadata);
397
                }
398
            } else {
399
                $valueClass = get_class($pathValue);
400
                if ($valueClass !== $resType) {
401
                    $metadata = $this->factory->getMetadataFor($valueClass);
402
                }
403
            }
404
405 5
            foreach ($metadata->getAttributes() as $attribute) {
406 5
                $this->parseProperty($value, $pathValue, $attribute);
407
            }
408
409 5
            foreach ($metadata->getRelationships() as $relationship) {
410 5
                $this->parseRelationship($value, $pathValue, $relationship);
411
            }
412
413 4
            if (null !== $id) {
414 4
                $this->context->registerResource($name, $id, $pathValue);
415
            }
416
        }
417
418 4
        $this->restorePath();
419
420 4
        return $pathValue;
421
    }
422
423
    /**
424
     * @inheritdoc
425
     */
426 5
    public function parseDocument($data, $docType)
427
    {
428
        try {
429 5
            $this->initPathStack();
430 5
            $this->initContext();
431
432 5
            $this->parseErrors($data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 426 can also be of type array; however, Reva2\JsonApi\Decoders\DataParser::parseErrors() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and 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...
433
434 4
            $metadata = $this->factory->getMetadataFor($docType);
435 4
            if (!$metadata instanceof DocumentMetadataInterface) {
436 1
                throw new \InvalidArgumentException(sprintf("Failed to parse %s as JSON API document", $docType));
437
            }
438
439
            /* @var $metadata \Reva2\JsonApi\Contracts\Decoders\Mapping\DocumentMetadataInterface */
440
441 3
            $docClass = $metadata->getClassName();
442 3
            $doc = new $docClass();
443
444 3
            $this->parseLinkedResources($data);
445
446 3
            $this->parseProperty($data, $doc, $metadata->getContentMetadata());
447
448
            $docMetadata = $metadata->getMetadata();
449
            if ($docMetadata !== null) {
450
                $this->parseProperty($data, $doc, $metadata->getMetadata());
451
            }
452
453
            return $doc;
454 5
        } catch (JsonApiException $e) {
455 1
            throw $e;
456 4
        } catch (\Exception $e) {
457 4
            throw  $this->convertToApiException($e, 'document');
458
        }
459
    }
460
461
    /**
462
     * @inheritdoc
463
     */
464 3
    public function parseQueryParams($data, $paramsType)
465
    {
466
        try {
467 3
            $this->initPathStack();
468 3
            $this->initContext();
469
470 3
            $query = $this->parseObjectValue($data, $paramsType);
471 3
            if (!$query instanceof EncodingParametersInterface) {
472
                throw new \InvalidArgumentException(sprintf(
473
                    "Query parameters object must implement interface %s",
474
                    EncodingParametersInterface::class
475
                ));
476
            }
477
478 3
            return $query;
479
        } catch (JsonApiException $e) {
480
            throw $e;
481
        } catch (\Exception $e) {
482
            throw  $this->convertToApiException($e, 'query');
483
        }
484
    }
485
486
    /**
487
     * Prepare path segment
488
     *
489
     * @param string $path
490
     * @return string
491
     */
492 16
    protected function preparePathSegment($path)
493
    {
494 16
        return trim(preg_replace('~[\/]+~si', '/', str_replace(['.', '[', ']'], '/', (string) $path)), '/');
495
    }
496
497
    /**
498
     * Initialize stack that store current path
499
     */
500 21
    protected function initPathStack()
501
    {
502 21
        $this->path = new \SplStack();
503 21
    }
504
505
    /**
506
     * Initialize decoder context
507
     */
508 21
    protected function initContext()
509
    {
510 21
        $this->context = new Context();
511 21
    }
512
513
    /**
514
     * Parse numeric value
515
     *
516
     * @param mixed $data
517
     * @param string $path
518
     * @param string $type
519
     * @return float|int|null
520
     */
521 4
    protected function parseNumeric($data, $path, $type)
522
    {
523 4
        $this->setPath($path);
524
525 4
        $pathValue = null;
526 4
        if ($this->hasValue($data, $path)) {
527 4
            $value = $this->getValue($data, $path);
528 4
            $rightType = ('int' === $type) ? is_int($value) : is_float($value);
529 4
            if ($rightType) {
530 3
                $pathValue = $value;
531 4
            } elseif (is_numeric($value)) {
532 4
                $pathValue = ('int' === $type) ? (int) $value : (float) $value;
533 2 View Code Duplication
            } elseif (null !== $value) {
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...
534 2
                throw new \InvalidArgumentException(
535 2
                    sprintf("Value expected to be %s, but %s given", $type, gettype($value)),
536 2
                    400
537
                );
538
            }
539
        }
540
541 4
        $this->restorePath();
542
543 4
        return $pathValue;
544
    }
545
546
    /**
547
     * Parse property of specified object
548
     *
549
     * @param object|array $data
550
     * @param object $obj
551
     * @param PropertyMetadataInterface $metadata
552
     * @param string|null $path
553
     */
554 8
    private function parseProperty($data, $obj, PropertyMetadataInterface $metadata, $path = null)
555
    {
556 8
        if (null === $path) {
557 8
            $path = $metadata->getDataPath();
558
        }
559
560 8
        if ((false === $this->hasValue($data, $path)) ||
561 8
            (true === $this->isExcludedProperty($metadata))
562
        ) {
563 7
            return;
564
        }
565
566 8
        if ('custom' === $metadata->getDataType()) {
567 4
            $value = $this->parseCallback($data, $path, [$obj, $metadata->getDataTypeParams()]);
568
        } else {
569 7
            $value = $this->parsePropertyValue($data, $path, $metadata);
570
        }
571
572 8
        if (null !== ($converter = $metadata->getConverter())) {
573 3
            $callback = $this->callbackResolver->resolveCallback($converter);
574
575 3
            $value = call_user_func($callback, $value);
576
        }
577
578 8
        $this->setProperty($obj, $value, $metadata);
579 8
    }
580
581
    /**
582
     * Parse value of specified property
583
     *
584
     * @param object|array $data
585
     * @param string $path
586
     * @param PropertyMetadataInterface $metadata
587
     * @return mixed|null
588
     */
589 7
    private function parsePropertyValue($data, $path, PropertyMetadataInterface $metadata)
590
    {
591 7
        switch ($metadata->getDataType()) {
592 7
            case 'scalar':
593 7
                return $this->parseScalarValue($data, $path, $metadata->getDataTypeParams());
594
595 5
            case 'datetime':
596 1
                $format = $metadata->getDataTypeParams();
597 1
                if (empty($format)) {
598 1
                    $format = 'Y-m-d';
599
                }
600
601 1
                return $this->parseDateTime($data, $path, $format);
602
603 5
            case 'array':
604 3
                return $this->parseArrayValue($data, $path, $metadata->getDataTypeParams(), $metadata);
605
606 5
            case 'object':
607 5
                return $this->parseResourceOrObject($data, $path, $metadata->getDataTypeParams(), $metadata);
608
609 1
            case 'raw':
610 1
                return $this->parseRaw($data, $path);
611
612
            default:
613
                throw new \InvalidArgumentException(sprintf(
614
                    "Unsupported property data type '%s'",
615
                    $metadata->getDataType()
616
                ));
617
        }
618
    }
619
620
    /**
621
     * Parse value as JSON API resource or object
622
     *
623
     * @param object|array $data
624
     * @param string $path
625
     * @param string $objClass
626
     * @param PropertyMetadataInterface $propMetadata
627
     * @return mixed|null
628
     */
629 5
    public function parseResourceOrObject($data, $path, $objClass, PropertyMetadataInterface $propMetadata)
630
    {
631 5
        $objMetadata = $this->factory->getMetadataFor($objClass);
632 5
        if ($objMetadata instanceof ResourceMetadataInterface) {
633 4
            $loader = null;
634 4
            foreach ($propMetadata->getLoaders() as $group => $groupLoader) {
635
                if (in_array($group, $this->serializationGroups)) {
636
                    $loader = $groupLoader;
637
                }
638
            }
639
640 4
            return $this->parseResource($data, $path, $objClass, $loader);
641
        }
642
643 1
        return $this->parseObject($data, $path, $objClass);
644
    }
645
646
    /**
647
     * Parse value that contains JSON API object
648
     *
649
     * @param object|array $data
650
     * @param string $objType
651
     * @return mixed
652
     */
653 4
    public function parseObjectValue($data, $objType)
654
    {
655 4
        $metadata = $this->factory->getMetadataFor($objType);
656 4
        if (!$metadata instanceof ObjectMetadataInterface) {
657
            throw new \InvalidArgumentException('Invalid object metadata');
658
        }
659
660 4
        $discClass = $this->getClassByDiscriminator($metadata, $data);
661 4
        if ((null !== $discClass) && ($discClass !== $objType)) {
662 1
            return $this->parseObjectValue($data, $discClass);
663
        }
664
665 4
        $objClass = $metadata->getClassName();
666 4
        $obj = new $objClass();
667
668 4
        $properties = $metadata->getProperties();
669 4
        foreach ($properties as $property) {
670 4
            $this->parseProperty($data, $obj, $property);
671
        }
672
673 4
        return $obj;
674
    }
675
676
    /**
677
     * Parse value that contains array
678
     *
679
     * @param object|array $data
680
     * @param string $path
681
     * @param array $params
682
     * @param PropertyMetadataInterface $propMetadata
683
     * @return array|null
684
     */
685 3
    public function parseArrayValue($data, $path, array $params, PropertyMetadataInterface $propMetadata)
686
    {
687 3
        $type = $params[0];
688 3
        $typeParams = $params[1];
689
690
        switch ($type) {
691 3
            case 'scalar':
692 1
                return $this->parseArray(
693 1
                    $data,
694 1
                    $path,
695 1
                    function ($data, $path, DataParser $parser) use ($typeParams) {
696 1
                        return $parser->parseScalarValue($data, $path, $typeParams);
697 1
                    }
698
                );
699
700 3
            case 'datetime':
701 1
                $format = (!empty($typeParams)) ? $typeParams : 'Y-m-d';
702 1
                return $this->parseArray(
703 1
                    $data,
704 1
                    $path,
705 1
                    function ($data, $path, DataParser $parser) use ($format) {
706 1
                        return $parser->parseDateTime($data, $path, $format);
707 1
                    }
708
                );
709
710 3
            case 'object':
711 3
                return $this->parseArray(
712 3
                    $data,
713 3
                    $path,
714 3
                    function ($data, $path, DataParser $parser) use ($typeParams, $propMetadata) {
715 3
                        return $parser->parseResourceOrObject($data, $path, $typeParams, $propMetadata);
716 3
                    }
717
                );
718
719 1
            case 'array':
720 1
                return $this->parseArray(
721 1
                    $data,
722 1
                    $path,
723 1
                    function ($data, $path, DataParser $parser) use ($typeParams, $propMetadata) {
724 1
                        return $parser->parseArrayValue($data, $path, $typeParams, $propMetadata);
725 1
                    }
726
                );
727
728 1
            case 'raw':
729 1
                return $this->parseArray(
730 1
                    $data,
731 1
                    $path,
732 1
                    function ($data, $path, DataParser $parser) {
733 1
                        return $parser->parseRaw($data, $path);
734 1
                    }
735
                );
736
737
            default:
738
                throw new \InvalidArgumentException(sprintf(
739
                    "Unsupported array item type '%s' specified",
740
                    $type
741
                ));
742
        }
743
    }
744
745
    /**
746
     * Parse scalar value
747
     *
748
     * @param object|array $data
749
     * @param string $path
750
     * @param string $type
751
     * @return bool|float|int|null|string
752
     */
753 7
    public function parseScalarValue($data, $path, $type)
754
    {
755
        switch ($type) {
756 7
            case 'string':
757 6
                return $this->parseString($data, $path);
758
759 2
            case 'bool':
760 2
            case 'boolean':
761 1
                return $this->parseBool($data, $path);
762
763 2
            case 'int':
764 2
            case 'integer':
765 2
                return $this->parseInt($data, $path);
766
767 1
            case 'float':
768 1
            case 'double':
769 1
                return $this->parseFloat($data, $path);
770
771
            default:
772
                throw new \InvalidArgumentException(sprintf("Unsupported scalar type '%s' specified", $type));
773
        }
774
    }
775
776
    /**
777
     * Convert any exception to JSON API exception
778
     *
779
     * @param \Exception $e
780
     * @param string $objType
781
     * @return JsonApiException
782
     */
783 4
    private function convertToApiException(\Exception $e, $objType)
784
    {
785 4
        $status = $e->getCode();
786 4
        $message = 'Failed to parse request';
787 4
        if (empty($status)) {
788 1
            $message = 'Internal server error';
789 1
            $status = 500;
790
        }
791
792 4
        $source = null;
793
        switch ($objType) {
794 4
            case 'document':
795 4
                $source = ['pointer' => $this->getPath()];
796 4
                break;
797
798
            case 'query':
799
                $source = ['parameter' => $this->getPath()];
800
                break;
801
        }
802
803 4
        $error = new Error(rand(), null, $status, self::ERROR_CODE, $message, $e->getMessage(), $source);
804
805 4
        return new JsonApiException($error, $status, $e);
806
    }
807
808
    /**
809
     * Returns appropriate discriminator class for specified data
810
     *
811
     * @param ClassMetadataInterface $metadata
812
     * @param array|object $data
813
     * @return string|null
814
     */
815 9
    private function getClassByDiscriminator(ClassMetadataInterface $metadata, $data)
816
    {
817 9
        if (null === ($discField = $metadata->getDiscriminatorField())) {
818 7
            return null;
819
        }
820
821 6
        $discValue = $this->parseString($data, $discField->getDataPath());
822 6
        if (empty($discValue)) {
823 1
            $this->setPath($discField->getDataPath());
824
825 1
            throw new \InvalidArgumentException("Field value required and can not be empty", 422);
826
        }
827
828 5
        return $metadata->getDiscriminatorClass($discValue);
829
    }
830
831
    /**
832
     * Check if specified property should be excluded
833
     *
834
     * @param PropertyMetadataInterface $metadata
835
     * @return bool
836
     */
837 8
    private function isExcludedProperty(PropertyMetadataInterface $metadata)
838
    {
839 8
        $propertyGroups = $metadata->getGroups();
840 8
        foreach ($propertyGroups as $group) {
841 8
            if (in_array($group, $this->serializationGroups)) {
842 8
                return false;
843
            }
844
        }
845
846
        return true;
847
    }
848
849
    /**
850
     *
851
     * @param mixed $data
852
     */
853 3
    private function parseLinkedResources($data)
854
    {
855 3
        if (false === $this->hasValue($data, 'included')) {
856 2
            return;
857
        }
858
859 1
        $linkedData = $this->getValue($data, 'included');
860 1
        if (!is_array($linkedData)) {
861
            return;
862
        }
863
864 1
        foreach ($linkedData as $idx => $resData) {
865 1
            $id = null;
866 1
            if ($this->hasValue($resData, 'id')) {
867 1
                $id = $this->getValue($resData, 'id');
868
            }
869
870 1
            $type = null;
871 1
            if ($this->hasValue($resData, 'type')) {
872 1
                $type = $this->getValue($resData, 'type');
873
            }
874
875 1
            if (empty($id) || empty($type) || !is_string($id) || !is_string($type)) {
876
                continue;
877
            }
878
879 1
            $this->context->addLinkedData($type, $id, $idx, $resData);
880
        }
881 1
    }
882
883
    /**
884
     * Parse specified relationship data
885
     *
886
     * @param mixed $data
887
     * @param mixed $pathValue
888
     * @param PropertyMetadataInterface $relationship
889
     * @return void
890
     */
891 5
    private function parseRelationship($data, $pathValue, PropertyMetadataInterface $relationship)
892
    {
893 5
        if ('array' === $relationship->getDataType()) {
894 3
            return $this->parseArrayRelationship($data, $pathValue, $relationship);
895
        }
896
897 5
        $resType = null;
898 5
        if ($this->hasValue($data, $relationship->getDataPath() . '.type')) {
899 5
            $resType = $this->parseString($data, $relationship->getDataPath() . '.type');
900
        }
901
902 5
        $resId = null;
903 5
        if ($this->hasValue($data, $relationship->getDataPath() . '.id')) {
904 5
            $resId = $this->getValue($data, $relationship->getDataPath() . '.id');
905
        }
906
907 5 View Code Duplication
        if ((null !== $resId) &&
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...
908 5
            (null !== $resType) &&
909 5
            (null !== ($res = $this->context->getResource($resType, $resId)))
910
        ) {
911 1
            return $this->setProperty($pathValue, $res, $relationship);
912
        }
913
914
915 5 View Code Duplication
        if (null !== ($linkedData = $this->context->getLinkedData($resType, $resId))) {
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...
916 1
            $idx = $this->context->getLinkedDataIndex($resType, $resId);
917 1
            $prevPath = $this->path;
918
919 1
            $this->initPathStack();
920 1
            $this->setPath('included')->setPath($idx);
921
922 1
            $this->parseProperty([$idx => $linkedData], $pathValue, $relationship, '[' . $idx . ']');
923 1
            $this->path = $prevPath;
924
925 1
            return;
926
        }
927
928 4
        $this->parseProperty($data, $pathValue, $relationship);
929 3
    }
930
931
    /**
932
     * Parse data for relationship that contains array of resources
933
     *
934
     * @param mixed $data
935
     * @param mixed $pathValue
936
     * @param PropertyMetadataInterface $relationship
937
     */
938
    private function parseArrayRelationship($data, $pathValue, PropertyMetadataInterface $relationship)
939
    {
940 3
        $data = $this->parseArray($data, $relationship->getDataPath(), function ($data, $path) use ($relationship) {
941 1
            $resType = null;
942 1
            if ($this->hasValue($data, $path . '.type')) {
943 1
                $resType = $this->parseString($data, $path . '.type');
944
            }
945
946 1
            $resId = null;
947 1
            if ($this->hasValue($data, $path .'.id')) {
948 1
                $resId = $this->getValue($data, $path . '.id');
949
            }
950
951 1 View Code Duplication
            if ((null !== $resType) &&
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...
952 1
                (null !== $resId) &&
953 1
                (null !== ($parsed = $this->context->getResource($resType, $resId)))
954
            ) {
955
                return $parsed;
956
            }
957
958 1
            $params = $relationship->getDataTypeParams();
959
960 1 View Code Duplication
            if (null !== ($linkedData = $this->context->getLinkedData($resType, $resId))) {
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...
961 1
                $idx = $this->context->getLinkedDataIndex($resType, $resId);
962
963 1
                $prevPath = $this->path;
964 1
                $this->initPathStack();
965 1
                $this->setPath('included')->setPath($idx);
966
967 1
                $parsed = $this->parseResourceOrObject(
968 1
                    [$idx => $linkedData],
969 1
                    '[' . $idx .']',
970 1
                    $params[1],
971 1
                    $relationship
972
                );
973
974 1
                $this->path = $prevPath;
975
976 1
                return $parsed;
977
            }
978
979
            return $this->parseResourceOrObject($data, $path, $params[1], $relationship);
980 3
        });
981
982 3
        if (is_array(data)) {
983
            $this->setProperty($pathValue, $data, $relationship);
984
        }
985
    }
986
987
    /**
988
     * Sets property value using metadata
989
     *
990
     * @param mixed $obj
991
     * @param mixed $value
992
     * @param PropertyMetadataInterface $metadata
993
     */
994 8
    private function setProperty($obj, $value, PropertyMetadataInterface $metadata)
995
    {
996 8
        $setter = $metadata->getSetter();
997 8
        if (null !== $setter) {
998 6
            $obj->{$setter}($value);
999
        } else {
1000 6
            $setter = $metadata->getPropertyName();
1001 6
            $obj->{$setter} = $value;
1002
        }
1003 8
    }
1004
1005
    /**
1006
     * Parse errors from JSON API document
1007
     *
1008
     * @param object $data
1009
     */
1010 5
    private function parseErrors($data)
1011
    {
1012 5
        if (!$this->hasValue($data, 'errors')) {
1013 4
            return;
1014
        }
1015
1016 1
        $errors = $this->parseArray($data, 'errors', function ($data, $path, DataParser $parser) {
0 ignored issues
show
Unused Code introduced by
The parameter $parser is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1017 1
            $source = null;
1018 1
            if ($this->hasValue($data, $path . '.source.pointer')) {
1019 1
                $source = ['pointer' => $this->parseString($data, $path . '.source.pointer')];
1020 1
            } elseif ($this->hasValue($data, $path . '.source.parameter')) {
1021 1
                $source = ['parameter' => $this->parseString($data, $path . '.source.parameter')];
1022
            }
1023
1024 1
            return new Error(
1025 1
                $this->parseString($data, $path . '.id'),
1026 1
                null,
1027 1
                $this->parseString($data, $path . '.status'),
1028 1
                $this->parseString($data, $path . '.code'),
1029 1
                $this->parseString($data, $path . '.title'),
1030 1
                $this->parseString($data, $path . '.detail'),
1031 1
                $source,
1032 1
                $this->parseRaw($data, $path . '.meta')
1033
            );
1034 1
        });
1035
1036
        throw new JsonApiException($errors);
0 ignored issues
show
Documentation introduced by
$errors is of type array|null, but the function expects a object<Neomerx\JsonApi\C...ptions\ErrorCollection>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1037
    }
1038
}
1039