Completed
Push — master ( ebc071...9dcd83 )
by Tobias
29:49 queued 29:28
created

src/Serializer/JsonApiSerializer.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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'])) {
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
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;
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 $data
304
     *
305
     * @return bool
306
     */
307 30
    protected function isCollection($data)
308
    {
309 30
        return array_key_exists('data', $data) &&
310 30
        array_key_exists(0, $data['data']);
311
    }
312
313
    /**
314
     * @param array $data
315
     *
316
     * @return bool
317
     */
318 22
    protected function isNull($data)
319
    {
320 22
        return array_key_exists('data', $data) && $data['data'] === null;
321
    }
322
323
    /**
324
     * @param array $data
325
     *
326
     * @return bool
327
     */
328 21
    protected function isEmpty($data)
329
    {
330 21
        return array_key_exists('data', $data) && $data['data'] === [];
331
    }
332
333
    /**
334
     * @param array $data
335
     * @param array $relationships
336
     *
337
     * @return array
338
     */
339 22
    protected function fillRelationships($data, $relationships)
340
    {
341 22
        if ($this->isCollection($data)) {
342 9
            foreach ($relationships as $key => $relationship) {
343 9
                $data = $this->fillRelationshipAsCollection($data, $relationship, $key);
344
            }
345
        } else { // Single resource
346 15
            foreach ($relationships as $key => $relationship) {
347 15
                $data = $this->fillRelationshipAsSingleResource($data, $relationship, $key);
348
            }
349
        }
350
351 22
        return $data;
352
    }
353
354
    /**
355
     * @param array $includedData
356
     *
357
     * @return array
358
     */
359 37
    protected function parseRelationships($includedData)
360
    {
361 37
        $relationships = [];
362
363 37
        foreach ($includedData as $key => $inclusion) {
364 37
            foreach ($inclusion as $includeKey => $includeObject) {
365 22
                $relationships = $this->buildRelationships($includeKey, $relationships, $includeObject, $key);
366 22
                if (isset($includedData[0][$includeKey]['meta'])) {
367 1
                    $relationships[$includeKey][0]['meta'] = $includedData[0][$includeKey]['meta'];
368
                }
369
            }
370
        }
371
372 37
        return $relationships;
373
    }
374
375
    /**
376
     * @param array $data
377
     *
378
     * @return integer
379
     */
380 38
    protected function getIdFromData(array $data)
381
    {
382 38
        if (!array_key_exists('id', $data)) {
383 1
            throw new InvalidArgumentException(
384 1
                'JSON API resource objects MUST have a valid id'
385
            );
386
        }
387 37
        return $data['id'];
388
    }
389
390
    /**
391
     * Keep all sideloaded inclusion data on the top level.
392
     *
393
     * @param array $data
394
     *
395
     * @return array
396
     */
397 37
    protected function pullOutNestedIncludedData(array $data)
