Completed
Branch master (1807c3)
by
unknown
01:52
created

Parser.php$0 ➔ hasIdentifierMeta()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 4
ccs 1
cts 1
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 74
    public function __construct(FactoryInterface $factory, SchemaContainerInterface $container)
84
    {
85 74
        $this->resourcesTracker = [];
86 74
        $this->factory          = $factory;
87 74
        $this->schemaContainer  = $container;
88 74
    }
89
90
    /**
91
     * @inheritdoc
92
     *
93
     * @SuppressWarnings(PHPMD.ElseExpression)
94
     */
95 74
    public function parse($data, array $paths = []): iterable
96
    {
97 74
        \assert(\is_array($data) === true || \is_object($data) === true || $data === null);
98
99 74
        $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 74
        $rootPosition = $this->factory->createPosition(
102 74
            ParserInterface::ROOT_LEVEL,
103 74
            ParserInterface::ROOT_PATH,
104 74
            null,
105 74
            null
106
        );
107
108 74
        if ($this->schemaContainer->hasSchema($data) === true) {
109 48
            yield $this->createDocumentDataIsResource($rootPosition);
110 48
            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 72
    }
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 46
    protected function isPathRequested(string $path): bool
169
    {
170 46
        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 63
    private function parseAsResource(
183
        PositionInterface $position,
184
        $data
185
    ): iterable {
186 63
        \assert($this->schemaContainer->hasSchema($data) === true);
187
188 63
        $resource = $this->factory->createParsedResource(
189 63
            $position,
190 63
            $this->schemaContainer,
191 63
            $data
192
        );
193
194 63
        yield from $this->parseResource($resource);
195 62
    }
196
197
    /**
198
     * @param ResourceInterface $resource
199
     *
200
     * @return iterable
201
     *
202
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
203
     */
204 63
    private function parseResource(ResourceInterface $resource): iterable
205
    {
206 63
        $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 63
        if ($seenBefore === false || $resource->getPosition()->getLevel() <= ParserInterface::ROOT_LEVEL) {
213 63
            yield $resource;
214
        }
215
216
        // parse relationships only for resources not seen before (prevents infinite loop for circular references)
217 63
        if ($seenBefore === false) {
218
            // remember by id and type
219 63
            $this->resourcesTracker[$resource->getId()][$resource->getType()] = true;
220
221 63
            foreach ($resource->getRelationships() as $name => $relationship) {
222 48
                \assert(\is_string($name));
223 48
                \assert($relationship instanceof RelationshipInterface);
224
225 48
                $isShouldParse = $this->isPathRequested($relationship->getPosition()->getPath());
226
227 48
                if ($isShouldParse === true && $relationship->hasData() === true) {
228 25
                    $relData = $relationship->getData();
229 25
                    if ($relData->isResource() === true) {
230 15
                        yield from $this->parseResource($relData->getResource());
231
232 15
                        continue;
233 23
                    } elseif ($relData->isCollection() === true) {
234 22
                        foreach ($relData->getResources() as $relResource) {
235 22
                            \assert($relResource instanceof ResourceInterface ||
236 22
                                $relResource instanceof IdentifierInterface);
237 22
                            if ($relResource instanceof ResourceInterface) {
238 22
                                yield from $this->parseResource($relResource);
239
                            }
240
                        }
241
242 21
                        continue;
243
                    }
244
245 47
                    \assert($relData->isNull() || $relData->isIdentifier());
246
                }
247
            }
248
        }
249 62
    }
250
251
    /**
252
     * @param PositionInterface         $position
253
     * @param SchemaIdentifierInterface $identifier
254
     *
255
     * @return IdentifierInterface
256
     */
257 2
    private function parseAsIdentifier(
258
        PositionInterface $position,
259
        SchemaIdentifierInterface $identifier
260
    ): IdentifierInterface {
261
        return new class ($position, $identifier) implements IdentifierInterface
262
        {
263
            /**
264
             * @var PositionInterface
265
             */
266
            private $position;
267
268
            /**
269
             * @var SchemaIdentifierInterface
270
             */
271
            private $identifier;
272
273
            /**
274
             * @param PositionInterface         $position
275
             * @param SchemaIdentifierInterface $identifier
276
             */
277
            public function __construct(PositionInterface $position, SchemaIdentifierInterface $identifier)
278
            {
279 2
                $this->position   = $position;
280 2
                $this->identifier = $identifier;
281 2
            }
282
283
            /**
284
             * @inheritdoc
285
             */
286
            public function getPosition(): PositionInterface
287
            {
288 2
                return $this->position;
289
            }
290
291
            /**
292
             * @inheritdoc
293
             */
294
            public function getId(): ?string
295
            {
296 2
                return $this->identifier->getId();
297
            }
298
299
            /**
300
             * @inheritdoc
301
             */
302
            public function getType(): string
303
            {
304 2
                return $this->identifier->getType();
305
            }
306
307
            /**
308
             * @inheritdoc
309
             */
310
            public function hasIdentifierMeta(): bool
311
            {
312 2
                return $this->identifier->hasIdentifierMeta();
313
            }
314
315
            /**
316
             * @inheritdoc
317
             */
318
            public function getIdentifierMeta()
319
            {
320 1
                return $this->identifier->getIdentifierMeta();
321
            }
322
        };
323
    }
324
325
    /**
326
     * @param PositionInterface $position
327
     *
328
     * @return DocumentDataInterface
329
     */
330 23
    private function createDocumentDataIsCollection(PositionInterface $position): DocumentDataInterface
331
    {
332 23
        return $this->createParsedDocumentData($position, true, false);
333
    }
334
335
    /**
336
     * @param PositionInterface $position
337
     *
338
     * @return DocumentDataInterface
339
     */
340 3
    private function createDocumentDataIsNull(PositionInterface $position): DocumentDataInterface
341
    {
342 3
        return $this->createParsedDocumentData($position, false, true);
343
    }
344
345
    /**
346
     * @param PositionInterface $position
347
     *
348
     * @return DocumentDataInterface
349
     */
350 48
    private function createDocumentDataIsResource(PositionInterface $position): DocumentDataInterface
351
    {
352 48
        return $this->createParsedDocumentData($position, false, false);
353
    }
354
355
    /**
356
     * @param PositionInterface $position
357
     *
358
     * @return DocumentDataInterface
359
     */
360 2
    private function createDocumentDataIsIdentifier(PositionInterface $position): DocumentDataInterface
361
    {
362 2
        return $this->createParsedDocumentData($position, false, false);
363
    }
364
365
    /**
366
     * @param PositionInterface $position
367
     * @param bool              $isCollection
368
     * @param bool              $isNull
369
     *
370
     * @return DocumentDataInterface
371
     */
372
    private function createParsedDocumentData(
373
        PositionInterface $position,
374
        bool $isCollection,
375
        bool $isNull
376
    ): DocumentDataInterface {
377
        return new class (
378
            $position,
379
            $isCollection,
380
            $isNull
381
        ) implements DocumentDataInterface
382
        {
383
            /**
384
             * @var PositionInterface
385
             */
386
            private $position;
387
            /**
388
             * @var bool
389
             */
390
            private $isCollection;
391
392
            /**
393
             * @var bool
394
             */
395
            private $isNull;
396
397
            /**
398
             * @param PositionInterface $position
399
             * @param bool              $isCollection
400
             * @param bool              $isNull
401
             */
402 73
            public function __construct(
403
                PositionInterface $position,
404
                bool $isCollection,
405
                bool $isNull
406
            ) {
407 73
                $this->position     = $position;
408 73
                $this->isCollection = $isCollection;
409 73
                $this->isNull       = $isNull;
410 73
            }
411
412
            /**
413
             * @inheritdoc
414
             */
415 73
            public function getPosition(): PositionInterface
416
            {
417 73
                return $this->position;
418
            }
419
420
            /**
421
             * @inheritdoc
422
             */
423 73
            public function isCollection(): bool
424
            {
425 73
                return $this->isCollection;
426
            }
427
428
            /**
429
             * @inheritdoc
430
             */
431 53
            public function isNull(): bool
432
            {
433 53
                return $this->isNull;
434
            }
435
        };
436
    }
437
438
    /**
439
     * @param iterable $paths
440
     *
441
     * @return array
442
     */
443 74
    private function normalizePaths(iterable $paths): array
444
    {
445 74
        $separator = DocumentInterface::PATH_SEPARATOR;
446
447
        // convert paths like a.b.c to paths that actually should be used a, a.b, a.b.c
448 74
        $normalizedPaths = [];
449 74
        foreach ($paths as $path) {
450 25
            $curPath = '';
451 25
            foreach (\explode($separator, $path) as $pathPart) {
452 25
                $curPath                   = empty($curPath) === true ? $pathPart : $curPath . $separator . $pathPart;
453 25
                $normalizedPaths[$curPath] = true;
454
            }
455
        }
456
457 74
        return $normalizedPaths;
458
    }
459
}
460