Completed
Branch next (29fadd)
by Neomerx
03:15
created

Parser::parseResource()   B

Complexity

Conditions 11
Paths 14

Size

Total Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 11

Importance

Changes 0
Metric Value
cc 11
nc 14
nop 1
dl 0
loc 42
ccs 22
cts 22
cp 1
crap 11
rs 7.3166
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
3
namespace Neomerx\JsonApi\Parser;
4
5
/**
6
 * Copyright 2015-2019 [email protected]
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 * http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
use IteratorAggregate;
22
use Neomerx\JsonApi\Contracts\Factories\FactoryInterface;
23
use Neomerx\JsonApi\Contracts\Parser\DocumentDataInterface;
24
use Neomerx\JsonApi\Contracts\Parser\IdentifierInterface;
25
use Neomerx\JsonApi\Contracts\Parser\ParserInterface;
26
use Neomerx\JsonApi\Contracts\Parser\PositionInterface;
27
use Neomerx\JsonApi\Contracts\Parser\RelationshipInterface;
28
use Neomerx\JsonApi\Contracts\Parser\ResourceInterface;
29
use Neomerx\JsonApi\Contracts\Schema\DocumentInterface;
30
use Neomerx\JsonApi\Contracts\Schema\IdentifierInterface as SchemaIdentifierInterface;
31
use Neomerx\JsonApi\Contracts\Schema\SchemaContainerInterface;
32
use Neomerx\JsonApi\Exceptions\InvalidArgumentException;
33
use Traversable;
34
use function Neomerx\JsonApi\I18n\format as _;
35
36
/**
37
 * @package Neomerx\JsonApi
38
 */
