JsonApiSerializer   F
last analyzed

Complexity

Total Complexity 82

Size/Duplication

Total Lines 625
Duplicated Lines 5.28 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 98.52%

Importance

Changes 0
Metric Value
wmc 82
lcom 1
cbo 0
dl 33
loc 625
ccs 200
cts 203
cp 0.9852
rs 1.975
c 0
b 0
f 0

32 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A collection() 0 10 2
B item() 8 39 6
A paginator() 0 30 3
A meta() 0 15 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

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
    protected $baseUrl;
21
    protected $rootObjects;
22
23
    /**
24
     * JsonApiSerializer constructor.
25
     *
26
     * @param string $baseUrl
27
     */
28 38
    public function __construct($baseUrl = null)
29
    {
30 38
        $this->baseUrl = $baseUrl;
31 38
        $this->rootObjects = [];
32 38
    }
33
34
    /**
35
     * Serialize a collection.
36
     *
37
     * @param string $resourceKey
38
     * @param array $data
39
     *
40
     * @return array
41
     */
42 24
    public function collection($resourceKey, array $data)
43
    {
44 24
        $resources = [];
45
46 24
        foreach ($data as $resource) {
47 23
            $resources[] = $this->item($resourceKey, $resource)['data'];
48
        }
49
50 24
        return ['data' => $resources];
51
    }
52
53
    /**
54
     * Serialize an item.
55
     *
56
     * @param string $resourceKey
57
     * @param array $data
58
     *
59
     * @return array
60
     */
61 38
    public function item($resourceKey, array $data)
