Completed
Pull Request — master (#280)
by
unknown
02:42
created

JsonApiSerializer::pullOutNestedIncludedData()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 24
Code Lines 14

Duplication

Lines 10
Ratio 41.67 %

Code Coverage

Tests 19
CRAP Score 6

Importance

Changes 6
Bugs 3 Features 2
Metric Value
c 6
b 3
f 2
dl 10
loc 24
ccs 19
cts 19
cp 1
rs 8.5125
cc 6
eloc 14
nc 6
nop 1
crap 6
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 27
    public function __construct($baseUrl = null)
24
    {
25 27
        $this->baseUrl = $baseUrl;
26 27
        $this->rootObjects = [];
27 27
    }
28
29
    /**
30
     * Serialize a collection.
31
     *
32
     * @param string $resourceKey
33
     * @param array  $data
34
     *
35
     * @return array
36
     */
37 19
    public function collection($resourceKey, array $data)
38
    {
39 19
        $resources = [];
40
41 19
        foreach ($data as $resource) {
42 18
            $resources[] = $this->item($resourceKey, $resource)['data'];
43 19
        }
44
45 19
        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 27
    public function item($resourceKey, array $data)
57
    {
58 27
        $id = $this->getIdFromData($data);
59
60
        $resource = [
61
            'data' => [
62 26
                'type' => $resourceKey,
63 26
                'id' => "$id",
64 26
                'attributes' => $data,
65 26
            ],
66 26
        ];
67
68 26
        unset($resource['data']['attributes']['id']);
69
70 26
        if ($this->shouldIncludeLinks()) {
71 9
            $resource['data']['links'] = [
72 9
                'self' => "{$this->baseUrl}/$resourceKey/$id",
73
            ];
74 9
        }
75
76 26
        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 26
    public function meta(array $meta)
125
    {
126 26
        if (empty($meta)) {
127 21
            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 26
    public function includedData(ResourceInterface $resource, array $data)
159
    {
160 26
        list($serializedData, $linkedIds) = $this->pullOutNestedIncludedData($data);
161
162 26
        foreach ($data as $value) {
163 26
            foreach ($value as $includeObject) {
164 17
                if ($this->isNull($includeObject) || $this->isEmpty($includeObject)) {
165 4
                    continue;
166
                }
167 15
                if ($this->isCollection($includeObject)) {
168 8
                    $includeObjects = $includeObject['data'];
169 8
                } else {
170 10
                    $includeObjects = [$includeObject['data']];
171
                }
172
173 15 View Code Duplication
                foreach ($includeObjects as $object) {
174 15
                    $includeType = $object['type'];
175 15
                    $includeId = $object['id'];
176 15
                    $cacheKey = "$includeType:$includeId";
177 15
                    if (!array_key_exists($cacheKey, $linkedIds)) {
178 15
                        $serializedData[] = $object;
179 15
                        $linkedIds[$cacheKey] = $object;
180 15
                    }
181 15
                }
182 26
            }
183 26
        }
184
185 26
        return empty($serializedData) ? [] : ['included' => $serializedData];
186
    }
187
188
    /**
189
     * Indicates if includes should be side-loaded.
190
     *
191
     * @return bool
192
     */
193 27
    public function sideloadIncludes()
194
    {
195 27
        return true;
196
    }
197
198
    /**
199
     * @param array $data
200
     * @param array $includedData
201
     *
202
     * @return array
203
     */
204 26
    public function injectData($data, $includedData)
205
    {
206 26
        $relationships = $this->parseRelationships($includedData);
207
208 26
        if (!empty($relationships)) {
209 17
            $data = $this->fillRelationships($data, $relationships);
210 17
        }
211
212 26
        return $data;
213
    }
214
215
    /**
216
     * Hook to manipulate the final sideloaded includes.
217
     *
218
     * The JSON API specification does not allow the root object to be included
219
     * into the sideloaded `included`-array. We have to make sure it is
220
     * filtered out, in case some object links to the root object in a
221
     * relationship.
222
     *
223
     * @param array             $includedData
224
     * @param array             $data
225
     *
226
     * @return array
227
     */
228 26
    public function filterIncludes($includedData, $data)
229
    {
230 26
        if (!isset($includedData['included'])) {
231 11
            return $includedData;
232
        }
233
234 15
        if ($this->isCollection($data)) {
235 8
            $this->setRootObjects($data['data']);
236 8
        } else {
237 7
            $this->setRootObjects([$data['data']]);
238
        }
239
240
        // Filter out the root objects
241 15
        $filteredIncludes = array_filter($includedData['included'], [$this, 'filterRootObject']);
242
243
        // Reset array indizes
244 15
        $includedData['included'] = array_merge([], $filteredIncludes);
245
246 15
        return $includedData;
247
    }
248
249
    /**
250
     * Filter function to delete root objects from array.
251
     *
252
     * @param array $object
253
     *
254
     * @return bool
255
     */
256 15
    protected function filterRootObject($object)
257
    {
258 15
        return !$this->isRootObject($object);
259
    }
260
261
    /**
262
     * Set the root objects of the JSON API tree.
263
     *
264
     * @param array $objects
265
     */
266
    protected function setRootObjects(array $objects = [])
267
    {
268 15
        $this->rootObjects = array_map(function ($object) {
269 15
            return "{$object['type']}:{$object['id']}";
270 15
        }, $objects);
271 15
    }
272
273
    /**
274
     * Determines whether an object is a root object of the JSON API tree.
275
     *
276
     * @param array $object
277
     *
278
     * @return bool
279
     */
280 15
    protected function isRootObject($object)
281
    {
282 15
        $objectKey = "{$object['type']}:{$object['id']}";
283 15
        return in_array($objectKey, $this->rootObjects);
284
    }
285
286
    /**
287
     * @param array $data
288
     *
289
     * @return bool
290
     */
291 17
    protected function isCollection($data)
292
    {
293 17
        return array_key_exists('data', $data) &&
294 17
               array_key_exists(0, $data['data']);
295
    }
296
297
    /**
298
     * @param array $data
299
     *
300
     * @return bool
301
     */
302 17
    protected function isNull($data)
303
    {
304 17
        return array_key_exists('data', $data) && $data['data'] === null;
305
    }
306
307
    /**
308
     * @param array $data
309
     *
310
     * @return bool
311
     */
312 16
    protected function isEmpty($data)
313
    {
314 16
        return array_key_exists('data', $data) && $data['data'] === [];
315
    }
316
317
    /**
318
     * @param array $data
319
     * @param array $relationships
320
     *
321
     * @return mixed
322
     */
323 17
    protected function fillRelationships($data, $relationships)
324
    {
325 17
        if ($this->isCollection($data)) {
326 9
            foreach ($relationships as $key => $relationship) {
327 9
                foreach ($relationship as $index => $relationshipData) {
328 9
                    $data['data'][$index]['relationships'][$key] = $relationshipData;
329
330 9
                    if ($this->shouldIncludeLinks()) {
331 2
                        $data['data'][$index]['relationships'][$key] = array_merge([
332
                            'links' => [
333 2
                                'self' => "{$this->baseUrl}/{$data['data'][$index]['type']}/{$data['data'][$index]['id']}/relationships/$key",
334 2
                                'related' => "{$this->baseUrl}/{$data['data'][$index]['type']}/{$data['data'][$index]['id']}/$key",
335 2
                            ],
336 2
                        ], $data['data'][$index]['relationships'][$key]);
337 2
                    }
338 9
                }
339 9
            }
340 9
        } else { // Single resource
341 10
            foreach ($relationships as $key => $relationship) {
342 10
                $data['data']['relationships'][$key] = $relationship[0];
343
344 10
                if ($this->shouldIncludeLinks()) {
345 2
                    $data['data']['relationships'][$key] = array_merge([
346
                        'links' => [
347 2
                            'self' => "{$this->baseUrl}/{$data['data']['type']}/{$data['data']['id']}/relationships/$key",
348 2
                            'related' => "{$this->baseUrl}/{$data['data']['type']}/{$data['data']['id']}/$key",
349 2
                        ],
350 2
                    ], $data['data']['relationships'][$key]);
351 2
                }
352 10
            }
353
        }
354
355 17
        return $data;
356
    }
357
358
    /**
359
     * @param array $includedData
360
     *
361
     * @return array
362
     */
363 26
    protected function parseRelationships($includedData)
364
    {
365 26
        $relationships = [];
366
367 26
        foreach ($includedData as $key => $inclusion) {
368 26
            foreach ($inclusion as $includeKey => $includeObject) {
369 17
                if (!array_key_exists($includeKey, $relationships)) {
370 17
                    $relationships[$includeKey] = [];
371 17
                }
372
373 17
                if ($this->isNull($includeObject)) {
374 2
                    $relationship = $this->null();
375 17
                } elseif ($this->isEmpty($includeObject)) {
376
                    $relationship = [
377 2
                        'data' => [],
378 2
                    ];
379 16
                } elseif ($this->isCollection($includeObject)) {
380 8
                    $relationship = ['data' => []];
381
382 8
                    foreach ($includeObject['data'] as $object) {
383 8
                        $relationship['data'][] = [
384 8
                            'type' => $object['type'],
385 8
                            'id' => $object['id'],
386
                        ];
387 8
                    }
388 8
                } else {
389
                    $relationship = [
390
                        'data' => [
391 10
                            'type' => $includeObject['data']['type'],
392 10
                            'id' => $includeObject['data']['id'],
393 10
                        ],
394 10
                    ];
395
                }
396
397 17
                $relationships[$includeKey][$key] = $relationship;
398 26
            }
399 26
        }
400
401 26
        return $relationships;
402
    }
403
404
    /**
405
     * @param array $data
406
     *
407
     * @return mixed
408
     */
409 27
    protected function getIdFromData(array $data)
410
    {
411 27
        if (!array_key_exists('id', $data)) {
412 1
            throw new InvalidArgumentException(
413
                'JSON API resource objects MUST have a valid id'
414 1
            );
415
        }
416 26
        return $data['id'];
417
    }
418
419
    /**
420
     * Keep all sideloaded inclusion data on the top level.
421
     *
422
     * @param array $data
423
     *
424
     * @return array
425
     */
426 26
    protected function pullOutNestedIncludedData(array $data)
427
    {
428 26
        $includedData = [];
429 26
        $linkedIds = [];
430
431 26
        foreach ($data as $value) {
432 26
            foreach ($value as $includeObject) {
433 17
                if (isset($includeObject['included'])) {
434 3 View Code Duplication
                    foreach ($includeObject['included'] as $object) {
435 3
                        $includeType = $object['type'];
436 3
                        $includeId = $object['id'];
437 3
                        $cacheKey = "$includeType:$includeId";
438
439 3
                        if (!array_key_exists($cacheKey, $linkedIds)) {
440 3
                            $includedData[] = $object;
441 3
                            $linkedIds[$cacheKey] = $object;
442 3
                        }
443 3
                    }
444 3
                }
445 26
            }
446 26
        }
447
448 26
        return [$includedData, $linkedIds];
449
    }
450
451
    /**
452
     * Whether or not the serializer should include `links` for resource objects.
453
     *
454
     * @return bool
455
     */
456 26
    protected function shouldIncludeLinks()
457
    {
458 26
        return $this->baseUrl !== null;
459
    }
460
}
461