Completed
Pull Request — master (#304)
by Benoît
03:33
created

JsonApiSerializer::pullOutNestedIncludedData()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 24
Code Lines 14

Duplication

Lines 10
Ratio 41.67 %

Code Coverage

Tests 19
CRAP Score 6

Importance

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