62
    {
63 38
        $id = $this->getIdFromData($data);
64
65
        $resource = [
66
            'data' => [
67 37
                'type' => $resourceKey,
68 37
                'id' => "$id",
69 37
                'attributes' => $data,
70
            ],
71
        ];
72
73 37
        unset($resource['data']['attributes']['id']);
74
75 37 View Code Duplication
        if (isset($resource['data']['attributes']['links'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
76 2
            $custom_links = $data['links'];
77 2
            unset($resource['data']['attributes']['links']);
78
        }
79
80 37 View Code Duplication
        if (isset($resource['data']['attributes']['meta'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
81 2
            $resource['data']['meta'] = $data['meta'];
82 2
            unset($resource['data']['attributes']['meta']);
83
        }
84
85 37
        if (empty($resource['data']['attributes'])) {
86 3
            $resource['data']['attributes'] = (object) [];
87
        }
88
89 37
        if ($this->shouldIncludeLinks()) {
90 12
            $resource['data']['links'] = [
91 12
                'self' => "{$this->baseUrl}/$resourceKey/$id",
92
            ];
93 12
            if (isset($custom_links)) {
94 2
                $resource['data']['links'] = array_merge($resource['data']['links'], $custom_links);
95
            }
96
        }
97
98 37
        return $resource;
99
    }
100
101
    /**
102
     * Serialize the paginator.
103
     *
104
     * @param PaginatorInterface $paginator
105
     *
106
     * @return array
107
     */
108 3
    public function paginator(PaginatorInterface $paginator)
109
    {
110 3
        $currentPage = (int)$paginator->getCurrentPage();
111 3
        $lastPage = (int)$paginator->getLastPage();
112
113
        $pagination = [
114 3
            'total' => (int)$paginator->getTotal(),
115 3
            'count' => (int)$paginator->getCount(),
116 3
            'per_page' => (int)$paginator->getPerPage(),
117 3
            'current_page' => $currentPage,
118 3
            'total_pages' => $lastPage,
119
        ];
120
121 3
        $pagination['links'] = [];
122
123 3
        $pagination['links']['self'] = $paginator->getUrl($currentPage);
124 3
        $pagination['links']['first'] = $paginator->getUrl(1);
125
126 3
        if ($currentPage > 1) {
127 2
            $pagination['links']['prev'] = $paginator->getUrl($currentPage - 1);
128
        }
129
130 3
        if ($currentPage < $lastPage) {
131 2
            $pagination['links']['next'] = $paginator->getUrl($currentPage + 1);
132
        }
133
134 3
        $pagination['links']['last'] = $paginator->getUrl($lastPage);
135
136 3
        return ['pagination' => $pagination];
137
    }
138
139
    /**
140
     * Serialize the meta.
141
     *
142
     * @param array $meta
143
     *
144
     * @return array
145
     */
146 37
    public function meta(array $meta)
147
    {
148 37
        if (empty($meta)) {
149 31
            return [];
150
        }
151
152 7
        $result['meta'] = $meta;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = 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...
153
154 7
        if (array_key_exists('pagination', $result['meta'])) {
155 3
            $result['links'] = $result['meta']['pagination']['links'];
156 3
            unset($result['meta']['pagination']['links']);
157
        }
158
159 7
        return $result;
160
    }
161
162
    /**
163
     * @return array
164
     */
165 2
    public function null()
166
    {
167
        return [
168 2
            'data' => null,
169
        ];
170
    }
171
172
    /**
173
     * Serialize the included data.
174
     *
175
     * @param ResourceInterface $resource
176
     * @param array $data
177
     *
178
     * @return array
179
     */
180 37
    public function includedData(ResourceInterface $resource, array $data)
181
    {
182 37
        list($serializedData, $linkedIds) = $this->pullOutNestedIncludedData($data);
183
184 37
        foreach ($data as $value) {
185 37
            foreach ($value as $includeObject) {
186 22
                if ($this->isNull($includeObject) || $this->isEmpty($includeObject)) {
187 4
                    continue;
188
                }
189
190 20
                $includeObjects = $this->createIncludeObjects($includeObject);
191 20
                list($serializedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey($includeObjects, $linkedIds, $serializedData);
192
            }
193
        }
194
195 37
        return empty($serializedData) ? [] : ['included' => $serializedData];
196
    }
197
198
    /**
199
     * Indicates if includes should be side-loaded.
200
     *
201
     * @return bool
202
     */
203 38
    public function sideloadIncludes()
204
    {
205 38
        return true;
206
    }
207
208
    /**
209
     * @param array $data
210
     * @param array $includedData
211
     *
212
     * @return array
213
     */
214 37
    public function injectData($data, $includedData)
215
    {
216 37
        $relationships = $this->parseRelationships($includedData);
217
218 37
        if (!empty($relationships)) {
219 22
            $data = $this->fillRelationships($data, $relationships);
220
        }
221
222 37
        return $data;
223
    }
224
225
    /**
226
     * Hook to manipulate the final sideloaded includes.
227
     * The JSON API specification does not allow the root object to be included
228
     * into the sideloaded `included`-array. We have to make sure it is
229
     * filtered out, in case some object links to the root object in a
230
     * relationship.
231
     *
232
     * @param array $includedData
233
     * @param array $data
234
     *
235
     * @return array
236
     */
237 37
    public function filterIncludes($includedData, $data)
238
    {
239 37
        if (!isset($includedData['included'])) {
240 19
            return $includedData;
241
        }
242
243
        // Create the RootObjects
244 18
        $this->createRootObjects($data);
245
246
        // Filter out the root objects
247 18
        $filteredIncludes = array_filter($includedData['included'], [$this, 'filterRootObject']);
248
249
        // Reset array indizes
250 18
        $includedData['included'] = array_merge([], $filteredIncludes);
251
252 18
        return $includedData;
253
    }
254
255
    /**
256
     * Get the mandatory fields for the serializer
257
     *
258
     * @return array
259
     */
260 4
    public function getMandatoryFields()
261
    {
262 4
        return ['id'];
263
    }
264
265
    /**
266
     * Filter function to delete root objects from array.
267
     *
268
     * @param array $object
269
     *
270
     * @return bool
271
     */
272 18
    protected function filterRootObject($object)
273
    {
274 18
        return !$this->isRootObject($object);
275
    }
276
277
    /**
278
     * Set the root objects of the JSON API tree.
279
     *
280
     * @param array $objects
281
     */
282 18
    protected function setRootObjects(array $objects = [])
283
    {
284
        $this->rootObjects = array_map(function ($object) {
285 18
            return "{$object['type']}:{$object['id']}";
286 18
        }, $objects);
287 18
    }
288
289
    /**
290
     * Determines whether an object is a root object of the JSON API tree.
291
     *
292
     * @param array $object
293
     *
294
     * @return bool
295
     */
296 18
    protected function isRootObject($object)
297
    {
298 18
        $objectKey = "{$object['type']}:{$object['id']}";
299 18
        return in_array($objectKey, $this->rootObjects);
300
    }
301
302
    /**
303
     * @param array|null $data
304
     *
305
     * @return bool
306
     */
307 30 View Code Duplication
    protected function isCollection($data)
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...
308
    {
309 30
        if ($data === null) {
310
            return false;
311
        }
312
313 30
        return array_key_exists('data', $data) &&
314 30
        array_key_exists(0, $data['data']);
315
    }
316
317
    /**
318
     * @param array|null $data
319
     *
320
     * @return bool
321
     */
322 22 View Code Duplication
    protected function isNull($data)
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...
323
    {
324 22
        if ($data === null) {
325
            return true;
326
        }
327
328 22
        return array_key_exists('data', $data) && $data['data'] === null;
329
    }
330
331
    /**
332
     * @param array|null $data
333
     *
334
     * @return bool
335
     */
336 21 View Code Duplication
    protected function isEmpty($data)
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...
337
    {
338 21
        if ($data === null) {
339
            return true;
340
        }
341
342 21
        return array_key_exists('data', $data) && $data['data'] === [];
343
    }
344
345
    /**
346
     * @param array $data
347
     * @param array $relationships
348
     *
349
     * @return array
350
     */
351 22
    protected function fillRelationships($data, $relationships)
352
    {
353 22
        if ($this->isCollection($data)) {
354 9
            foreach ($relationships as $key => $relationship) {
355 9
                $data = $this->fillRelationshipAsCollection($data, $relationship, $key);
356
            }
357
        } else { // Single resource
358 15
            foreach ($relationships as $key => $relationship) {
359 15
                $data = $this->fillRelationshipAsSingleResource($data, $relationship, $key);
360
            }
361
        }
362
363 22
        return $data;
364
    }
365
366
    /**
367
     * @param array $includedData
368
     *
369
     * @return array
370
     */
371 37
    protected function parseRelationships($includedData)
372
    {
373 37
        $relationships = [];
374
375 37
        foreach ($includedData as $key => $inclusion) {
376 37
            foreach ($inclusion as $includeKey => $includeObject) {
377 22
                $relationships = $this->buildRelationships($includeKey, $relationships, $includeObject, $key);
378 22
                if (isset($includedData[0][$includeKey]['meta'])) {
379 1
                    $relationships[$includeKey][0]['meta'] = $includedData[0][$includeKey]['meta'];
380
                }
381
            }
382
        }
383
384 37
        return $relationships;
385
    }
386
387
    /**
388
     * @param array $data
389
     *
390
     * @return integer
391
     */
392 38
    protected function getIdFromData(array $data)
393
    {
394 38
        if (!array_key_exists('id', $data)) {
395 1
            throw new InvalidArgumentException(
396 1
                'JSON API resource objects MUST have a valid id'
397
            );
398
        }
399 37
        return $data['id'];
400
    }
401
402
    /**
403
     * Keep all sideloaded inclusion data on the top level.
404
     *
405
     * @param array $data
406
     *
407
     * @return array
408
     */
409 37
    protected function pullOutNestedIncludedData(array $data)
410
    {
411 37
        $includedData = [];
412 37
        $linkedIds = [];
413
414 37
        foreach ($data as $value) {
415 37
            foreach ($value as $includeObject) {
416 22
                if (isset($includeObject['included'])) {
417 4
                    list($includedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey($includeObject['included'], $linkedIds, $includedData);
418
                }
419
            }
420
        }
421
422 37
        return [$includedData, $linkedIds];
423
    }
424
425
    /**
426
     * Whether or not the serializer should include `links` for resource objects.
427
     *
428
     * @return bool
429
     */
430 37
    protected function shouldIncludeLinks()
431
    {
432 37
        return $this->baseUrl !== null;
433
    }
434
435
    /**
436
     * Check if the objects are part of a collection or not
437
     *
438
     * @param array $includeObject
439
     *
440
     * @return array
441
     */
442 20
    private function createIncludeObjects($includeObject)
443
    {
444 20
        if ($this->isCollection($includeObject)) {
445 11
            $includeObjects = $includeObject['data'];
446
447 11
            return $includeObjects;
448
        } else {
449 13
            $includeObjects = [$includeObject['data']];
450
451 13
            return $includeObjects;
452
        }
453
    }
454
455
    /**
456
     * Sets the RootObjects, either as collection or not.
457
     *
458
     * @param array $data
459
     */
460 18
    private function createRootObjects($data)
461
    {
462 18
        if ($this->isCollection($data)) {
463 8
            $this->setRootObjects($data['data']);
464
        } else {
465 10
            $this->setRootObjects([$data['data']]);
466
        }
467 18
    }
468
469
470
    /**
471
     * Loops over the relationships of the provided data and formats it
472
     *
473
     * @param array $data
474
     * @param array $relationship
475
     * @param string $key
476
     *
477
     * @return array
478
     */
479 9
    private function fillRelationshipAsCollection($data, $relationship, $key)
480
    {
481 9
        foreach ($relationship as $index => $relationshipData) {
482 9
            $data['data'][$index]['relationships'][$key] = $relationshipData;
483
        }
484
485 9
        return $data;
486
    }
487
488
489
    /**
490
     * @param array $data
491
     * @param array $relationship
492
     * @param string $key
493
     *
494
     * @return array
495
     */
496 15
    private function fillRelationshipAsSingleResource($data, $relationship, $key)
497
    {
498 15
        $data['data']['relationships'][$key] = $relationship[0];
499
500 15
        return $data;
501
    }
502
503
    /**
504
     * @param mixed $includeKey
505
     * @param array $relationships
506
     * @param array|null $includeObject
507
     * @param string $key
508
     *
509
     * @return array
510
     */
511 22
    private function buildRelationships($includeKey, $relationships, $includeObject, $key)
512
    {
513 22
        $relationships = $this->addIncludekeyToRelationsIfNotSet($includeKey, $relationships);
514
515 22
        if ($this->isNull($includeObject)) {
516 2
            $relationship = $this->null();
517 21
        } elseif ($this->isEmpty($includeObject)) {
518
            $relationship = [
519 2
                'data' => [],
520
            ];
521 20
        } elseif ($includeObject && $this->isCollection($includeObject)) {
522 11
            $relationship = ['data' => []];
523 11
            $relationship = $this->addIncludedDataToRelationship($includeObject, $relationship);
524
        } else {
525
            $relationship = [
526
                'data' => [
527 13
                    'type' => $includeObject['data']['type'],
528 13
                    'id' => $includeObject['data']['id'],
529
                ],
530
            ];
531
        }
532
533 22
        $relationships[$includeKey][$key] = $relationship;
534
535 22
        return $relationships;
536
    }
537
538
    /**
539
     * @param mixed $includeKey
540
     * @param array $relationships
541
     *
542
     * @return array
543
     */
544 22
    private function addIncludekeyToRelationsIfNotSet($includeKey, $relationships)
545
    {
546 22
        if (!array_key_exists($includeKey, $relationships)) {
547 22
            $relationships[$includeKey] = [];
548 22
            return $relationships;
549
        }
550
551 9
        return $relationships;
552
    }
553
554
    /**
555
     * @param array $includeObject
556
     * @param array $relationship
557
     *
558
     * @return array
559
     */
560 11
    private function addIncludedDataToRelationship($includeObject, $relationship)
561
    {
562 11
        foreach ($includeObject['data'] as $object) {
563 11
            $relationship['data'][] = [
564 11
                'type' => $object['type'],
565 11
                'id' => $object['id'],
566
            ];
567
        }
568
569 11
        return $relationship;
570
    }
571
572
    /**
573
     * {@inheritdoc}
574
     */
575 36
    public function injectAvailableIncludeData($data, $availableIncludes)
576
    {
577 36
        if (!$this->shouldIncludeLinks()) {
578 24
            return $data;
579
        }
580
581 12
        if ($this->isCollection($data)) {
582
            $data['data'] = array_map(function ($resource) use ($availableIncludes) {
583 7
                foreach ($availableIncludes as $relationshipKey) {
584 7
                    $resource = $this->addRelationshipLinks($resource, $relationshipKey);
585
                }
586 7
                return $resource;
587 7
            }, $data['data']);
588
        } else {
589 7
            foreach ($availableIncludes as $relationshipKey) {
590 7
                $data['data'] = $this->addRelationshipLinks($data['data'], $relationshipKey);
591
            }
592
        }
593
594 12
        return $data;
595
    }
596
597
    /**
598
     * Adds links for all available includes to a single resource.
599
     *
600
     * @param array $resource         The resource to add relationship links to
601
     * @param string $relationshipKey The resource key of the relationship
602
     */
603 12
    private function addRelationshipLinks($resource, $relationshipKey)
604
    {
605 12
        if (!isset($resource['relationships']) || !isset($resource['relationships'][$relationshipKey])) {
606 12
            $resource['relationships'][$relationshipKey] = [];
607
        }
608
609 12
        $resource['relationships'][$relationshipKey] = array_merge(
610
            [
611
                'links' => [
612 12
                    'self'   => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/relationships/{$relationshipKey}",
613 12
                    'related' => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/{$relationshipKey}",
614
                ]
615
            ],
616 12
            $resource['relationships'][$relationshipKey]
617
        );
618
619 12
        return $resource;
620
    }
621
622
    /**
623
     * @param array $includeObjects
624
     * @param array $linkedIds
625
     * @param array $serializedData
626
     *
627
     * @return array
628
     */
629 20
    private function serializeIncludedObjectsWithCacheKey($includeObjects, $linkedIds, $serializedData)
630
    {
631 20
        foreach ($includeObjects as $object) {
632 20
            $includeType = $object['type'];
633 20
            $includeId = $object['id'];
634 20
            $cacheKey = "$includeType:$includeId";
635 20
            if (!array_key_exists($cacheKey, $linkedIds)) {
636 20
                $serializedData[] = $object;
637 20
                $linkedIds[$cacheKey] = $object;
638
            }
639
        }
640 20
        return [$serializedData, $linkedIds];
641
    }
642
}
643