Completed
Pull Request — master (#290)
by
unknown
02:52
created

JsonApiSerializer::sideloadIncludes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
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 27
    public function __construct($baseUrl = null)
29
    {
30 27
        $this->baseUrl = $baseUrl;
31 27
        $this->rootObjects = [];
32 27
    }
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 27
    public function item($resourceKey, array $data)
62
    {
63 27
        $id = $this->getIdFromData($data);
64
65
        $resource = [
66
            'data' => [
67 26
                'type' => $resourceKey,
68 26
                'id' => "$id",
69 26
                'attributes' => $data,
70 26
            ],
71 26
        ];
72
73 26
        unset($resource['data']['attributes']['id']);
74
75 26
        if ($this->shouldIncludeLinks()) {
76 9
            $resource['data']['links'] = [
77 9
                'self' => "{$this->baseUrl}/$resourceKey/$id",
78
            ];
79 9
        }
80
81 26
        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 26
    public function meta(array $meta)
130
    {
131 26
        if (empty($meta)) {
132 21
            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 26
    public function includedData(ResourceInterface $resource, array $data)
164
    {
165 26
        list($serializedData, $linkedIds) = $this->pullOutNestedIncludedData($data);
166
167 26
        foreach ($data as $value) {
168 26
            foreach ($value as $includeObject) {
169 17
                if ($this->isNull($includeObject) || $this->isEmpty($includeObject)) {
170 4
                    continue;
171
                }
172
173 15
                $includeObjects = $this->createIncludeObjects($includeObject);
174
175 15 View Code Duplication
                foreach ($includeObjects as $object) {
176 15
                    $includeType = $object['type'];
177 15
                    $includeId = $object['id'];
178 15
                    $cacheKey = "$includeType:$includeId";
179 15
                    if (!array_key_exists($cacheKey, $linkedIds)) {
180 15
                        $serializedData[] = $object;
181 15
                        $linkedIds[$cacheKey] = $object;
182 15
                    }
183 15
                }
184 26
            }
185 26
        }
186
187 26
        return empty($serializedData) ? [] : ['included' => $serializedData];
188
    }
189
190
    /**
191
     * Indicates if includes should be side-loaded.
192
     *
193
     * @return bool
194
     */
195 27
    public function sideloadIncludes()
196
    {
197 27
        return true;
198
    }
199
200
    /**
201
     * @param array $data
202
     * @param array $includedData
203
     *
204
     * @return array
205
     */
206 26
    public function injectData($data, $includedData)
207
    {
208 26
        $relationships = $this->parseRelationships($includedData);
209
210 26
        if (!empty($relationships)) {
211 17
            $data = $this->fillRelationships($data, $relationships);
212 17
        }
213
214 26
        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 26
    public function filterIncludes($includedData, $data)
230
    {
231 26
        if (!isset($includedData['included'])) {
232 11
            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
     * Filter function to delete root objects from array.
249
     *
250
     * @param array $object
251
     *
252
     * @return bool
253
     */
254 15
    protected function filterRootObject($object)
255
    {
256 15
        return !$this->isRootObject($object);
257
    }
258
259
    /**
260
     * Set the root objects of the JSON API tree.
261
     *
262
     * @param array $objects
263
     */
264
    protected function setRootObjects(array $objects = [])
265
    {
266 15
        $this->rootObjects = array_map(function ($object) {
267 15
            return "{$object['type']}:{$object['id']}";
268 15
        }, $objects);
269 15
    }
270
271
    /**
272
     * Determines whether an object is a root object of the JSON API tree.
273
     *
274
     * @param array $object
275
     *
276
     * @return bool
277
     */
278 15
    protected function isRootObject($object)
279
    {
280 15
        $objectKey = "{$object['type']}:{$object['id']}";
281 15
        return in_array($objectKey, $this->rootObjects);
282
    }
283
284
    /**
285
     * @param array $data
286
     *
287
     * @return bool
288
     */
289 17
    protected function isCollection($data)
290
    {
291 17
        return array_key_exists('data', $data) &&
292 17
        array_key_exists(0, $data['data']);
293
    }
294
295
    /**
296
     * @param array $data
297
     *
298
     * @return bool
299
     */
300 17
    protected function isNull($data)
301
    {
302 17
        return array_key_exists('data', $data) && $data['data'] === null;
303
    }
304
305
    /**
306
     * @param array $data
307
     *
308
     * @return bool
309
     */
310 16
    protected function isEmpty($data)
311
    {
312 16
        return array_key_exists('data', $data) && $data['data'] === [];
313
    }
314
315
    /**
316
     * @param array $data
317
     * @param array $relationships
318
     *
319
     * @return array
320
     */
321 17
    protected function fillRelationships($data, $relationships)
322
    {
323 17
        if ($this->isCollection($data)) {
324 9
            foreach ($relationships as $key => $relationship) {
325 9
                $data = $this->fillRelationshipAsCollection($data, $relationship, $key);
326 9
            }
327 9
        } else { // Single resource
328 10
            foreach ($relationships as $key => $relationship) {
329 10
                $data = $this->fillRelationshipAsSingleResource($data, $relationship, $key);
330 10
            }
331
        }
332
333 17
        return $data;
334
    }
335
336
    /**
337
     * @param array $includedData
338
     *
339
     * @return array
340
     */
341 26
    protected function parseRelationships($includedData)
342
    {
343 26
        $relationships = [];
344
345 26
        foreach ($includedData as $key => $inclusion) {
346 26
            foreach ($inclusion as $includeKey => $includeObject) {
347 17
                $relationships = $this->buildRelationships($includeKey, $relationships, $includeObject, $key);
348 26
            }
349 26
        }
350
351 26
        return $relationships;
352
    }
353
354
    /**
355
     * @param array $data
356
     *
357
     * @return integer
358
     */
359 27
    protected function getIdFromData(array $data)
360
    {
361 27
        if (!array_key_exists('id', $data)) {
362 1
            throw new InvalidArgumentException(
363
                'JSON API resource objects MUST have a valid id'
364 1
            );
365
        }
366 26
        return $data['id'];
367
    }
368
369
    /**
370
     * Keep all sideloaded inclusion data on the top level.
371
     *
372
     * @param array $data
373
     *
374
     * @return array
375
     */
376 26
    protected function pullOutNestedIncludedData(array $data)
377
    {
378 26
        $includedData = [];
379 26
        $linkedIds = [];
380
381 26
        foreach ($data as $value) {
382 26
            foreach ($value as $includeObject) {
383 17
                if (isset($includeObject['included'])) {
384 3 View Code Duplication
                    foreach ($includeObject['included'] as $object) {
385 3
                        $includeType = $object['type'];
386 3
                        $includeId = $object['id'];
387 3
                        $cacheKey = "$includeType:$includeId";
388
389 3
                        if (!array_key_exists($cacheKey, $linkedIds)) {
390 3
                            $includedData[] = $object;
391 3
                            $linkedIds[$cacheKey] = $object;
392 3
                        }
393 3
                    }
394 3
                }
395 26
            }
396 26
        }
397
398 26
        return [$includedData, $linkedIds];
399
    }
400
401
    /**
402
     * Whether or not the serializer should include `links` for resource objects.
403
     *
404
     * @return bool
405
     */
406 26
    protected function shouldIncludeLinks()
407
    {
408 26
        return $this->baseUrl !== null;
409
    }
410
411
    /**
412
     * Check if the objects are part of a collection or not
413
     *
414
     * @param $includeObject
415
     *
416
     * @return array
417
     */
418 15
    private function createIncludeObjects($includeObject)
419
    {
420 15
        if ($this->isCollection($includeObject)) {
421 8
            $includeObjects = $includeObject['data'];
422
423 8
            return $includeObjects;
424
        } else {
425 10
            $includeObjects = [$includeObject['data']];
426
427 10
            return $includeObjects;
428
        }
429
    }
430
431
    /**
432
     * Sets the RootObjects, either as collection or not.
433
     *
434
     * @param $data
435
     */
436 15
    private function createRootObjects($data)
437
    {
438 15
        if ($this->isCollection($data)) {
439 8
            $this->setRootObjects($data['data']);
440 8
        } else {
441 7
            $this->setRootObjects([$data['data']]);
442
        }
443 15
    }
444
445
446
    /**
447
     * Loops over the relationships of the provided data and formats it
448
     *
449
     * @param $data
450
     * @param $relationship
451
     * @param $key
452
     *
453
     * @return array
454
     */
455 9
    private function fillRelationshipAsCollection($data, $relationship, $key)
456
    {
457 9
        foreach ($relationship as $index => $relationshipData) {
458 9
            $data['data'][$index]['relationships'][$key] = $relationshipData;
459
460 9
            if ($this->shouldIncludeLinks()) {
461 2
                $data['data'][$index]['relationships'][$key] = array_merge([
462
                    'links' => [
463 2
                        'self' => "{$this->baseUrl}/{$data['data'][$index]['type']}/{$data['data'][$index]['id']}/relationships/$key",
464 2
                        'related' => "{$this->baseUrl}/{$data['data'][$index]['type']}/{$data['data'][$index]['id']}/$key",
465 2
                    ],
466 2
                ], $data['data'][$index]['relationships'][$key]);
467 2
            }
468 9
        }
469
470 9
        return $data;
471
    }
472
473
474
    /**
475
     * @param $data
476
     * @param $relationship
477
     * @param $key
478
     *
479
     * @return array
480
     */
481 10
    private function fillRelationshipAsSingleResource($data, $relationship, $key)
482
    {
483 10
        $data['data']['relationships'][$key] = $relationship[0];
484
485 10
        if ($this->shouldIncludeLinks()) {
486 2
            $data['data']['relationships'][$key] = array_merge([
487
                'links' => [
488 2
                    'self' => "{$this->baseUrl}/{$data['data']['type']}/{$data['data']['id']}/relationships/$key",
489 2
                    'related' => "{$this->baseUrl}/{$data['data']['type']}/{$data['data']['id']}/$key",
490 2
                ],
491 2
            ], $data['data']['relationships'][$key]);
492
493 2
            return $data;
494
        }
495 8
        return $data;
496
    }
497
498
    /**
499
     * @param $includeKey
500
     * @param $relationships
501
     * @param $includeObject
502
     * @param $key
503
     *
504
     * @return array
505
     */
506 17
    private function buildRelationships($includeKey, $relationships, $includeObject, $key)
507
    {
508 17
        $relationships = $this->addIncludekeyToRelationsIfNotSet($includeKey, $relationships);
509
510 17
        if ($this->isNull($includeObject)) {
511 2
            $relationship = $this->null();
512 17
        } elseif ($this->isEmpty($includeObject)) {
513
            $relationship = [
514 2
                'data' => [],
515 2
            ];
516 16
        } elseif ($this->isCollection($includeObject)) {
517 8
            $relationship = ['data' => []];
518
519 8
            $relationship = $this->addIncludedDataToRelationship($includeObject, $relationship);
520 8
        } else {
521
            $relationship = [
522
                'data' => [
523 10
                    'type' => $includeObject['data']['type'],
524 10
                    'id' => $includeObject['data']['id'],
525 10
                ],
526 10
            ];
527
        }
528
529 17
        $relationships[$includeKey][$key] = $relationship;
530
531 17
        return $relationships;
532
    }
533
534
    /**
535
     * @param $includeKey
536
     * @param $relationships
537
     *
538
     * @return array
539
     */
540 17
    private function addIncludekeyToRelationsIfNotSet($includeKey, $relationships)
541
    {
542 17
        if (!array_key_exists($includeKey, $relationships)) {
543 17
            $relationships[$includeKey] = [];
544 17
            return $relationships;
545
        }
546
547 9
        return $relationships;
548
    }
549
550
    /**
551
     * @param $includeObject
552
     * @param $relationship
553
     *
554
     * @return array
555
     */
556 8
    private function addIncludedDataToRelationship($includeObject, $relationship)
557
    {
558 8
        foreach ($includeObject['data'] as $object) {
559 8
            $relationship['data'][] = [
560 8
                'type' => $object['type'],
561 8
                'id' => $object['id'],
562
            ];
563 8
        }
564
565 8
        return $relationship;
566
    }
567
}
568