Completed
Branch master (195c43)
by
unknown
04:51
created

Parser::getFactory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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