Completed
Branch develop (5a7a06)
by Neomerx
04:07
created

Parser::getContext()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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