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

JsonApiSerializer::item()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 3

Importance

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