Completed
Branch next (e13c7e)
by Neomerx
01:45
created

IdentifierAndResource::parseRelationshipLinks()   B

Complexity

Conditions 11
Paths 96

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 14.0032

Importance

Changes 0
Metric Value
cc 11
nc 96
nop 2
dl 0
loc 35
ccs 17
cts 24
cp 0.7083
crap 14.0032
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\ParserInterface;
24
use Neomerx\JsonApi\Contracts\Parser\PositionInterface;
25
use Neomerx\JsonApi\Contracts\Parser\RelationshipDataInterface;
26
use Neomerx\JsonApi\Contracts\Parser\ResourceInterface;
27
use Neomerx\JsonApi\Contracts\Schema\IdentifierInterface;
28
use Neomerx\JsonApi\Contracts\Schema\LinkInterface;
29
use Neomerx\JsonApi\Contracts\Schema\SchemaContainerInterface;
30
use Neomerx\JsonApi\Contracts\Schema\SchemaInterface;
31
use Neomerx\JsonApi\Exceptions\InvalidArgumentException;
32
use Traversable;
33
use function Neomerx\JsonApi\I18n\format as _;
34
35
/**
36
 * @package Neomerx\JsonApi
37
 */
38
class IdentifierAndResource implements ResourceInterface
39
{
40
    /** @var string */
41
    public const MSG_NO_SCHEMA_FOUND = 'No Schema found for resource `%s` at path `%s`.';
42
43
    /** @var string */
44
    public const MSG_INVALID_OPERATION = 'Invalid operation.';
45
46
    /**
47
     * @var PositionInterface
48
     */
49
    private $position;
50
51
    /**
52
     * @var FactoryInterface
53
     */
54
    private $factory;
55
56
    /**
57
     * @var SchemaContainerInterface
58
     */
59
    private $schemaContainer;
60
61
    /**
62
     * @var SchemaInterface
63
     */
64
    private $schema;
65
66
    /**
67
     * @var mixed
68
     */
69
    private $data;
70
71
    /**
72
     * @var string
73
     */
74
    private $index;
75
76
    /**
77
     * @var string
78
     */
79
    private $type;
80
81
    /**
82
     * @var null|array
83
     */
84
    private $links = null;
85
86
    /**
87
     * @var null|array
88
     */
89
    private $relationshipsCache = null;
90
91
    /**
92
     * @param PositionInterface        $position
93
     * @param FactoryInterface         $factory
94
     * @param SchemaContainerInterface $container
95
     * @param mixed                    $data
96
     */
97 59
    public function __construct(
98
        PositionInterface $position,
99
        FactoryInterface $factory,
100
        SchemaContainerInterface $container,
101
        $data
102
    ) {
103 59
        $schema = $container->getSchema($data);
104
        $this
105 59
            ->setPosition($position)
106 59
            ->setFactory($factory)
107 59
            ->setSchemaContainer($container)
108 59
            ->setSchema($schema)
109 59
            ->setData($data);
110 59
    }
111
112
    /**
113
     * @inheritdoc
114
     */
115 59
    public function getPosition(): PositionInterface
116
    {
117 59
        return $this->position;
118
    }
119
120
    /**
121
     * @inheritdoc
122
     */
123 59
    public function getId(): ?string
124
    {
125 59
        return $this->index;
126
    }
127
128
    /**
129
     * @inheritdoc
130
     */
131 59
    public function getType(): string
132
    {
133 59
        return $this->type;
134
    }
135
136
    /**
137
     * @inheritdoc
138
     */
139 31
    public function hasIdentifierMeta(): bool
140
    {
141 31
        return $this->getSchema()->hasIdentifierMeta($this->getData());
142
    }
143
144
    /**
145
     * @inheritdoc
146
     */
147
    public function getIdentifierMeta()
148
    {
149
        return $this->getSchema()->getIdentifierMeta($this->getData());
150
    }
151
152
    /**
153
     * @inheritdoc
154
     */
155 56
    public function getAttributes(): iterable
156
    {
157 56
        return $this->getSchema()->getAttributes($this->getData());
158
    }
159
160
    /**
161
     * @inheritdoc
162
     */
163 59
    public function getRelationships(): iterable
164
    {
165 59
        if ($this->relationshipsCache !== null) {
166 56
            yield from $this->relationshipsCache;
167
168 55
            return;
169
        }
170
171 59
        $this->relationshipsCache = [];
172
173 59
        $currentPath    = $this->getPosition()->getPath();
174 59
        $nextLevel      = $this->getPosition()->getLevel() + 1;
175 59
        $nextPathPrefix = empty($currentPath) === true ? '' : $currentPath . PositionInterface::PATH_SEPARATOR;
176 59
        foreach ($this->getSchema()->getRelationships($this->getData()) as $name => $description) {
177 44
            assert($this->assertRelationshipNameAndDescription($name, $description) === true);
178
179
            [$hasData, $relationshipData, $nextPosition] =
0 ignored issues
show
Bug introduced by
The variable $hasData does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $relationshipData does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $nextPosition does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
180 44
                $this->parseRelationshipData($name, $description, $nextLevel, $nextPathPrefix);
181
182 44
            [$hasLinks, $links] = $this->parseRelationshipLinks($name, $description);
0 ignored issues
show
Bug introduced by
The variable $hasLinks does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $links does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
183
184 44
            $hasMeta = array_key_exists(SchemaInterface::RELATIONSHIP_META, $description);
185 44
            $meta    = $hasMeta === true ? $description[SchemaInterface::RELATIONSHIP_META] : null;
186
187 44
            assert(
188 44
                $hasData || $hasMeta || $hasLinks,
189 44
                "Relationship `$name` for type `" . $this->getType() .
190 44
                '` MUST contain at least one of the following: links, data or meta.'
191
            );
192
193 44
            $relationship = $this->getFactory()->createRelationship(
194 44
                $nextPosition,
195 44
                $hasData,
196 44
                $relationshipData,
197 44
                $hasLinks,
198 44
                $links,
199 44
                $hasMeta,
200 44
                $meta
201
            );
202
203 44
            $this->relationshipsCache[$name] = $relationship;
204
205 44
            yield $name => $relationship;
206
        }
207 59
    }
208
209
    /**
210
     * @inheritdoc
211
     */
212 56
    public function hasLinks(): bool
213
    {
214 56
        $this->cacheLinks();
215
216 56
        return empty($this->links) === false;
217
    }
218
219
    /**
220
     * @inheritdoc
221
     */
222 55
    public function getLinks(): iterable
223
    {
224 55
        $this->cacheLinks();
225
226 55
        return $this->links;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->links; of type null|array adds the type array to the return on line 226 which is incompatible with the return type declared by the interface Neomerx\JsonApi\Contract...urceInterface::getLinks of type Neomerx\JsonApi\Contracts\Parser\iterable.
Loading history...
227
    }
228
229
    /**
230
     * @inheritdoc
231
     */
232 56
    public function hasResourceMeta(): bool
233
    {
234 56
        return $this->getSchema()->hasResourceMeta($this->getData());
235
    }
236
237
    /**
238
     * @inheritdoc
239
     */
240
    public function getResourceMeta()
241
    {
242
        return $this->getSchema()->getResourceMeta($this->getData());
243
    }
244
245
    /**
246
     * @inheritdoc
247
     */
248 59
    protected function setPosition(PositionInterface $position): self
249
    {
250 59
        assert($position->getLevel() >= ParserInterface::ROOT_LEVEL);
251
252 59
        $this->position = $position;
253
254 59
        return $this;
255
    }
256
257
    /**
258
     * @return FactoryInterface
259
     */
260 44
    protected function getFactory(): FactoryInterface
261
    {
262 44
        return $this->factory;
263
    }
264
265
    /**
266
     * @param FactoryInterface $factory
267
     *
268
     * @return self
269
     */
270 59
    protected function setFactory(FactoryInterface $factory): self
271
    {
272 59
        $this->factory = $factory;
273
274 59
        return $this;
275
    }
276
277
    /**
278
     * @return SchemaContainerInterface
279
     */
280 34
    protected function getSchemaContainer(): SchemaContainerInterface
281
    {
282 34
        return $this->schemaContainer;
283
    }
284
285
    /**
286
     * @param SchemaContainerInterface $container
287
     *
288
     * @return self
289
     */
290 59
    protected function setSchemaContainer(SchemaContainerInterface $container): self
291
    {
292 59
        $this->schemaContainer = $container;
293
294 59
        return $this;
295
    }
296
297
    /**
298
     * @return SchemaInterface
299
     */
300 59
    protected function getSchema(): SchemaInterface
301
    {
302 59
        return $this->schema;
303
    }
304
305
    /**
306
     * @param SchemaInterface $schema
307
     *
308
     * @return self
309
     */
310 59
    protected function setSchema(SchemaInterface $schema): self
311
    {
312 59
        $this->schema = $schema;
313
314 59
        return $this;
315
    }
316
317
    /**
318
     * @return mixed
319
     */
320 59
    protected function getData()
321
    {
322 59
        return $this->data;
323
    }
324
325
    /**
326
     * @param mixed $data
327
     *
328
     * @return self
329
     */
330 59
    protected function setData($data): self
331
    {
332 59
        $this->data  = $data;
333 59
        $this->index = $this->getSchema()->getId($data);
334 59
        $this->type  = $this->getSchema()->getType();
335
336 59
        return $this;
337
    }
338
339
    /**
340
     * Read and parse links from schema.
341
     */
342 56
    private function cacheLinks(): void
343
    {
344 56
        if ($this->links === null) {
345 56
            $this->links = [];
346 56
            foreach ($this->getSchema()->getLinks($this->getData()) as $name => $link) {
347 55
                assert(is_string($name) === true && empty($name) === false);
348 55
                assert($link instanceof LinkInterface);
349 55
                $this->links[$name] = $link;
350
            }
351
        }
352 56
    }
353
354
    /**
355
     * @param PositionInterface $position
356
     * @param mixed             $data
357
     *
358
     * @return RelationshipDataInterface
359
     */
360 34
    private function parseData(
361
        PositionInterface $position,
362
        $data
363
    ): RelationshipDataInterface {
364
        // support if data is callable (e.g. a closure used to postpone actual data reading)
365 34
        if (is_callable($data) === true) {
366 1
            $data = call_user_func($data);
367
        }
368
369 34
        $factory = $this->getFactory();
370 34
        if ($this->getSchemaContainer()->hasSchema($data) === true) {
371 23
            return $factory->createRelationshipDataIsResource($this->getSchemaContainer(), $position, $data);
372 33
        } elseif ($data instanceof IdentifierInterface) {
373 1
            return $factory->createRelationshipDataIsIdentifier($this->getSchemaContainer(), $position, $data);
374 32
        } elseif (is_array($data) === true) {
375 30
            return $factory->createRelationshipDataIsCollection($this->getSchemaContainer(), $position, $data);
0 ignored issues
show
Documentation introduced by
$data is of type array, but the function expects a object<Neomerx\JsonApi\C...cts\Factories\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...
376 6
        } elseif ($data instanceof Traversable) {
377 1
            return $factory->createRelationshipDataIsCollection(
378 1
                $this->getSchemaContainer(),
379 1
                $position,
380 1
                $data instanceof IteratorAggregate ? $data->getIterator() : $data
0 ignored issues
show
Documentation introduced by
$data instanceof \Iterat...->getIterator() : $data is of type object<Traversable>, but the function expects a object<Neomerx\JsonApi\C...cts\Factories\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...
381
            );
382 5
        } elseif ($data === null) {
383 5
            return $factory->createRelationshipDataIsNull();
384
        }
385
386 1
        throw new InvalidArgumentException(
387 1
            _(static::MSG_NO_SCHEMA_FOUND, get_class($data), $position->getPath())
388
        );
389
    }
390
391
    /**
392
     * @param string   $relationshipName
393
     * @param iterable $schemaLinks
394
     * @param bool     $addSelfLink
395
     * @param bool     $addRelatedLink
396
     *
397
     * @return iterable
398
     */
399 21
    private function parseLinks(
400
        string $relationshipName,
401
        iterable $schemaLinks,
402
        bool $addSelfLink,
403
        bool $addRelatedLink
404
    ): iterable {
405 21
        $gotSelf    = false;
406 21
        $gotRelated = false;
407
408 21
        foreach ($schemaLinks as $name => $link) {
409 11
            assert($link instanceof LinkInterface);
410 11
            if ($name === LinkInterface::SELF) {
411 6
                assert($gotSelf === false);
412 6
                $gotSelf     = true;
413 6
                $addSelfLink = false;
414 5
            } elseif ($name === LinkInterface::RELATED) {
415
                assert($gotRelated === false);
416
                $gotRelated     = true;
417
                $addRelatedLink = false;
418
            }
419
420 11
            yield $name => $link;
421
        }
422
423 21
        if ($addSelfLink === true) {
424 15
            $link = $this->getSchema()->getRelationshipSelfLink($this->getData(), $relationshipName);
425 15
            yield LinkInterface::SELF => $link;
426 15
            $gotSelf = true;
427
        }
428 21
        if ($addRelatedLink === true) {
429 13
            $link = $this->getSchema()->getRelationshipRelatedLink($this->getData(), $relationshipName);
430 13
            yield LinkInterface::RELATED => $link;
431 13
            $gotRelated = true;
432
        }
433
434
        // spec: check links has at least one of the following: self or related
435 21
        assert($gotSelf || $gotRelated);
436 21
    }
437
438
    /**
439
     * @param string $name
440
     * @param array  $description
441
     *
442
     * @return bool
443
     */
444 44
    private function assertRelationshipNameAndDescription(string $name, array $description): bool
445
    {
446 44
        assert(
447 44
            is_string($name) === true && empty($name) === false,
448 44
            "Relationship names for type `" . $this->getType() . '` should be non-empty strings.'
449
        );
450 44
        assert(
451 44
            is_array($description) === true && empty($description) === false,
452 44
            "Relationship `$name` for type `" . $this->getType() . '` should be a non-empty array.'
453
        );
454
455 44
        return true;
456
    }
457
458
    /**
459
     * @param string $name
460
     * @param array  $description
461
     * @param int    $nextLevel
462
     * @param string $nextPathPrefix
463
     *
464
     * @return array [has data, parsed data]
465
     */
466 44
    private function parseRelationshipData(
467
        string $name,
468
        array $description,
469
        int $nextLevel,
470
        string $nextPathPrefix
471
    ): array {
472 44
        $hasData = array_key_exists(SchemaInterface::RELATIONSHIP_DATA, $description);
473
        // either no data or data should be array/object/null
474 44
        assert(
475 44
            $hasData === false ||
476
            (
477 34
                is_array($data = $description[SchemaInterface::RELATIONSHIP_DATA]) === true ||
478 29
                is_object($data) === true ||
479 44
                $data === null
480
            )
481
        );
482
483 44
        $nextPosition = $this->getFactory()->createPosition(
484 44
            $nextLevel,
485 44
            $nextPathPrefix . $name,
486 44
            $this->getType(),
487 44
            $name
488
        );
489
490 44
        $relationshipData = $hasData === true ? $this->parseData(
491 34
            $nextPosition,
492 34
            $description[SchemaInterface::RELATIONSHIP_DATA]
493 44
        ) : null;
494
495 44
        return [$hasData, $relationshipData, $nextPosition];
496
    }
497
498
    /**
499
     * @param string $name
500
     * @param array  $description
501
     *
502
     * @return array
503
     */
504 44
    private function parseRelationshipLinks(string $name, array $description): array
505
    {
506 44
        $addSelfLink    = $description[SchemaInterface::RELATIONSHIP_LINKS_SELF] ??
507 44
            $this->getSchema()->isAddSelfLinkInRelationshipByDefault();
508 44
        $addRelatedLink = $description[SchemaInterface::RELATIONSHIP_LINKS_RELATED] ??
509 44
            $this->getSchema()->isAddRelatedLinkInRelationshipByDefault();
510 44
        assert(is_bool($addSelfLink) === true || $addSelfLink instanceof LinkInterface);
511 44
        assert(is_bool($addRelatedLink) === true || $addRelatedLink instanceof LinkInterface);
512
513 44
        $schemaLinks = array_key_exists(SchemaInterface::RELATIONSHIP_LINKS, $description) === true ?
514 44
            $description[SchemaInterface::RELATIONSHIP_LINKS] : [];
515 44
        assert(is_array($schemaLinks));
516
517
        // if `self` or `related` link was given as LinkInterface merge it with the other links
518 44
        if (is_bool($addSelfLink) === false) {
519
            $extraSchemaLinks[LinkInterface::SELF] = $addSelfLink;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$extraSchemaLinks was never initialized. Although not strictly required by PHP, it is generally a good practice to add $extraSchemaLinks = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
520
            $addSelfLink                           = false;
521
        }
522 44
        if (is_bool($addRelatedLink) === false) {
523
            $extraSchemaLinks[LinkInterface::RELATED] = $addRelatedLink;
0 ignored issues
show
Bug introduced by
The variable $extraSchemaLinks does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
524
            $addRelatedLink                           = false;
525
        }
526 44
        if (isset($extraSchemaLinks) === true) {
527
            // IDE do not understand it's defined without he line below
528
            assert(isset($extraSchemaLinks));
529
            $schemaLinks = array_merge($extraSchemaLinks, $schemaLinks);
530
            unset($extraSchemaLinks);
531
        }
532 44
        assert(is_bool($addSelfLink) === true && is_bool($addRelatedLink) === true);
533
534 44
        $hasLinks = $addSelfLink === true || $addRelatedLink === true || empty($schemaLinks) === false;
535 44
        $links    = $hasLinks === true ? $this->parseLinks($name, $schemaLinks, $addSelfLink, $addRelatedLink) : null;
0 ignored issues
show
Documentation introduced by
$schemaLinks 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...
536
537 44
        return [$hasLinks, $links];
538
    }
539
}
540