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

JsonApiSerializer::areResourceLinksSet()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 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
    /** @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 ($this->areResourceLinksSet($resource)) {
75 1
            $custom_links = $data['links'];
76 1
            unset($resource['data']['attributes']['links']);
77
        }
78
79 36
        if ($this->isResourceMetaSet($resource)) {
80 2
            $resource['data']['meta'] = $data['meta'];
81 2
            unset($resource['data']['attributes']['meta']);
82
        }
83
84 36
        if ($this->isDataAttributesEmpty($resource)) {
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
        $result = [];
141
142 36
        if (empty($meta)) {
143 30
            return [];
144
        }
145
146 7
        $result['meta'] = $meta;
147
148 7
        if (array_key_exists('pagination', $result['meta'])) {
149 3
            $result['links'] = $result['meta']['pagination']['links'];
150 3
            unset($result['meta']['pagination']['links']);
151
        }
152
153 7
        return $result;
154
    }
155
156 2
    public function null(): array
157
    {
158
        return [
159 2
            'data' => null,
160
        ];
161
    }
162
163 36
    public function includedData(ResourceInterface $resource, array $data): array
164
    {
165 36
        list($serializedData, $linkedIds) = $this->pullOutNestedIncludedData($data);
166
167 36
        foreach ($data as $value) {
168 36
            foreach ($value as $includeObject) {
169 22
                if ($this->isNull($includeObject) || $this->isEmpty($includeObject)) {
170 4
                    continue;
171
                }
172
173 20
                $includeObjects = $this->createIncludeObjects($includeObject);
174 20
                list($serializedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey($includeObjects, $linkedIds, $serializedData);
175
            }
176
        }
177
178 36
        return empty($serializedData) ? [] : ['included' => $serializedData];
179
    }
180
181
    /**
182
     * Indicates if includes should be side-loaded.
183
     *
184
     * @return bool
185
     */
186 37
    public function sideloadIncludes()
187
    {
188 37
        return true;
189
    }
190
191
    /**
192
     * @param array $data
193
     * @param array $includedData
194
     *
195
     * @return array
196
     */
197 36
    public function injectData($data, $includedData): array
198
    {
199 36
        $relationships = $this->parseRelationships($includedData);
200
201 36
        if (!empty($relationships)) {
202 22
            $data = $this->fillRelationships($data, $relationships);
203
        }
204
205 36
        return $data;
206
    }
207
208
    /**
209
     * Hook to manipulate the final sideloaded includes.
210
     * The JSON API specification does not allow the root object to be included
211
     * into the sideloaded `included`-array. We have to make sure it is
212
     * filtered out, in case some object links to the root object in a
213
     * relationship.
214
     *
215
     * @param array $includedData
216
     * @param array $data
217
     *
218
     * @return array
219
     */
220 36
    public function filterIncludes($includedData, $data): array
221
    {
222 36
        if (!isset($includedData['included'])) {
223 18
            return $includedData;
224
        }
225
226
        // Create the RootObjects
227 18
        $this->createRootObjects($data);
228
229
        // Filter out the root objects
230 18
        $filteredIncludes = array_filter($includedData['included'], [$this, 'filterRootObject']);
231
232
        // Reset array indices
233 18
        $includedData['included'] = array_merge([], $filteredIncludes);
234
235 18
        return $includedData;
236
    }
237
238
    /**
239
     * Get the mandatory fields for the serializer
240
     *
241
     * @return array
242
     */
243 4
    public function getMandatoryFields() : array
244
    {
245 4
        return ['id'];
246
    }
247
248
    /**
249
     * Filter function to delete root objects from array.
250
     *
251
     * @param array $object
252
     *
253
     * @return bool
254
     */
255 18
    protected function filterRootObject($object): bool
256
    {
257 18
        return !$this->isRootObject($object);
258
    }
259
260
    /**
261
     * Set the root objects of the JSON API tree.
262
     *
263
     * @param array $objects
264
     */
265 18
    protected function setRootObjects(array $objects = []): void
266
    {
267
        $this->rootObjects = array_map(function ($object) {
268 18
            return "{$object['type']}:{$object['id']}";
269 18
        }, $objects);
270 18
    }
271
272
    /**
273
     * Determines whether an object is a root object of the JSON API tree.
274
     *
275
     * @param array $object
276
     *
277
     * @return bool
278
     */
279 18
    protected function isRootObject($object): bool
280
    {
281 18
        $objectKey = "{$object['type']}:{$object['id']}";
282 18
        return in_array($objectKey, $this->rootObjects);
283
    }
284
285
    /**
286
     * @param array|null $data
287
     *
288
     * @return bool
289
     */
