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

JsonApiSerializer::buildRelationships()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 13
cts 13
cp 1
rs 9.1928
c 0
b 0
f 0
cc 5
nc 4
nop 4
crap 5
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 36
    public function meta(array $meta): array
144
    {
145 36
        $result = [];
146
147 36
        if (empty($meta)) {
148 30
            return [];
149
        }
150
151 7
        $result['meta'] = $meta;
152
153 7
        if (array_key_exists('pagination', $result['meta'])) {
154 3
            $result['links'] = $result['meta']['pagination']['links'];
155 3
            unset($result['meta']['pagination']['links']);
156
        }
157
158 7
        return $result;
159
    }
160
161
    /**
162
     * Serialize the meta.
163
     *
164
     * @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...
165
     *
166
     * @return array
167
     */
168 2
    public function null(): array
169
    {
170
        return [
171 2
            'data' => null,
172
        ];
173
    }
174
175
    /**
176
     * Serialize the included data.
177
     *
178
     * @param ResourceInterface $resource
179
     * @param array $data
180
     *
181
     * @return array
182
     */
183 36
    public function includedData(ResourceInterface $resource, array $data): array
184
    {
185 36
        list($serializedData, $linkedIds) = $this->pullOutNestedIncludedData($data);
186
187 36
        foreach ($data as $value) {
188 36
            foreach ($value as $includeObject) {
189 22
                if ($this->isNull($includeObject) || $this->isEmpty($includeObject)) {
190 4
                    continue;
191
                }
192
193 20
                $includeObjects = $this->createIncludeObjects($includeObject);
194 20
                list($serializedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey($includeObjects, $linkedIds, $serializedData);
195
            }
196
        }
197
198 36
        return empty($serializedData) ? [] : ['included' => $serializedData];
199
    }
200
201
    /**
202
     * Indicates if includes should be side-loaded.
203
     *
204
     * @return bool
205
     */
206 37
    public function sideloadIncludes(): bool
207
    {
208 37
        return true;
209
    }
210
211
    /**
212
     * @param array $data
213
     * @param array $includedData
214
     *
215
     * @return array
216
     */
217 36
    public function injectData($data, $includedData): array
218
    {
219 36
        $relationships = $this->parseRelationships($includedData);
220
221 36
        if (!empty($relationships)) {
222 22
            $data = $this->fillRelationships($data, $relationships);
223
        }
224
225 36
        return $data;
226
    }
227
228
    /**
229
     * Hook to manipulate the final sideloaded includes.
230
     * The JSON API specification does not allow the root object to be included
231
     * into the sideloaded `included`-array. We have to make sure it is
232
     * filtered out, in case some object links to the root object in a
233
     * relationship.
234
     *
235
     * @param array $includedData
236
     * @param array $data
237
     *
238
     * @return array
239
     */
240 36
    public function filterIncludes($includedData, $data): array
241
    {
242 36
        if (!isset($includedData['included'])) {
243 18
            return $includedData;
244
        }
245
246
        // Create the RootObjects
247 18
        $this->createRootObjects($data);
248
249
        // Filter out the root objects
250 18
        $filteredIncludes = array_filter($includedData['included'], [$this, 'filterRootObject']);
251
252
        // Reset array indices
253 18
        $includedData['included'] = array_merge([], $filteredIncludes);
254
255 18
        return $includedData;
256
    }
257
258
    /**
259
     * Get the mandatory fields for the serializer
260
     *
261
     * @return array
262
     */
263 4
    public function getMandatoryFields() : array
264
    {
265 4
        return ['id'];
266
    }
267
268
    /**
269
     * Filter function to delete root objects from array.
270
     *
271
     * @param array $object
272
     *
273
     * @return bool
274
     */
275 18
    protected function filterRootObject($object): bool
276
    {
277 18
        return !$this->isRootObject($object);
278
    }
279
280
    /**
281
     * Set the root objects of the JSON API tree.
282
     *
283
     * @param array $objects
284
     */
285 18
    protected function setRootObjects(array $objects = []): void
286
    {
287
        $this->rootObjects = array_map(function ($object) {
288 18
            return "{$object['type']}:{$object['id']}";
289 18
        }, $objects);
290 18
    }
291
292
    /**
293
     * Determines whether an object is a root object of the JSON API tree.
294
     *
295
     * @param array $object
296
     *
297
     * @return bool
298
     */
299 18
    protected function isRootObject($object): bool
300
    {
301 18
        $objectKey = "{$object['type']}:{$object['id']}";
302 18
        return in_array($objectKey, $this->rootObjects);
303
    }
304
305
    /**
306
     * @param array|null $data
307
     *
308
     * @return bool
309
     */
310 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...
311
    {
312 29
        if ($data === null) {
313
            return false;
314
        }
315
316 29
        return array_key_exists('data', $data) &&
317 29
        array_key_exists(0, $data['data']);
318
    }
319
320
    /**
321
     * @param array|null $data
322
     *
323
     * @return bool
324
     */
325 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...
326
    {
327 22
        if ($data === null) {
328
            return true;
329
        }
330
331 22
        return array_key_exists('data', $data) && $data['data'] === null;
332
    }
333
334
    /**
335
     * @param array|null $data
336
     *
337
     * @return bool
338
     */
339 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...
340
    {
341 21
        if ($data === null) {
342
            return true;
343
        }
344
345 21
        return array_key_exists('data', $data) && $data['data'] === [];
346
    }
347
348
    /**
349
     * @param array $data
350
     * @param array $relationships
351
     *
352
     * @return array
353
     */
354 22
    protected function fillRelationships($data, $relationships): array
355
    {
356 22
        if ($this->isCollection($data)) {
357 9
            foreach ($relationships as $key => $relationship) {
358 9
                $data = $this->fillRelationshipAsCollection($data, $relationship, $key);
359
            }
360
        } else { // Single resource
361 15
            foreach ($relationships as $key => $relationship) {
362 15
                $data = $this->fillRelationshipAsSingleResource($data, $relationship, $key);
363
            }
364
        }
365
366 22
        return $data;
367
    }
368
369
    /**
370
     * @param array $includedData
371
     *
372
     * @return array
373
     */
374 36
    protected function parseRelationships($includedData): array
375
    {
376 36
        $relationships = [];
377
378 36
        foreach ($includedData as $key => $inclusion) {
379 36
            foreach ($inclusion as $includeKey => $includeObject) {
380 22
                $relationships = $this->buildRelationships($includeKey, $relationships, $includeObject, $key);
381 22
                if (isset($includedData[0][$includeKey]['meta'])) {
382 1
                    $relationships[$includeKey][0]['meta'] = $includedData[0][$includeKey]['meta'];
383
                }
384
            }
385
        }
386
387 36
        return $relationships;
388
    }
389
390
    /**
391
     * @param array $data
392
     *
393
     * @return integer
394
     */
395 37
    protected function getIdFromData(array $data): int
396
    {
397 37
        if (!array_key_exists('id', $data)) {
398 1
            throw new InvalidArgumentException(
399 1
                'JSON API resource objects MUST have a valid id'
400
            );
401
        }
402 36
        return $data['id'];
403
    }
404
405
    /**
406
     * Keep all sideloaded inclusion data on the top level.
407
     *
408
     * @param array $data
409
     *
410
     * @return array
411
     */
412 36
    protected function pullOutNestedIncludedData(array $data): array
413
    {
414 36
        $includedData = [];
415 36
        $linkedIds = [];
416
417 36
        foreach ($data as $value) {
418 36
            foreach ($value as $includeObject) {
419 22
                if (isset($includeObject['included'])) {
420 4
                    list($includedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey($includeObject['included'], $linkedIds, $includedData);
421
                }
422
            }
423
        }
424
425 36
        return [$includedData, $linkedIds];
426
    }
427
428
    /**
429
     * Whether or not the serializer should include `links` for resource objects.
430
     *
431
     * @return bool
432
     */
433 36
    protected function shouldIncludeLinks(): bool
434
    {
435 36
        return $this->baseUrl !== null;
436
    }
437
438
    /**
439
     * Check if the objects are part of a collection or not
440
     *
441
     * @param array $includeObject
442
     *
443
     * @return array
444
     */
445 20
    private function createIncludeObjects($includeObject): array
446
    {
447 20
        if ($this->isCollection($includeObject)) {
448 11
            $includeObjects = $includeObject['data'];
449
450 11
            return $includeObjects;
451
        } else {
452 13
            $includeObjects = [$includeObject['data']];
453
454 13
            return $includeObjects;
455
        }
456
    }
457
458
    /**
459
     * Sets the RootObjects, either as collection or not.
460
     *
461
     * @param array $data
462
     */
463 18
    private function createRootObjects(array $data): void
464
    {
465 18
        if ($this->isCollection($data)) {
466 8
            $this->setRootObjects($data['data']);
467
        } else {
468 10
            $this->setRootObjects([$data['data']]);
469
        }
470 18
    }
471
472
    /**
473
     * Loops over the relationships of the provided data and formats it
474
     *
475
     * @param $data
476
     * @param $relationship
477
     * @param $key
478
     *
479
     * @return array
480
     */
481 9
    private function fillRelationshipAsCollection($data, $relationship, $key): array
482
    {
483 9
        foreach ($relationship as $index => $relationshipData) {
484 9
            $data['data'][$index]['relationships'][$key] = $relationshipData;
485
        }
486
487 9
        return $data;
488
    }
489
490
491
    /**
492
     * @param $data
493
     * @param $relationship
494
     * @param $key
495
     *
496
     * @return array
497
     */
498 15
    private function fillRelationshipAsSingleResource($data, $relationship, $key): array
499
    {
500 15
        $data['data']['relationships'][$key] = $relationship[0];
501
502 15
        return $data;
503
    }
504
505
    /**
506
     * @param $includeKey
507
     * @param $relationships
508
     * @param $includeObject
509
     * @param $key
510
     *
511
     * @return array
512
     */
513 22
    private function buildRelationships(string $includeKey, array $relationships, array $includeObject, string $key): array
514
    {
515 22
        $relationships = $this->addIncludeKeyToRelationsIfNotSet($includeKey, $relationships);
516
517 22
        if ($this->isNull($includeObject)) {
518 2
            $relationship = $this->null();
519 21
        } elseif ($this->isEmpty($includeObject)) {
520
            $relationship = [
521 2
                'data' => [],
522
            ];
523 20
        } elseif (! empty($includeObject) && $this->isCollection($includeObject)) {
524 11
            $relationship = ['data' => []];
525 11
            $relationship = $this->addIncludedDataToRelationship($includeObject, $relationship);
526
        } else {
527
            $relationship = [
528
                'data' => [
529 13
                    'type' => $includeObject['data']['type'],
530 13
                    'id' => $includeObject['data']['id'],
531
                ],
532
            ];
533
        }
534
535 22
        $relationships[$includeKey][$key] = $relationship;
536
537 22
        return $relationships;
538
    }
539
540
    /**
541
     * @param $includeKey
542
     * @param $relationships
543
     *
544
     * @return array
545
     */
546 22
    private function addIncludeKeyToRelationsIfNotSet(string $includeKey, array $relationships): array
547
    {
548 22
        if (!array_key_exists($includeKey, $relationships)) {
549 22
            $relationships[$includeKey] = [];
550 22
            return $relationships;
551
        }
552
553 9
        return $relationships;
554
    }
555
556
    /**
557
     * @param array $includeObject
558
     * @param array $relationship
559
     *
560
     * @return array
561
     */
562 11
    private function addIncludedDataToRelationship(array $includeObject, array $relationship) : array
563
    {
564 11
        foreach ($includeObject['data'] as $object) {
565 11
            $relationship['data'][] = [
566 11
                'type' => $object['type'],
567 11
                'id' => $object['id'],
568
            ];
569
        }
570
571 11
        return $relationship;
572
    }
573
574
    /**
575
     * {@inheritdoc}
576
     */
577 35
    public function injectAvailableIncludeData($data, $availableIncludes): array
578
    {
579 35
        if (!$this->shouldIncludeLinks()) {
580 24
            return $data;
581
        }
582
583 11
        if ($this->isCollection($data)) {
584
            $data['data'] = array_map(function ($resource) use ($availableIncludes) {
585 7
                foreach ($availableIncludes as $relationshipKey) {
586 7
                    $resource = $this->addRelationshipLinks($resource, $relationshipKey);
587
                }
588 7
                return $resource;
589 7
            }, $data['data']);
590
        } else {
591 6
            foreach ($availableIncludes as $relationshipKey) {
592 6
                $data['data'] = $this->addRelationshipLinks($data['data'], $relationshipKey);
593
            }
594
        }
595
596 11
        return $data;
597
    }
598
599
600
    /**
601
     * Adds links for all available includes to a single resource.
602
     *
603
     * @param array $resource         The resource to add relationship links to
604
     * @param string $relationshipKey The resource key of the relationship
605
     */
606 11
    private function addRelationshipLinks(array $resource, string $relationshipKey): array
607
    {
608 11
        if (!isset($resource['relationships']) || !isset($resource['relationships'][$relationshipKey])) {
609 11
            $resource['relationships'][$relationshipKey] = [];
610
        }
611
612 11
        $resource['relationships'][$relationshipKey] = array_merge(
613
            [
614
                'links' => [
615 11
                    'self'   => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/relationships/{$relationshipKey}",
616 11
                    'related' => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/{$relationshipKey}",
617
                ]
618
            ],
619 11
            $resource['relationships'][$relationshipKey]
620
        );
621
622 11
        return $resource;
623
    }
624
625
    /**
626
     * @param $includeObjects
627
     * @param $linkedIds
628
     * @param $serializedData
629
     *
630
     * @return array
631
     */
632 20
    private function serializeIncludedObjectsWithCacheKey(array $includeObjects, array $linkedIds, array $serializedData): array
633
    {
634 20
        foreach ($includeObjects as $object) {
635 20
            $includeType = $object['type'];
636 20
            $includeId = $object['id'];
637 20
            $cacheKey = "$includeType:$includeId";
638 20
            if (!array_key_exists($cacheKey, $linkedIds)) {
639 20
                $serializedData[] = $object;
640 20
                $linkedIds[$cacheKey] = $object;
641
            }
642
        }
643 20
        return [$serializedData, $linkedIds];
644
    }
645
646
    /**
647
     * @param array $resource
648
     *
649
     * @return bool
650
     */
651 36
    private function areResourceLinksSet(array $resource): bool
652
    {
653 36
        return isset($resource['data']['attributes']['links'])?: false;
654
    }
655
656
    /**
657
     * @param array $resource
658
     *
659
     * @return bool
660
     */
661 36
    private function isResourceMetaSet(array $resource): bool
662
    {
663 36
        return isset($resource['data']['attributes']['meta'])?: false;
664
    }
665
666
    /**
667
     * @param array $resource
668
     *
669
     * @return bool
670
     */
671 36
    private function isDataAttributesEmpty(array $resource) : bool
672
    {
673 36
        return empty($resource['data']['attributes'])?: false;
674
    }
675
}
676