Completed
Pull Request — master (#284)
by
unknown
04:36
created

JsonApiSerializer::setRootObjects()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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