398
    {
399 37
        $includedData = [];
400 37
        $linkedIds = [];
401
402 37
        foreach ($data as $value) {
403 37
            foreach ($value as $includeObject) {
404 22
                if (isset($includeObject['included'])) {
405 4
                    list($includedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey($includeObject['included'], $linkedIds, $includedData);
406
                }
407
            }
408
        }
409
410 37
        return [$includedData, $linkedIds];
411
    }
412
413
    /**
414
     * Whether or not the serializer should include `links` for resource objects.
415
     *
416
     * @return bool
417
     */
418 37
    protected function shouldIncludeLinks()
419
    {
420 37
        return $this->baseUrl !== null;
421
    }
422
423
    /**
424
     * Check if the objects are part of a collection or not
425
     *
426
     * @param $includeObject
427
     *
428
     * @return array
429
     */
430 20
    private function createIncludeObjects($includeObject)
431
    {
432 20
        if ($this->isCollection($includeObject)) {
433 11
            $includeObjects = $includeObject['data'];
434
435 11
            return $includeObjects;
436
        } else {
437 13
            $includeObjects = [$includeObject['data']];
438
439 13
            return $includeObjects;
440
        }
441
    }
442
443
    /**
444
     * Sets the RootObjects, either as collection or not.
445
     *
446
     * @param $data
447
     */
448 18
    private function createRootObjects($data)
449
    {
450 18
        if ($this->isCollection($data)) {
451 8
            $this->setRootObjects($data['data']);
452
        } else {
453 10
            $this->setRootObjects([$data['data']]);
454
        }
455 18
    }
456
457
458
    /**
459
     * Loops over the relationships of the provided data and formats it
460
     *
461
     * @param $data
462
     * @param $relationship
463
     * @param $key
464
     *
465
     * @return array
466
     */
467 9
    private function fillRelationshipAsCollection($data, $relationship, $key)
468
    {
469 9
        foreach ($relationship as $index => $relationshipData) {
470 9
            $data['data'][$index]['relationships'][$key] = $relationshipData;
471
        }
472
473 9
        return $data;
474
    }
475
476
477
    /**
478
     * @param $data
479
     * @param $relationship
480
     * @param $key
481
     *
482
     * @return array
483
     */
484 15
    private function fillRelationshipAsSingleResource($data, $relationship, $key)
485
    {
486 15
        $data['data']['relationships'][$key] = $relationship[0];
487
488 15
        return $data;
489
    }
490
491
    /**
492
     * @param $includeKey
493
     * @param $relationships
494
     * @param $includeObject
495
     * @param $key
496
     *
497
     * @return array
498
     */
499 22
    private function buildRelationships($includeKey, $relationships, $includeObject, $key)
500
    {
501 22
        $relationships = $this->addIncludekeyToRelationsIfNotSet($includeKey, $relationships);
502
503 22
        if ($this->isNull($includeObject)) {
504 2
            $relationship = $this->null();
505 21
        } elseif ($this->isEmpty($includeObject)) {
506
            $relationship = [
507 2
                'data' => [],
508
            ];
509 20
        } elseif ($this->isCollection($includeObject)) {
510 11
            $relationship = ['data' => []];
511
512 11
            $relationship = $this->addIncludedDataToRelationship($includeObject, $relationship);
513
        } else {
514
            $relationship = [
515
                'data' => [
516 13
                    'type' => $includeObject['data']['type'],
517 13
                    'id' => $includeObject['data']['id'],
518
                ],
519
            ];
520
        }
521
522 22
        $relationships[$includeKey][$key] = $relationship;
523
524 22
        return $relationships;
525
    }
526
527
    /**
528
     * @param $includeKey
529
     * @param $relationships
530
     *
531
     * @return array
532
     */
533 22
    private function addIncludekeyToRelationsIfNotSet($includeKey, $relationships)
534
    {
535 22
        if (!array_key_exists($includeKey, $relationships)) {
536 22
            $relationships[$includeKey] = [];
537 22
            return $relationships;
538
        }
539
540 9
        return $relationships;
541
    }
542
543
    /**
544
     * @param $includeObject
545
     * @param $relationship
546
     *
547
     * @return array
548
     */
549 11
    private function addIncludedDataToRelationship($includeObject, $relationship)
550
    {
551 11
        foreach ($includeObject['data'] as $object) {
552 11
            $relationship['data'][] = [
553 11
                'type' => $object['type'],
554 11
                'id' => $object['id'],
555
            ];
556
        }
557
558 11
        return $relationship;
559
    }
560
561
    /**
562
     * {@inheritdoc}
563
     */
564 36
    public function injectAvailableIncludeData($data, $availableIncludes)
565
    {
566 36
        if (!$this->shouldIncludeLinks()) {
567 24
            return $data;
568
        }
569
570 12
        if ($this->isCollection($data)) {
571
            $data['data'] = array_map(function ($resource) use ($availableIncludes) {
572 7
                foreach ($availableIncludes as $relationshipKey) {
573 7
                    $resource = $this->addRelationshipLinks($resource, $relationshipKey);
574
                }
575 7
                return $resource;
576 7
            }, $data['data']);
577
        } else {
578 7
            foreach ($availableIncludes as $relationshipKey) {
579 7
                $data['data'] = $this->addRelationshipLinks($data['data'], $relationshipKey);
580
            }
581
        }
582
583 12
        return $data;
584
    }
585
586
    /**
587
     * Adds links for all available includes to a single resource.
588
     *
589
     * @param array $resource         The resource to add relationship links to
590
     * @param string $relationshipKey The resource key of the relationship
591
     */
592 12
    private function addRelationshipLinks($resource, $relationshipKey)
593
    {
594 12
        if (!isset($resource['relationships']) || !isset($resource['relationships'][$relationshipKey])) {
595 12
            $resource['relationships'][$relationshipKey] = [];
596
        }
597
598 12
        $resource['relationships'][$relationshipKey] = array_merge(
599
            [
600
                'links' => [
601 12
                    'self'   => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/relationships/{$relationshipKey}",
602 12
                    'related' => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/{$relationshipKey}",
603
                ]
604
            ],
605 12
            $resource['relationships'][$relationshipKey]
606
        );
607
608 12
        return $resource;
609
    }
610
611
    /**
612
     * @param $includeObjects
613
     * @param $linkedIds
614
     * @param $serializedData
615
     *
616
     * @return array
617
     */
618 20
    private function serializeIncludedObjectsWithCacheKey($includeObjects, $linkedIds, $serializedData)
619
    {
620 20
        foreach ($includeObjects as $object) {
621 20
            $includeType = $object['type'];
622 20
            $includeId = $object['id'];
623 20
            $cacheKey = "$includeType:$includeId";
624 20
            if (!array_key_exists($cacheKey, $linkedIds)) {
625 20
                $serializedData[] = $object;
626 20
                $linkedIds[$cacheKey] = $object;
627
            }
628
        }
629 20
        return [$serializedData, $linkedIds];
630
    }
631
}
632