39
class Parser implements ParserInterface
40
{
41
    /** @var string */
42
    public const MSG_NO_SCHEMA_FOUND = 'No Schema found for top-level resource `%s`.';
43
44
    /** @var string */
45
    public const MSG_NO_DATA_IN_RELATIONSHIP =
46
        'For resource of type `%s` with ID `%s` relationship `%s` cannot be parsed because it has no data. Skipping.';
47
48
    /** @var string */
49
    public const MSG_CAN_NOT_PARSE_RELATIONSHIP =
50
        'For resource of type `%s` with ID `%s` relationship `%s` cannot be parsed because it either ' .
51
        'has `null` or identifier as data. Skipping.';
52
53
    /**
54
     * @var SchemaContainerInterface
55
     */
56
    private $schemaContainer;
57
58
    /**
59
     * @var FactoryInterface
60
     */
61
    private $factory;
62
63
    /**
64
     * @var array
65
     */
66
    private $paths;
67
68
    /**
69
     * @var array
70
     */
71
    private $resourcesTracker;
72
73
    /**
74
     * @param FactoryInterface         $factory
75
     * @param SchemaContainerInterface $container
76
     */
77 67
    public function __construct(FactoryInterface $factory, SchemaContainerInterface $container)
78
    {
79 67
        $this->resourcesTracker = [];
80
81 67
        $this->setFactory($factory)->setSchemaContainer($container);
82 67
    }
83
84
    /**
85
     * @inheritdoc
86
     *
87
     * @SuppressWarnings(PHPMD.ElseExpression)
88
     */
89 67
    public function parse($data, array $paths = []): iterable
90
    {
91 67
        assert(is_array($data) === true || is_object($data) === true || $data === null);
92
93 67
        $this->paths = $this->normalizePaths($paths);
0 ignored issues
show
Documentation introduced by
$paths is of type array, but the function expects a object<Neomerx\JsonApi\Parser\iterable>.

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...
94
95 67
        $rootPosition = $this->getFactory()->createPosition(
96 67
            ParserInterface::ROOT_LEVEL,
97 67
            ParserInterface::ROOT_PATH,
98 67
            null,
99 67
            null
100
        );
101
102 67
        if ($this->getSchemaContainer()->hasSchema($data) === true) {
103 44
            yield $this->createDocumentDataIsResource($rootPosition);
104 44
            yield from $this->parseAsResource($rootPosition, $data);
105 24
        } elseif ($data instanceof SchemaIdentifierInterface) {
106
            yield $this->createDocumentDataIsIdentifier($rootPosition);
107
            yield $this->parseAsIdentifier($rootPosition, $data);
108 24
        } elseif (is_array($data) === true) {
109 18
            yield $this->createDocumentDataIsCollection($rootPosition);
110 18
            yield from $this->parseAsResourcesOrIdentifiers($rootPosition, $data);
0 ignored issues
show
Documentation introduced by
$data is of type array, but the function expects a object<Neomerx\JsonApi\Parser\iterable>.

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...
111 6
        } elseif ($data instanceof Traversable) {
112 3
            $data = $data instanceof IteratorAggregate ? $data->getIterator() : $data;
113 3
            yield $this->createDocumentDataIsCollection($rootPosition);
114 3
            yield from $this->parseAsResourcesOrIdentifiers($rootPosition, $data);
0 ignored issues
show
Documentation introduced by
$data is of type object<Traversable>, but the function expects a object<Neomerx\JsonApi\Parser\iterable>.

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...
115 3
        } elseif ($data === null) {
116 2
            yield $this->createDocumentDataIsNull($rootPosition);
117
        } else {
118 1
            throw new InvalidArgumentException(_(static::MSG_NO_SCHEMA_FOUND, get_class($data)));
119
        }
120 65
    }
121
122
    /**
123
     * @return SchemaContainerInterface
124
     */
125 67
    protected function getSchemaContainer(): SchemaContainerInterface
126
    {
127 67
        return $this->schemaContainer;
128
    }
129
130
    /**
131
     * @param SchemaContainerInterface $container
132
     *
133
     * @return self
134
     */
135 67
    protected function setSchemaContainer(SchemaContainerInterface $container): self
136
    {
137 67
        $this->schemaContainer = $container;
138
139 67
        return $this;
140
    }
141
142
    /**
143
     * @return FactoryInterface
144
     */
145 67
    protected function getFactory(): FactoryInterface
146
    {
147 67
        return $this->factory;
148
    }
149
150
    /**
151
     * @param FactoryInterface $factory
152
     *
153
     * @return self
154
     */
155 67
    protected function setFactory(FactoryInterface $factory): self
156
    {
157 67
        $this->factory = $factory;
158
159 67
        return $this;
160
    }
161
162
    /**
163
     * @param ResourceInterface $resource
164
     *
165
     * @return void
166
     */
167 59
    private function rememberResource(ResourceInterface $resource): void
168
    {
169 59
        $this->resourcesTracker[$resource->getId()][$resource->getType()] = true;
170 59
    }
171
172
    /**
173
     * @param ResourceInterface $resource
174
     *
175
     * @return bool
176
     */
177 59
    private function hasSeenResourceBefore(ResourceInterface $resource): bool
178
    {
179 59
        return isset($this->resourcesTracker[$resource->getId()][$resource->getType()]);
180
    }
181
182
    /**
183
     * @param PositionInterface $position
184
     * @param iterable          $dataOrIds
185
     *
186
     * @see ResourceInterface
187
     * @see IdentifierInterface
188
     *
189
     * @return iterable
190
     */
191 21
    private function parseAsResourcesOrIdentifiers(
192
        PositionInterface $position,
193
        iterable $dataOrIds
194
    ): iterable {
195 21
        foreach ($dataOrIds as $dataOrId) {
196 16
            if ($this->getSchemaContainer()->hasSchema($dataOrId) === true) {
197 16
                yield from $this->parseAsResource($position, $dataOrId);
198
199 16
                continue;
200
            }
201
202
            assert($dataOrId instanceof SchemaIdentifierInterface);
203
            yield $this->parseAsIdentifier($position, $dataOrId);
204
        }
205 21
    }
206
207
    /**
208
     * @param PositionInterface $position
209
     * @param mixed             $data
210
     *
211
     * @see ResourceInterface
212
     *
213
     * @return iterable
214
     *
215
     */
216 59
    private function parseAsResource(
217
        PositionInterface $position,
218
        $data
219
    ): iterable {
220 59
        assert($this->getSchemaContainer()->hasSchema($data) === true);
221
222 59
        $resource = $this->getFactory()->createParsedResource(
223 59
            $position,
224 59
            $this->getSchemaContainer(),
225 59
            $data
226
        );
227
228 59
        yield from $this->parseResource($resource);
229 58
    }
230
231
    /**
232
     * @param ResourceInterface $resource
233
     *
234
     * @return iterable
235
     */
236 59
    private function parseResource(ResourceInterface $resource): iterable
237
    {
238 59
        $seenBefore = $this->hasSeenResourceBefore($resource);
239
240
        // top level resources should be yielded in any case as it could be an array of the resources
241
        // for deeper levels it's not needed as they go to `included` section and it must have no more
242
        // than one instance of the same resource.
243
244 59
        if ($resource->getPosition()->getLevel() <= ParserInterface::ROOT_LEVEL || $seenBefore === false) {
245 59
            yield $resource;
246
        }
247
248
        // parse relationships only for resources not seen before (prevents infinite loop for circular references)
249 59
        if ($seenBefore === false) {
250 59
            $this->rememberResource($resource);
251
252 59
            foreach ($resource->getRelationships() as $name => $relationship) {
253 44
                assert(is_string($name));
254 44
                assert($relationship instanceof RelationshipInterface);
255
256 44
                $isShouldParse = $this->isPathRequested($relationship->getPosition()->getPath());
257
258 44
                if ($relationship->hasData() === true && $isShouldParse === true) {
259 22
                    $relData = $relationship->getData();
260 22
                    if ($relData->isResource() === true) {
261 13
                        yield from $this->parseResource($relData->getResource());
262
263 13
                        continue;
264 20
                    } elseif ($relData->isCollection() === true) {
265 19
                        foreach ($relData->getResources() as $relResource) {
266 19
                            assert($relResource instanceof ResourceInterface);
267 19
                            yield from $this->parseResource($relResource);
268
                        }
269
270 18
                        continue;
271
                    }
272
273 44
                    assert($relData->isNull() || $relData->isIdentifier());
274
                }
275
            }
276
        }
277 58
    }
278
279
    /**
280
     * @param PositionInterface         $position
281
     * @param SchemaIdentifierInterface $identifier
282
     *
283
     * @return IdentifierInterface
284
     */
285
    private function parseAsIdentifier(
286
        PositionInterface $position,
287
        SchemaIdentifierInterface $identifier
288
    ): IdentifierInterface {
289
        return new class ($position, $identifier) implements IdentifierInterface
290
        {
291
            /**
292
             * @var PositionInterface
293
             */
294
            private $position;
295
296
            /**
297
             * @var SchemaIdentifierInterface
298
             */
299
            private $identifier;
300
301
            /**
302
             * @param PositionInterface         $position
303
             * @param SchemaIdentifierInterface $identifier
304
             */
305
            public function __construct(PositionInterface $position, SchemaIdentifierInterface $identifier)
306
            {
307
                $this->position   = $position;
308
                $this->identifier = $identifier;
309
            }
310
311
            /**
312
             * @inheritdoc
313
             */
314
            public function getPosition(): PositionInterface
315
            {
316
                return $this->position;
317
            }
318
319
            /**
320
             * @inheritdoc
321
             */
322
            public function getId(): ?string
323
            {
324
                return $this->getIdentifier()->getId();
325
            }
326
327
            /**
328
             * @inheritdoc
329
             */
330
            public function getType(): string
331
            {
332
                return $this->getIdentifier()->getType();
333
            }
334
335
            /**
336
             * @inheritdoc
337
             */
338
            public function hasIdentifierMeta(): bool
339
            {
340
                return $this->getIdentifier()->hasIdentifierMeta();
341
            }
342
343
            /**
344
             * @inheritdoc
345
             */
346
            public function getIdentifierMeta()
347
            {
348
                return $this->getIdentifier()->getIdentifierMeta();
349
            }
350
351
            /**
352
             * @return SchemaIdentifierInterface
353
             */
354
            private function getIdentifier(): SchemaIdentifierInterface
355
            {
356
                return $this->identifier;
357
            }
358
        };
359
    }
360
361
    /**
362
     * @param PositionInterface $position
363
     *
364
     * @return DocumentDataInterface
365
     */
366 21
    private function createDocumentDataIsCollection(PositionInterface $position): DocumentDataInterface
367
    {
368 21
        return $this->createParsedDocumentData($position, true, false, false, false);
369
    }
370
371
    /**
372
     * @param PositionInterface $position
373
     *
374
     * @return DocumentDataInterface
375
     */
376 2
    private function createDocumentDataIsNull(PositionInterface $position): DocumentDataInterface
377
    {
378 2
        return $this->createParsedDocumentData($position, false, true, false, false);
379
    }
380
381
    /**
382
     * @param PositionInterface $position
383
     *
384
     * @return DocumentDataInterface
385
     */
386 44
    private function createDocumentDataIsResource(PositionInterface $position): DocumentDataInterface
387
    {
388 44
        return $this->createParsedDocumentData($position, false, false, true, false);
389
    }
390
391
    /**
392
     * @param PositionInterface $position
393
     *
394
     * @return DocumentDataInterface
395
     */
396
    private function createDocumentDataIsIdentifier(PositionInterface $position): DocumentDataInterface
397
    {
398
        return $this->createParsedDocumentData($position, false, false, false, true);
399
    }
400
401
    /**
402
     * @param PositionInterface $position
403
     * @param bool              $isCollection
404
     * @param bool              $isNull
405
     * @param bool              $isResource
406
     * @param bool              $isIdentifier
407
     *
408
     * @return DocumentDataInterface
409
     */
410
    private function createParsedDocumentData(
411
        PositionInterface $position,
412
        bool $isCollection,
413
        bool $isNull,
414
        bool $isResource,
415
        bool $isIdentifier
416
    ): DocumentDataInterface {
417
        return new class (
418
            $position,
419
            $isCollection,
420
            $isNull,
421
            $isResource,
422
            $isIdentifier
423
        ) implements DocumentDataInterface
424
        {
425
            /**
426
             * @var PositionInterface
427
             */
428
            private $position;
429
            /**
430
             * @var bool
431
             */
432
            private $isCollection;
433
434
            /**
435
             * @var bool
436
             */
437
            private $isNull;
438
439
            /**
440
             * @var bool
441
             */
442
            private $isResource;
443
444
            /**
445
             * @var bool
446
             */
447
            private $isIdentifier;
448
449
            /**
450
             * @param PositionInterface $position
451
             * @param bool              $isCollection
452
             * @param bool              $isNull
453
             * @param bool              $isResource
454
             * @param bool              $isIdentifier
455
             */
456 66
            public function __construct(
457
                PositionInterface $position,
458
                bool $isCollection,
459
                bool $isNull,
460
                bool $isResource,
461
                bool $isIdentifier
462
            ) {
463 66
                $this->position     = $position;
464 66
                $this->isCollection = $isCollection;
465 66
                $this->isNull       = $isNull;
466 66
                $this->isResource   = $isResource;
467 66
                $this->isIdentifier = $isIdentifier;
468 66
            }
469
470
            /**
471
             * @inheritdoc
472
             */
473
            public function getPosition(): PositionInterface
474
            {
475
                return $this->position;
476
            }
477
478
            /**
479
             * @inheritdoc
480
             */
481 66
            public function isCollection(): bool
482
            {
483 66
                return $this->isCollection;
484
            }
485
486
            /**
487
             * @inheritdoc
488
             */
489 46
            public function isNull(): bool
490
            {
491 46
                return $this->isNull;
492
            }
493
494
            /**
495
             * @inheritdoc
496
             */
497
            public function isResource(): bool
498
            {
499
                return $this->isResource;
500
            }
501
502
            /**
503
             * @inheritdoc
504
             */
505
            public function isIdentifier(): bool
506
            {
507
                return $this->isIdentifier;
508
            }
509
        };
510
    }
511
512
    /**
513
     * @param string $path
514
     *
515
     * @return bool
516
     */
517 44
    private function isPathRequested(string $path): bool
518
    {
519 44
        return array_key_exists($path, $this->paths);
520
    }
521
522
    /**
523
     * @param iterable $paths
524
     *
525
     * @return array
526
     */
527 67
    private function normalizePaths(iterable $paths): array
528
    {
529 67
        $separator = DocumentInterface::PATH_SEPARATOR;
530
531
        // convert paths like a.b.c to paths that actually should be used a, a.b, a.b.c
532 67
        $normalizedPaths = [];
533 67
        foreach ($paths as $path) {
534 22
            $curPath = '';
535 22
            foreach (explode($separator, $path) as $pathPart) {
536 22
                $curPath                   = empty($curPath) === true ? $pathPart : $curPath . $separator . $pathPart;
537 22
                $normalizedPaths[$curPath] = true;
538
            }
539
        }
540
541 67
        return $normalizedPaths;
542
    }
543
}
544