Completed
Pull Request — master (#1)
by Matt
02:40
created

JsonApiSerializer::pullOutNestedIncludedData()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.0312

Importance

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