Completed
Pull Request — master (#1)
by Matt
02:50
created

JsonApiSerializer   F

Complexity

Total Complexity 88

Size/Duplication

Total Lines 665
Duplicated Lines 3.76 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 98.57%

Importance

Changes 0
Metric Value
wmc 88
lcom 1
cbo 0
dl 25
loc 665
ccs 207
cts 210
cp 0.9857
rs 1.935
c 0
b 0
f 0

35 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A collection() 0 10 2
B item() 0 40 6
A paginator() 0 30 3
A meta() 0 17 3
A null() 0 6 1
A includedData() 0 17 6
A sideloadIncludes() 0 4 1
A injectData() 0 10 2
A filterIncludes() 0 17 2
A getMandatoryFields() 0 4 1
A filterRootObject() 0 4 1
A setRootObjects() 0 6 1
A isRootObject() 0 5 1
A isCollection() 9 9 3
A isNull() 8 8 3
A isEmpty() 8 8 3
A fillRelationships() 0 14 4
A parseRelationships() 0 15 4
A getIdFromData() 0 9 2
A pullOutNestedIncludedData() 0 15 4
A shouldIncludeLinks() 0 4 1
A createIncludeObjects() 0 12 2
A createRootObjects() 0 8 2
A fillRelationshipAsCollection() 0 8 2
A fillRelationshipAsSingleResource() 0 6 1
A buildRelationships() 0 26 5
A addIncludeKeyToRelationsIfNotSet() 0 9 2
A addIncludedDataToRelationship() 0 11 2
A injectAvailableIncludeData() 0 21 5
A addRelationshipLinks() 0 18 3
A serializeIncludedObjectsWithCacheKey() 0 13 3
A areResourceLinksSet() 0 4 2
A isResourceMetaSet() 0 4 2
A isDataAttributesEmpty() 0 4 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like JsonApiSerializer 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 JsonApiSerializer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the League\Fractal package.
5
 *
6
 * (c) Phil Sturgeon <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace League\Fractal\Serializer;
13
14
use InvalidArgumentException;
15
use League\Fractal\Pagination\PaginatorInterface;
16
use League\Fractal\Resource\ResourceInterface;
17
18
class JsonApiSerializer extends ArraySerializer
19
{
20
    /** @var string|null */
21
    protected $baseUrl;
22
23
    /** @var array */
24
    protected $rootObjects;
25
26
    /**
27
     * JsonApiSerializer constructor.
28
     *
29
     * @param string $baseUrl
30
     */
31 37
    public function __construct(string $baseUrl = null)
32
    {
33 37
        $this->baseUrl = $baseUrl;
34 37
        $this->rootObjects = [];
35 37
    }
36
37
    /**
38
     * Serialize a collection.
39
     *
40
     * @param string $resourceKey
41
     * @param array $data
42
     *
43
     * @return array
44
     */
45 24
    public function collection($resourceKey, array $data): array
46
    {
47 24
        $resources = [];
48
49 24
        foreach ($data as $resource) {
50 23
            $resources[] = $this->item($resourceKey, $resource)['data'];
51
        }
52
53 24
        return ['data' => $resources];
54
    }
55
56
    /**
57
     * Serialize an item.
58
     *
59
     * @param string $resourceKey
60
     * @param array $data
61
     *
62
     * @return array
63
     */
64 37
    public function item($resourceKey, array $data): array
65
    {
66 37
        $id = $this->getIdFromData($data);
67
68
        $resource = [
69
            'data' => [
70 36
                'type' => $resourceKey,
71 36
                'id' => "$id",
72 36
                'attributes' => $data,
73
            ],
74
        ];
75
76 36
        unset($resource['data']['attributes']['id']);
77
78
79 36
        if ($this->areResourceLinksSet($resource)) {
80 1
            $custom_links = $data['links'];
81 1
            unset($resource['data']['attributes']['links']);
82
        }
83
84 36
        if ($this->isResourceMetaSet($resource)) {
85 2
            $resource['data']['meta'] = $data['meta'];
86 2
            unset($resource['data']['attributes']['meta']);
87
        }
88
89 36
        if ($this->isDataAttributesEmpty($resource)) {
90 3
            $resource['data']['attributes'] = (object) [];
91
        }
92
93 36
        if ($this->shouldIncludeLinks()) {
94 11
            $resource['data']['links'] = [
95 11
                'self' => "{$this->baseUrl}/$resourceKey/$id",
96
            ];
97 11
            if (isset($custom_links)) {
98 1
                $resource['data']['links'] = array_merge($resource['data']['links'], $custom_links);
99
            }
100
        }
101
102 36
        return $resource;
103
    }
104
105
    /**
106
     * Serialize the paginator.
107
     *
108
     * @param PaginatorInterface $paginator
109
     *
110
     * @return array
111
     */
112 3
    public function paginator(PaginatorInterface $paginator): array
113
    {
114 3
        $currentPage = (int) $paginator->getCurrentPage();
115 3
        $lastPage = (int) $paginator->getLastPage();
116
117
        $pagination = [
118 3
            'total' => (int) $paginator->getTotal(),
119 3
            'count' => (int) $paginator->getCount(),
120 3
            'per_page' => (int) $paginator->getPerPage(),
121 3
            'current_page' => $currentPage,
122 3
            'total_pages' => $lastPage,
123
        ];
124
125 3
        $pagination['links'] = [];
126
127 3
        $pagination['links']['self'] = $paginator->getUrl($currentPage);
128 3
        $pagination['links']['first'] = $paginator->getUrl(1);
129
130 3
        if ($currentPage > 1) {
131 2
            $pagination['links']['prev'] = $paginator->getUrl($currentPage - 1);
132
        }
133
134 3
        if ($currentPage < $lastPage) {
135 2
            $pagination['links']['next'] = $paginator->getUrl($currentPage + 1);
136
        }
137
138 3
        $pagination['links']['last'] = $paginator->getUrl($lastPage);
139
140 3
        return ['pagination' => $pagination];
141
    }
142
143
    /**
144
     * Serialize the meta.
145
     *
146
     * @param array $meta
147
     *
148
     * @return array
149
     */
150 36
    public function meta(array $meta): array
151
    {
152 36
        $result = [];
153
154 36
        if (empty($meta)) {
155 30
            return [];
156
        }
157
158 7
        $result['meta'] = $meta;
159
160 7
        if (array_key_exists('pagination', $result['meta'])) {
161 3
            $result['links'] = $result['meta']['pagination']['links'];
162 3
            unset($result['meta']['pagination']['links']);
163
        }
164
165 7
        return $result;
166
    }
167
168
    /**
169
     * Serialize the meta.
170
     *
171
     * @param array $meta
0 ignored issues
show
Bug introduced by
There is no parameter named $meta. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
172
     *
173
     * @return array
174
     */
175 2
    public function null(): array
176
    {
177
        return [
178 2
            'data' => null,
179
        ];
180
    }
181
182
    /**
183
     * Serialize the included data.
184
     *
185
     * @param ResourceInterface $resource
186
     * @param array $data
187
     *
188
     * @return array
189
     */
190 36
    public function includedData(ResourceInterface $resource, array $data): array
191
    {
192 36
        list($serializedData, $linkedIds) = $this->pullOutNestedIncludedData($data);
193
194 36
        foreach ($data as $value) {
195 36
            foreach ($value as $includeObject) {
196 22
                if ($this->isNull($includeObject) || $this->isEmpty($includeObject)) {
197 4
                    continue;
198
                }
199
200 20
                $includeObjects = $this->createIncludeObjects($includeObject);
201 20
                list($serializedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey($includeObjects, $linkedIds, $serializedData);
202
            }
203
        }
204
205 36
        return empty($serializedData) ? [] : ['included' => $serializedData];
206
    }
207
208
    /**
209
     * Indicates if includes should be side-loaded.
210
     *
211
     * @return bool
212
     */
213 37
    public function sideloadIncludes(): bool
214
    {
215 37
        return true;
216
    }
217
218
    /**
219
     * @param array $data
220
     * @param array $includedData
221
     *
222
     * @return array
223
     */
224 36
    public function injectData($data, $includedData): array
225
    {
226 36
        $relationships = $this->parseRelationships($includedData);
227
228 36
        if (!empty($relationships)) {
229 22
            $data = $this->fillRelationships($data, $relationships);
230
        }
231
232 36
        return $data;
233
    }
234
235
    /**
236
     * Hook to manipulate the final sideloaded includes.
237
     * The JSON API specification does not allow the root object to be included
238
     * into the sideloaded `included`-array. We have to make sure it is
239
     * filtered out, in case some object links to the root object in a
240
     * relationship.
241
     *
242
     * @param array $includedData
243
     * @param array $data
244
     *
245
     * @return array
246
     */
247 36
    public function filterIncludes($includedData, $data): array
248
    {
249 36
        if (!isset($includedData['included'])) {
250 18
            return $includedData;
251
        }
252
253
        // Create the RootObjects
254 18
        $this->createRootObjects($data);
255
256
        // Filter out the root objects
257 18
        $filteredIncludes = array_filter($includedData['included'], [$this, 'filterRootObject']);
258
259
        // Reset array indices
260 18
        $includedData['included'] = array_merge([], $filteredIncludes);
261
262 18
        return $includedData;
263
    }
264
265
    /**
266
     * Get the mandatory fields for the serializer
267
     *
268
     * @return array
269
     */
270 4
    public function getMandatoryFields() : array
271
    {
272 4
        return ['id'];
273
    }
274
275
    /**
276
     * Filter function to delete root objects from array.
277
     *
278
     * @param array $object
279
     *
280
     * @return bool
281
     */
282 18
    protected function filterRootObject($object): bool
283
    {
284 18
        return !$this->isRootObject($object);
285
    }
286
287
    /**
288
     * Set the root objects of the JSON API tree.
289
     *
290
     * @param array $objects
291
     */
292 18
    protected function setRootObjects(array $objects = []): void
293
    {
294
        $this->rootObjects = array_map(function ($object) {
295 18
            return "{$object['type']}:{$object['id']}";
296 18
        }, $objects);
297 18
    }
298
299
    /**
300
     * Determines whether an object is a root object of the JSON API tree.
301
     *
302
     * @param array $object
303
     *
304
     * @return bool
305
     */
306 18
    protected function isRootObject($object): bool
307
    {
308 18
        $objectKey = "{$object['type']}:{$object['id']}";
309 18
        return in_array($objectKey, $this->rootObjects);
310
    }
311
312
    /**
313
     * @param array|null $data
314
     *
315
     * @return bool
316
     */
317 29 View Code Duplication
    protected function isCollection($data): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
318
    {
319 29
        if ($data === null) {
320
            return false;
321
        }
322
323 29
        return array_key_exists('data', $data) &&
324 29
        array_key_exists(0, $data['data']);
325
    }
326
327
    /**
328
     * @param array|null $data
329
     *
330
     * @return bool
331
     */
332 22 View Code Duplication
    protected function isNull($data): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
333
    {
334 22
        if ($data === null) {
335
            return true;
336
        }
337
338 22
        return array_key_exists('data', $data) && $data['data'] === null;
339
    }
340
341
    /**
342
     * @param array|null $data
343
     *
344
     * @return bool
345
     */
346 21 View Code Duplication
    protected function isEmpty($data): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
347
    {
348 21
        if ($data === null) {
349
            return true;
350
        }
351
352 21
        return array_key_exists('data', $data) && $data['data'] === [];
353
    }
354
355
    /**
356
     * @param array $data
357
     * @param array $relationships
358
     *
359
     * @return array
360
     */
361 22
    protected function fillRelationships($data, $relationships): array
362
    {
363 22
        if ($this->isCollection($data)) {
364 9
            foreach ($relationships as $key => $relationship) {
365 9
                $data = $this->fillRelationshipAsCollection($data, $relationship, $key);
366
            }
367
        } else { // Single resource
368 15
            foreach ($relationships as $key => $relationship) {
369 15
                $data = $this->fillRelationshipAsSingleResource($data, $relationship, $key);
370
            }
371
        }
372
373 22
        return $data;
374
    }
375
376
    /**
377
     * @param array $includedData
378
     *
379
     * @return array
380
     */
381 36
    protected function parseRelationships($includedData): array
382
    {
383 36
        $relationships = [];
384
385 36
        foreach ($includedData as $key => $inclusion) {
386 36
            foreach ($inclusion as $includeKey => $includeObject) {
387 22
                $relationships = $this->buildRelationships($includeKey, $relationships, $includeObject, $key);
388 22
                if (isset($includedData[0][$includeKey]['meta'])) {
389 1
                    $relationships[$includeKey][0]['meta'] = $includedData[0][$includeKey]['meta'];
390
                }
391
            }
392
        }
393
394 36
        return $relationships;
395
    }
396
397
    /**
398
     * @param array $data
399
     *
400
     * @return integer
401
     */
402 37
    protected function getIdFromData(array $data): int
403
    {
404 37
        if (!array_key_exists('id', $data)) {
405 1
            throw new InvalidArgumentException(
406 1
                'JSON API resource objects MUST have a valid id'
407
            );
408
        }
409 36
        return $data['id'];
410
    }
411
412
    /**
413
     * Keep all sideloaded inclusion data on the top level.
414
     *
415
     * @param array $data
416
     *
417
     * @return array
418
     */
419 36
    protected function pullOutNestedIncludedData(array $data): array
420
    {
421 36
        $includedData = [];
422 36
        $linkedIds = [];
423
424 36
        foreach ($data as $value) {
425 36
            foreach ($value as $includeObject) {
426 22
                if (isset($includeObject['included'])) {
427 4
                    list($includedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey($includeObject['included'], $linkedIds, $includedData);
428
                }
429
            }
430
        }
431
432 36
        return [$includedData, $linkedIds];
433
    }
434
435
    /**
436
     * Whether or not the serializer should include `links` for resource objects.
437
     *
438
     * @return bool
439
     */
440 36
    protected function shouldIncludeLinks(): bool
441
    {
442 36
        return $this->baseUrl !== null;
443
    }
444
445
    /**
446
     * Check if the objects are part of a collection or not
447
     *
448
     * @param array $includeObject
449
     *
450
     * @return array
451
     */
452 20
    private function createIncludeObjects($includeObject): array
453
    {
454 20
        if ($this->isCollection($includeObject)) {
455 11
            $includeObjects = $includeObject['data'];
456
457 11
            return $includeObjects;
458
        } else {
459 13
            $includeObjects = [$includeObject['data']];
460
461 13
            return $includeObjects;
462
        }
463
    }
464
465
    /**
466
     * Sets the RootObjects, either as collection or not.
467
     *
468
     * @param array $data
469
     */
470 18
    private function createRootObjects(array $data): void
471
    {
472 18
        if ($this->isCollection($data)) {
473 8
            $this->setRootObjects($data['data']);
474
        } else {
475 10
            $this->setRootObjects([$data['data']]);
476
        }
477 18
    }
478
479
    /**
480
     * Loops over the relationships of the provided data and formats it
481
     *
482
     * @param array $data
483
     * @param array $relationship
484
     * @param string $key
485
     *
486
     * @return array
487
     */
488 9
    private function fillRelationshipAsCollection($data, $relationship, $key): array
489
    {
490 9
        foreach ($relationship as $index => $relationshipData) {
491 9
            $data['data'][$index]['relationships'][$key] = $relationshipData;
492
        }
493
494 9
        return $data;
495
    }
496
497
498
    /**
499
     * @param array $data
500
     * @param array $relationship
501
     * @param string $key
502
     *
503
     * @return array
504
     */
505 15
    private function fillRelationshipAsSingleResource($data, $relationship, $key): array
506
    {
507 15
        $data['data']['relationships'][$key] = $relationship[0];
508
509 15
        return $data;
510
    }
511
512
    /**
513
     * @param string $includeKey
514
     * @param array $relationships
515
     * @param array $includeObject
516
     * @param string $key
517
     *
518
     * @return array
519
     */
520 22
    private function buildRelationships(string $includeKey, array $relationships, array $includeObject, string $key): array
521
    {
522 22
        $relationships = $this->addIncludeKeyToRelationsIfNotSet($includeKey, $relationships);
523
524 22
        if ($this->isNull($includeObject)) {
525 2
            $relationship = $this->null();
526 21
        } elseif ($this->isEmpty($includeObject)) {
527
            $relationship = [
528 2
                'data' => [],
529
            ];
530 20
        } elseif (! empty($includeObject) && $this->isCollection($includeObject)) {
531 11
            $relationship = ['data' => []];
532 11
            $relationship = $this->addIncludedDataToRelationship($includeObject, $relationship);
533
        } else {
534
            $relationship = [
535
                'data' => [
536 13
                    'type' => $includeObject['data']['type'],
537 13
                    'id' => $includeObject['data']['id'],
538
                ],
539
            ];
540
        }
541
542 22
        $relationships[$includeKey][$key] = $relationship;
543
544 22
        return $relationships;
545
    }
546
547
    /**
548
     * @param string $includeKey
549
     * @param array $relationships
550
     *
551
     * @return array
552
     */
553 22
    private function addIncludeKeyToRelationsIfNotSet(string $includeKey, array $relationships): array
554
    {
555 22
        if (!array_key_exists($includeKey, $relationships)) {
556 22
            $relationships[$includeKey] = [];
557 22
            return $relationships;
558
        }
559
560 9
        return $relationships;
561
    }
562
563
    /**
564
     * @param array $includeObject
565
     * @param array $relationship
566
     *
567
     * @return array
568
     */
569 11
    private function addIncludedDataToRelationship(array $includeObject, array $relationship) : array
570
    {
571 11
        foreach ($includeObject['data'] as $object) {
572 11
            $relationship['data'][] = [
573 11
                'type' => $object['type'],
574 11
                'id' => $object['id'],
575
            ];
576
        }
577
578 11
        return $relationship;
579
    }
580
581
    /**
582
     * {@inheritdoc}
583
     */
584 35
    public function injectAvailableIncludeData($data, $availableIncludes): array
585
    {
586 35
        if (!$this->shouldIncludeLinks()) {
587 24
            return $data;
588
        }
589
590 11
        if ($this->isCollection($data)) {
591
            $data['data'] = array_map(function ($resource) use ($availableIncludes) {
592 7
                foreach ($availableIncludes as $relationshipKey) {
593 7
                    $resource = $this->addRelationshipLinks($resource, $relationshipKey);
594
                }
595 7
                return $resource;
596 7
            }, $data['data']);
597
        } else {
598 6
            foreach ($availableIncludes as $relationshipKey) {
599 6
                $data['data'] = $this->addRelationshipLinks($data['data'], $relationshipKey);
600
            }
601
        }
602
603 11
        return $data;
604
    }
605
606
607
    /**
608
     * Adds links for all available includes to a single resource.
609
     *
610
     * @param array $resource         The resource to add relationship links to
611
     * @param string $relationshipKey The resource key of the relationship
612
     */
613 11
    private function addRelationshipLinks(array $resource, string $relationshipKey): array
614
    {
615 11
        if (!isset($resource['relationships']) || !isset($resource['relationships'][$relationshipKey])) {
616 11
            $resource['relationships'][$relationshipKey] = [];
617
        }
618
619 11
        $resource['relationships'][$relationshipKey] = array_merge(
620
            [
621
                'links' => [
622 11
                    'self'   => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/relationships/{$relationshipKey}",
623 11
                    'related' => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/{$relationshipKey}",
624
                ]
625
            ],
626 11
            $resource['relationships'][$relationshipKey]
627
        );
628
629 11
        return $resource;
630
    }
631
632
    /**
633
     * @param $includeObjects
634
     * @param $linkedIds
635
     * @param $serializedData
636
     *
637
     * @return array
638
     */
639 20
    private function serializeIncludedObjectsWithCacheKey(array $includeObjects, array $linkedIds, array $serializedData): array
640
    {
641 20
        foreach ($includeObjects as $object) {
642 20
            $includeType = $object['type'];
643 20
            $includeId = $object['id'];
644 20
            $cacheKey = "$includeType:$includeId";
645 20
            if (!array_key_exists($cacheKey, $linkedIds)) {
646 20
                $serializedData[] = $object;
647 20
                $linkedIds[$cacheKey] = $object;
648
            }
649
        }
650 20
        return [$serializedData, $linkedIds];
651
    }
652
653
    /**
654
     * @param array $resource
655
     *
656
     * @return bool
657
     */
658 36
    private function areResourceLinksSet(array $resource): bool
659
    {
660 36
        return isset($resource['data']['attributes']['links'])?: false;
661
    }
662
663
    /**
664
     * @param array $resource
665
     *
666
     * @return bool
667
     */
668 36
    private function isResourceMetaSet(array $resource): bool
669
    {
670 36
        return isset($resource['data']['attributes']['meta'])?: false;
671
    }
672
673
    /**
674
     * @param array $resource
675
     *
676
     * @return bool
677
     */
678 36
    private function isDataAttributesEmpty(array $resource) : bool
679
    {
680 36
        return empty($resource['data']['attributes'])?: false;
681
    }
682
}
683