Completed
Pull Request — master (#279)
by
unknown
02:45
created

JsonApiSerializer::isEmpty()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

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