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