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