Completed
Pull Request — master (#273)
by Gonçalo
04:11
created

JsonApiSerializer   C

Complexity

Total Complexity 67

Size/Duplication

Total Lines 551
Duplicated Lines 3.45 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 100%

Importance

Changes 25
Bugs 10 Features 8
Metric Value
wmc 67
c 25
b 10
f 8
lcom 1
cbo 2
dl 19
loc 551
ccs 207
cts 207
cp 1
rs 5.7097

29 Methods

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