290 29 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...
291
    {
292 29
        if ($data === null) {
293
            return false;
294
        }
295
296 29
        return array_key_exists('data', $data) &&
297 29
        array_key_exists(0, $data['data']);
298
    }
299
300
    /**
301
     * @param array|null $data
302
     *
303
     * @return bool
304
     */
305 22 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...
306
    {
307 22
        if ($data === null) {
308
            return true;
309
        }
310
311 22
        return array_key_exists('data', $data) && $data['data'] === null;
312
    }
313
314
    /**
315
     * @param array|null $data
316
     *
317
     * @return bool
318
     */
319 21 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...
320
    {
321 21
        if ($data === null) {
322
            return true;
323
        }
324
325 21
        return array_key_exists('data', $data) && $data['data'] === [];
326
    }
327
328
    /**
329
     * @param array $data
330
     * @param array $relationships
331
     *
332
     * @return array
333
     */
334 22
    protected function fillRelationships($data, $relationships): array
335
    {
336 22
        if ($this->isCollection($data)) {
337 9
            foreach ($relationships as $key => $relationship) {
338 9
                $data = $this->fillRelationshipAsCollection($data, $relationship, $key);
339
            }
340
        } else { // Single resource
341 15
            foreach ($relationships as $key => $relationship) {
342 15
                $data = $this->fillRelationshipAsSingleResource($data, $relationship, $key);
343
            }
344
        }
345
346 22
        return $data;
347
    }
348
349
    /**
350
     * @param array $includedData
351
     *
352
     * @return array
353
     */
354 36
    protected function parseRelationships($includedData): array
355
    {
356 36
        $relationships = [];
357
358 36
        foreach ($includedData as $key => $inclusion) {
359 36
            foreach ($inclusion as $includeKey => $includeObject) {
360 22
                $relationships = $this->buildRelationships($includeKey, $relationships, $includeObject, $key);
361 22
                if (isset($includedData[0][$includeKey]['meta'])) {
362 1
                    $relationships[$includeKey][0]['meta'] = $includedData[0][$includeKey]['meta'];
363
                }
364
            }
365
        }
366
367 36
        return $relationships;
368
    }
369
370
    /**
371
     * @param array $data
372
     *
373
     * @return integer
374
     */
375 37
    protected function getIdFromData(array $data): int
376
    {
377 37
        if (!array_key_exists('id', $data)) {
378 1
            throw new InvalidArgumentException(
379 1
                'JSON API resource objects MUST have a valid id'
380
            );
381
        }
382 36
        return $data['id'];
383
    }
384
385
    /**
386
     * Keep all sideloaded inclusion data on the top level.
387
     *
388
     * @param array $data
389
     *
390
     * @return array
391
     */
392 36
    protected function pullOutNestedIncludedData(array $data): array
393
    {
394 36
        $includedData = [];
395 36
        $linkedIds = [];
396
397 36
        foreach ($data as $value) {
398 36
            foreach ($value as $includeObject) {
399 22
                if (isset($includeObject['included'])) {
400 4
                    list($includedData, $linkedIds) = $this->serializeIncludedObjectsWithCacheKey($includeObject['included'], $linkedIds, $includedData);
401
                }
402
            }
403
        }
404
405 36
        return [$includedData, $linkedIds];
406
    }
407
408
    /**
409
     * Whether or not the serializer should include `links` for resource objects.
410
     *
411
     * @return bool
412
     */
413 36
    protected function shouldIncludeLinks(): bool
414
    {
415 36
        return $this->baseUrl !== null;
416
    }
417
418
    /**
419
     * Check if the objects are part of a collection or not
420
     *
421
     * @param array $includeObject
422
     *
423
     * @return array
424
     */
425 20
    private function createIncludeObjects($includeObject): array
426
    {
427 20
        if ($this->isCollection($includeObject)) {
428 11
            $includeObjects = $includeObject['data'];
429
430 11
            return $includeObjects;
431
        } else {
432 13
            $includeObjects = [$includeObject['data']];
433
434 13
            return $includeObjects;
435
        }
436
    }
437
438
    /**
439
     * Sets the RootObjects, either as collection or not.
440
     *
441
     * @param array $data
442
     */
443 18
    private function createRootObjects(array $data): void
444
    {
445 18
        if ($this->isCollection($data)) {
446 8
            $this->setRootObjects($data['data']);
447
        } else {
448 10
            $this->setRootObjects([$data['data']]);
449
        }
450 18
    }
451
452 9
    private function fillRelationshipAsCollection($data, $relationship, $key): array
453
    {
454 9
        foreach ($relationship as $index => $relationshipData) {
455 9
            $data['data'][$index]['relationships'][$key] = $relationshipData;
456
        }
457
458 9
        return $data;
459
    }
