Completed
Branch master (335615)
by
unknown
06:49
created

Parser::getNormalizedPaths()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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