Completed
Branch next (92e3a0)
by Neomerx
01:44
created

IdentifierAndResource   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 454
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 92%

Importance

Changes 0
Metric Value
dl 0
loc 454
c 0
b 0
f 0
ccs 161
cts 175
cp 0.92
rs 3.44
wmc 62
lcom 1
cbo 5

24 Methods

Rating   Name   Duplication   Size   Complexity  
B parseLinks() 0 38 7
A __construct() 0 14 1
A getPosition() 0 4 1
A getId() 0 4 1
A getType() 0 4 1
A hasIdentifierMeta() 0 4 1
A getIdentifierMeta() 0 4 1
A getAttributes() 0 4 1
F getRelationships() 0 99 23
A hasLinks() 0 6 1
A getLinks() 0 6 1
A hasResourceMeta() 0 4 1
A getResourceMeta() 0 4 1
A setPosition() 0 8 1
A getFactory() 0 4 1
A setFactory() 0 6 1
A getSchemaContainer() 0 4 1
A setSchemaContainer() 0 6 1
A getSchema() 0 4 1
A setSchema() 0 6 1
A getData() 0 4 1
A setData() 0 8 1
A cacheLinks() 0 11 4
B parseData() 0 30 8

How to fix   Complexity   

Complex Class

Complex classes like IdentifierAndResource often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use IdentifierAndResource, and based on these observations, apply Extract Interface, too.

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