460
461 15
    private function fillRelationshipAsSingleResource($data, $relationship, $key): array
462
    {
463 15
        $data['data']['relationships'][$key] = $relationship[0];
464
465 15
        return $data;
466
    }
467
468 22
    private function buildRelationships(string $includeKey, array $relationships, array $includeObject, string $key): array
469
    {
470 22
        $relationships = $this->addIncludeKeyToRelationsIfNotSet($includeKey, $relationships);
471
472 22
        if ($this->isNull($includeObject)) {
473 2
            $relationship = $this->null();
474 21
        } elseif ($this->isEmpty($includeObject)) {
475
            $relationship = [
476 2
                'data' => [],
477
            ];
478 20
        } elseif (! empty($includeObject) && $this->isCollection($includeObject)) {
479 11
            $relationship = ['data' => []];
480 11
            $relationship = $this->addIncludedDataToRelationship($includeObject, $relationship);
481
        } else {
482
            $relationship = [
483
                'data' => [
484 13
                    'type' => $includeObject['data']['type'],
485 13
                    'id' => $includeObject['data']['id'],
486
                ],
487
            ];
488
        }
489
490 22
        $relationships[$includeKey][$key] = $relationship;
491
492 22
        return $relationships;
493
    }
494
495 22
    private function addIncludeKeyToRelationsIfNotSet(string $includeKey, array $relationships): array
496
    {
497 22
        if (!array_key_exists($includeKey, $relationships)) {
498 22
            $relationships[$includeKey] = [];
499 22
            return $relationships;
500
        }
501
502 9
        return $relationships;
503
    }
504
505
    /**
506
     * @param array $includeObject
507
     * @param array $relationship
508
     *
509
     * @return array
510
     */
511 11
    private function addIncludedDataToRelationship(array $includeObject, array $relationship) : array
512
    {
513 11
        foreach ($includeObject['data'] as $object) {
514 11
            $relationship['data'][] = [
515 11
                'type' => $object['type'],
516 11
                'id' => $object['id'],
517
            ];
518
        }
519
520 11
        return $relationship;
521
    }
522
523 35
    public function injectAvailableIncludeData($data, $availableIncludes): array
524
    {
525 35
        if (!$this->shouldIncludeLinks()) {
526 24
            return $data;
527
        }
528
529 11
        if ($this->isCollection($data)) {
530
            $data['data'] = array_map(function ($resource) use ($availableIncludes) {
531 7
                foreach ($availableIncludes as $relationshipKey) {
532 7
                    $resource = $this->addRelationshipLinks($resource, $relationshipKey);
533
                }
534 7
                return $resource;
535 7
            }, $data['data']);
536
        } else {
537 6
            foreach ($availableIncludes as $relationshipKey) {
538 6
                $data['data'] = $this->addRelationshipLinks($data['data'], $relationshipKey);
539
            }
540
        }
541
542 11
        return $data;
543
    }
544
545 11
    private function addRelationshipLinks(array $resource, string $relationshipKey): array
546
    {
547 11
        if (!isset($resource['relationships']) || !isset($resource['relationships'][$relationshipKey])) {
548 11
            $resource['relationships'][$relationshipKey] = [];
549
        }
550
551 11
        $resource['relationships'][$relationshipKey] = array_merge(
552
            [
553
                'links' => [
554 11
                    'self'   => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/relationships/{$relationshipKey}",
555 11
                    'related' => "{$this->baseUrl}/{$resource['type']}/{$resource['id']}/{$relationshipKey}",
556
                ]
557
            ],
558 11
            $resource['relationships'][$relationshipKey]
559
        );
560
561 11
        return $resource;
562
    }
563
564 20
    private function serializeIncludedObjectsWithCacheKey(array $includeObjects, array $linkedIds, array $serializedData): array
565
    {
566 20
        foreach ($includeObjects as $object) {
567 20
            $includeType = $object['type'];
568 20
            $includeId = $object['id'];
569 20
            $cacheKey = "$includeType:$includeId";
570 20
            if (!array_key_exists($cacheKey, $linkedIds)) {
571 20
                $serializedData[] = $object;
572 20
                $linkedIds[$cacheKey] = $object;
573
            }
574
        }
575 20
        return [$serializedData, $linkedIds];
576
    }
577
578 36
    private function areResourceLinksSet($resource): bool
579
    {
580 36
        return isset($resource['data']['attributes']['links'])?: false;
581
    }
582
583 36
    private function isResourceMetaSet($resource): bool
584
    {
585 36
        return isset($resource['data']['attributes']['meta'])?: false;
586
    }
587
588 36
    private function isDataAttributesEmpty($resource) : bool
589
    {
590 36
        return empty($resource['data']['attributes'])?: false;
591
    }
592
}
593