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

JsonApiSerializer::paginator()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 3

Importance

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