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