Completed
Pull Request — master (#331)
by Matt
03:42
created

JsonApiSerializer::injectAvailableIncludeData()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5

Importance

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