Completed
Pull Request — master (#1)
by Matt
01:17
created

JsonApiSerializer::null()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
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
    /** @var null|string */
21
    protected $baseUrl;
22
23
    /** @var array */
24
    protected $rootObjects;
25
26
    /**
27
     * JsonApiSerializer constructor.
28
     *
29
     * @param string $baseUrl
30
     */
31 19
    public function __construct($baseUrl = null)
32
    {
33 19
        $this->baseUrl = $baseUrl;
34 19
        $this->rootObjects = [];
35 19
    }
36
37
    /**
38
     * Serialize a collection.
39
     *
40
     * @param string $resourceKey
41
     * @param array $data
42
     *
43
     * @return array
44
     */
45 13
    public function collection($resourceKey, array $data) : array
46
    {
47 13
        $resources = [];
48
49 13
        foreach ($data as $resource) {
50 12
            $resources[] = $this->serializeItem($resourceKey, $resource)['data'];
51
        }
52
53 13
        return ['data' => $resources];
54
    }
55
56
    /**
57
     * Serialize an item.
58
     *
59
     * @param string $resourceKey
60
     * @param array $data
61
     *
62
     * @return array
63
     */
64 12
    public function serializeItem($resourceKey, array $data) : array
65
    {
66 12
        $id = $this->getIdFromData($data);
67
68
        $resource = [
69
            'data' => [
70 12
                'type' => $resourceKey,
71 12
                'id' => "$id",
72 12
                'attributes' => $data,
73
            ],
74
        ];
75
76 12
        unset($resource['data']['attributes']['id']);
77
78
79 12 View Code Duplication
        if (isset($resource['data']['attributes']['links'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
80
            $custom_links = $data['links'];
81
            unset($resource['data']['attributes']['links']);
82
        }
83
84 12 View Code Duplication
        if (isset($resource['data']['attributes']['meta'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
85 1
            $resource['data']['meta'] = $data['meta'];
86 1
            unset($resource['data']['attributes']['meta']);
87
        }
88
89 12
        if (empty($resource['data']['attributes'])) {
90
            $resource['data']['attributes'] = (object) [];
91
        }
92
93 12
        if ($this->shouldIncludeLinks()) {
94 5
            $resource['data']['links'] = [
95 5
                'self' => "{$this->baseUrl}/$resourceKey/$id",
96
            ];
97 5
            if (isset($custom_links)) {
98
                $resource['data']['links'] = array_merge($resource['data']['links'], $custom_links);
99
            }
100
        }
101
102 12
        return $resource;
103
    }
104
105
    /**
106
     * Serialize the paginator.
107
     *
108
     * @param PaginatorInterface $paginator
109
     *
110
     * @return array
111
     */
112
    public function serializePaginator(PaginatorInterface $paginator) : array
113
    {
114
        $currentPage = (int)$paginator->getCurrentPage();
115
        $lastPage = (int)$paginator->getLastPage();
116
117
        $pagination = [
118
            'total' => (int)$paginator->getTotal(),
119
            'count' => (int)$paginator->getCount(),
120
            'per_page' => (int)$paginator->getPerPage(),
121
            'current_page' => $currentPage,
122
            'total_pages' => $lastPage,
123
        ];
124
125
        $pagination['links'] = [];
126
127
        $pagination['links']['self'] = $paginator->getUrl($currentPage);
128
        $pagination['links']['first'] = $paginator->getUrl(1);
129
130
        if ($currentPage > 1) {
131
            $pagination['links']['prev'] = $paginator->getUrl($currentPage - 1);
132
        }
133
134
        if ($currentPage < $lastPage) {
135
            $pagination['links']['next'] = $paginator->getUrl($currentPage + 1);
136
        }
137
138
        $pagination['links']['last'] = $paginator->getUrl($lastPage);
139
140
        return ['pagination' => $pagination];
141
    }
142
143 19
    public function meta(array $meta) : array
144
    {
145 19
        if (empty($meta)) {
146 13
            return [];
147
        }
148
149 6
        $result['meta'] = $meta;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
150
151 6
        if (array_key_exists('pagination', $result['meta'])) {
152 3
            $result['links'] = $result['meta']['pagination']['links'];
153 3
            unset($result['meta']['pagination']['links']);
154
        }
155
156 6
        return $result;
157
    }
158
159 1
    public function null() : array
160
    {
161
        return [
162 1
            'data' => null,
163
        ];
164
    }
165
166 19
    public function includedData(ResourceInterface $resource, array $data) : array
167
    {
168 19
        list($serializedData, $linkedIds) = $this->pullOutNestedIncludedData($data);
169
170 19
        foreach ($data as $value) {
171 19
            foreach ($value as $includeObject) {
172 7
                if ($this->isNull($includeObject) || $this->isEmpty($includeObject)) {
173 3
                    continue;
174
                }
175
176 5
                $includeObjects = $this->createIncludeObjects($includeObject);
177 5
                list($serializedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey($includeObjects, $linkedIds, $serializedData);
178
            }
179
        }
180
181 19
        return empty($serializedData) ? [] : ['included' => $serializedData];
182
    }
183
184
    /**
185
     * Indicates if includes should be side-loaded.
186
     *
187
     * @return bool
188
     */
189 19
    public function sideloadIncludes()
190
    {
191 19
        return true;
192
    }
193
194
    /**
195
     * @param array $data
196
     * @param array $includedData
197
     *
198
     * @return array
199
     */
200 19
    public function injectData($data, $includedData) : array
201
    {
202 19
        $relationships = $this->parseRelationships($includedData);
203
204 19
        if (!empty($relationships)) {
205 7
            $data = $this->fillRelationships($data, $relationships);
206
        }
207
208 19
        return $data;
209
    }
210
211
    /**
212
     * Hook to manipulate the final sideloaded includes.
213
     * The JSON API specification does not allow the root object to be included
214
     * into the sideloaded `included`-array. We have to make sure it is
215
     * filtered out, in case some object links to the root object in a
216
     * relationship.
217
     *
218
     * @param array $includedData
219
     * @param array $data
220
     *
221
     * @return array
222
     */
223 19
    public function filterIncludes($includedData, $data) : array
224
    {
225 19
        if (!isset($includedData['included'])) {
226 16
            return $includedData;
227
        }
228
229
        // Create the RootObjects
230 3
        $this->createRootObjects($data);
231
232
        // Filter out the root objects
233 3
        $filteredIncludes = array_filter($includedData['included'], [$this, 'filterRootObject']);
234
235
        // Reset array indices
236 3
        $includedData['included'] = array_merge([], $filteredIncludes);
237
238 3
        return $includedData;
239
    }
240
241
    /**
242
     * Get the mandatory fields for the serializer
243
     *
244
     * @return array
245
     */
246 2
    public function getMandatoryFields() : array
247
    {
248 2
        return ['id'];
249
    }
250
251
    /**
252
     * Filter function to delete root objects from array.
253
     *
254
     * @param array $object
255
     *
256
     * @return bool
257
     */
258 3
    protected function filterRootObject($object) : bool
259
    {
260 3
        return !$this->isRootObject($object);
261
    }
262
263
    /**
264
     * Set the root objects of the JSON API tree.
265
     *
266
     * @param array $objects
267
     */
268 3
    protected function setRootObjects(array $objects = []) : void
269
    {
270
        $this->rootObjects = array_map(function ($object) {
271 3
            return "{$object['type']}:{$object['id']}";
272 3
        }, $objects);
273 3
    }
274
275
    /**
276
     * Determines whether an object is a root object of the JSON API tree.
277
     *
278
     * @param array $object
279
     *
280
     * @return bool
281
     */
282 3
    protected function isRootObject($object) : bool
283
    {
284 3
        $objectKey = "{$object['type']}:{$object['id']}";
285 3
        return in_array($objectKey, $this->rootObjects);
286
    }
287
288
    /**
289
     * @param array|null $data
290
     *
291
     * @return bool
292
     */
293 11 View Code Duplication
    protected function isCollection($data) : bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
294
    {
295 11
        if ($data === null) {
296
            return false;
297
        }
298
299 11
        return array_key_exists('data', $data) &&
300 11
        array_key_exists(0, $data['data']);
301
    }
302
303
    /**
304
     * @param array|null $data
305
     *
306
     * @return bool
307
     */
308 7 View Code Duplication
    protected function isNull($data) : bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
309
    {
310 7
        if ($data === null) {
311
            return true;
312
        }
313
314 7
        return array_key_exists('data', $data) && $data['data'] === null;
315
    }
316
317
    /**
318
     * @param array|null $data
319
     *
320
     * @return bool
321
     */
322 6 View Code Duplication
    protected function isEmpty($data) : bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
323
    {
324 6
        if ($data === null) {
325
            return true;
326
        }
327
328 6
        return array_key_exists('data', $data) && $data['data'] === [];
329
    }
330
331
    /**
332
     * @param array $data
333
     * @param array $relationships
334
     *
335
     * @return array
336
     */
337 7
    protected function fillRelationships($data, $relationships) : array
338
    {
339 7
        if ($this->isCollection($data)) {
340 3
            foreach ($relationships as $key => $relationship) {
341 3
                $data = $this->fillRelationshipAsCollection($data, $relationship, $key);
342
            }
343
        } else { // Single resource
344 4
            foreach ($relationships as $key => $relationship) {
345 4
                $data = $this->fillRelationshipAsSingleResource($data, $relationship, $key);
346
            }
347
        }
348
349 7
        return $data;
350
    }
351
352
    /**
353
     * @param array $includedData
354
     *
355
     * @return array
356
     */
357 19
    protected function parseRelationships($includedData) : array
358
    {
359 19
        $relationships = [];
360
361 19
        foreach ($includedData as $key => $inclusion) {
362 19
            foreach ($inclusion as $includeKey => $includeObject) {
363 7
                $relationships = $this->buildRelationships($includeKey, $relationships, $includeObject, $key);
364 7
                if (isset($includedData[0][$includeKey]['meta'])) {
365
                    $relationships[$includeKey][0]['meta'] = $includedData[0][$includeKey]['meta'];
366
                }
367
            }
368
        }
369
370 19
        return $relationships;
371
    }
372
373
    /**
374
     * @param array $data
375
     *
376
     * @return integer
377
     */
378 12
    protected function getIdFromData(array $data) : int
379
    {
380 12
        if (!array_key_exists('id', $data)) {
381
            throw new InvalidArgumentException(
382
                'JSON API resource objects MUST have a valid id'
383
            );
384
        }
385 12
        return $data['id'];
386
    }
387
388
    /**
389
     * Keep all sideloaded inclusion data on the top level.
390
     *
391
     * @param array $data
392
     *
393
     * @return array
394
     */
395 19
    protected function pullOutNestedIncludedData(array $data) : array
396
    {
397 19
        $includedData = [];
398 19
        $linkedIds = [];
399
400 19
        foreach ($data as $value) {
401 19
            foreach ($value as $includeObject) {
402 7
                if (isset($includeObject['included'])) {
403
                    list($includedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey($includeObject['included'], $linkedIds, $includedData);
404
                }
405
            }
406
        }
407
408 19
        return [$includedData, $linkedIds];
409
    }
410
411
    /**
412
     * Whether or not the serializer should include `links` for resource objects.
413
     *
414
     * @return bool
415
     */
416 18
    protected function shouldIncludeLinks() : bool
417
    {
418 18
        return $this->baseUrl !== null;
419
    }
420
421
    /**
422
     * Check if the objects are part of a collection or not
423
     *
424
     * @param array $includeObject
425
     *
426
     * @return array
427
     */
428 5
    private function createIncludeObjects($includeObject) : array
429
    {
430 5
        if ($this->isCollection($includeObject)) {
431 5
            $includeObjects = $includeObject['data'];
432
433 5
            return $includeObjects;
434
        } else {
435
            $includeObjects = [$includeObject['data']];
436
437
            return $includeObjects;
438
        }
439
    }
440
441
    /**
442
     * Sets the RootObjects, either as collection or not.
443
     *
444
     * @param array $data
445
     */
446 3
    private function createRootObjects(array $data) : void
447
    {
448 3
        if ($this->isCollection($data)) {
449 3
            $this->setRootObjects($data['data']);
450
        } else {
451
            $this->setRootObjects([$data['data']]);
452
        }
453 3
    }
454
455 3
    private function fillRelationshipAsCollection($data, $relationship, $key) : array
456
    {
457 3
        foreach ($relationship as $index => $relationshipData) {
458 3
            $data['data'][$index]['relationships'][$key] = $relationshipData;
459
        }
460
461 3
        return $data;
462
    }
463
464 4
    private function fillRelationshipAsSingleResource($data, $relationship, $key) : array
465
    {
466 4
        $data['data']['relationships'][$key] = $relationship[0];
467
468 4
        return $data;
469
    }
470
471
    /**
472
     * @param mixed $includeKey
473
     * @param array $relationships
474
     * @param array|null $includeObject
475
     * @param string $key
476
     *
477
     * @return array
478
     */
479 7
    private function buildRelationships($includeKey, array $relationships, array $includeObject,  string $key) : array
480
    {
481 7
        $relationships = $this->addIncludekeyToRelationsIfNotSet($includeKey, $relationships);
482
483 7
        if ($this->isNull($includeObject)) {
484 1
            $relationship = $this->null();
485 6
        } elseif ($this->isEmpty($includeObject)) {
486
            $relationship = [
487 2
                'data' => [],
488
            ];
489 5
        } elseif ($includeObject && $this->isCollection($includeObject)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $includeObject of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
490 5
            $relationship = ['data' => []];
491 5
            $relationship = $this->addIncludedDataToRelationship($includeObject, $relationship);
492
        } else {
493
            $relationship = [
494
                'data' => [
495
                    'type' => $includeObject['data']['type'],
496
                    'id' => $includeObject['data']['id'],
497
                ],
498
            ];
499
        }
500
501 7
        $relationships[$includeKey][$key] = $relationship;
502
503 7
        return $relationships;
504
    }
505
506
    /**
507
     * @param mixed $includeKey
508
     * @param array $relationships
509
     *
510
     * @return array
511
     */
512 7
    private function addIncludekeyToRelationsIfNotSet($includeKey, array $relationships) : array
513
    {
514 7
        if (!array_key_exists($includeKey, $relationships)) {
515 7
            $relationships[$includeKey] = [];
516 7
            return $relationships;
517
        }
518
519 3
        return $relationships;
520
    }
521
522
    /**
523
     * @param array $includeObject
524
     * @param array $relationship
525
     *
526
     * @return array
527
     */
528 5
    private function addIncludedDataToRelationship(array $includeObject, array $relationship) : array
529
    {
530 5
        foreach ($includeObject['data'] as $object) {
531 5
            $relationship['data'][] = [
532 5
                'type' => $object['type'],
533 5
                'id' => $object['id'],
534
            ];
535
        }
536
537 5
        return $relationship;
538
    }
539
540 18
    public function injectAvailableIncludeData($data, $availableIncludes) : array
541
    {
542 18
        if (!$this->shouldIncludeLinks()) {
543 13
            return $data;
544
        }
545
546 5
        if ($this->isCollection($data)) {
547
            $data['data'] = array_map(function ($resource) use ($availableIncludes) {
548 5
                foreach ($availableIncludes as $relationshipKey) {
549 5
                    $resource = $this->addRelationshipLinks($resource, $relationshipKey);
550
                }
551 5
                return $resource;
552 5
            }, $data['data']);
553
        } else {
554
            foreach ($availableIncludes as $relationshipKey) {
555
                $data['data'] = $this->addRelationshipLinks($data['data'], $relationshipKey);
556
            }
557
        }
558
559 5
        return $data;
560
    }
561
562 5
    private function addRelationshipLinks(array $resource, string $relationshipKey) : array
563
    {
564 5
        if (!isset($resource['relationships']) || !isset($resource['relationships'][$relationshipKey])) {
565 5
            $resource['relationships'][$relationshipKey] = [];
566
        }
567
568 5
        $resource['relationships'][$relationshipKey] = array_merge(
569
            [
570
                'links' => [
571 5
                    'self'   => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/relationships/{$relationshipKey}",
572 5
                    'related' => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/{$relationshipKey}",
573
                ]
574
            ],
575 5
            $resource['relationships'][$relationshipKey]
576
        );
577
578 5
        return $resource;
579
    }
580
581 5
    private function serializeIncludedObjectsWithCacheKey(array $includeObjects, array $linkedIds, array $serializedData) : array
582
    {
583 5
        foreach ($includeObjects as $object) {
584 5
            $includeType = $object['type'];
585 5
            $includeId = $object['id'];
586 5
            $cacheKey = "$includeType:$includeId";
587 5
            if (!array_key_exists($cacheKey, $linkedIds)) {
588 5
                $serializedData[] = $object;
589 5
                $linkedIds[$cacheKey] = $object;
590
            }
591
        }
592 5
        return [$serializedData, $linkedIds];
593
    }
594
}
595