1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the API Platform project. |
5
|
|
|
* |
6
|
|
|
* (c) Kévin Dunglas <[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
|
|
|
declare(strict_types=1); |
13
|
|
|
|
14
|
|
|
namespace ApiPlatform\Core\JsonApi\Serializer; |
15
|
|
|
|
16
|
|
|
use ApiPlatform\Core\Api\IriConverterInterface; |
17
|
|
|
use ApiPlatform\Core\Api\OperationType; |
18
|
|
|
use ApiPlatform\Core\Api\ResourceClassResolverInterface; |
19
|
|
|
use ApiPlatform\Core\Exception\InvalidArgumentException; |
20
|
|
|
use ApiPlatform\Core\Exception\ItemNotFoundException; |
21
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
22
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
23
|
|
|
use ApiPlatform\Core\Metadata\Property\PropertyMetadata; |
24
|
|
|
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; |
25
|
|
|
use ApiPlatform\Core\Serializer\AbstractItemNormalizer; |
26
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; |
27
|
|
|
use Symfony\Component\Serializer\NameConverter\NameConverterInterface; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Converts between objects and array. |
31
|
|
|
* |
32
|
|
|
* @author Kévin Dunglas <[email protected]> |
33
|
|
|
* @author Amrouche Hamza <[email protected]> |
34
|
|
|
* @author Baptiste Meyer <[email protected]> |
35
|
|
|
*/ |
36
|
|
|
final class ItemNormalizer extends AbstractItemNormalizer |
37
|
|
|
{ |
38
|
|
|
const FORMAT = 'jsonapi'; |
39
|
|
|
|
40
|
|
|
private $componentsCache = []; |
41
|
|
|
private $resourceMetadataFactory; |
42
|
|
|
|
43
|
|
|
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory) |
44
|
|
|
{ |
45
|
|
|
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter); |
46
|
|
|
|
47
|
|
|
$this->resourceMetadataFactory = $resourceMetadataFactory; |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* {@inheritdoc} |
52
|
|
|
*/ |
53
|
|
|
public function supportsNormalization($data, $format = null) |
54
|
|
|
{ |
55
|
|
|
return self::FORMAT === $format && parent::supportsNormalization($data, $format); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* {@inheritdoc} |
60
|
|
|
*/ |
61
|
|
|
public function normalize($object, $format = null, array $context = []) |
62
|
|
|
{ |
63
|
|
|
$context['cache_key'] = $this->getJsonApiCacheKey($format, $context); |
64
|
|
|
|
65
|
|
|
// Get and populate attributes data |
66
|
|
|
$objectAttributesData = parent::normalize($object, $format, $context); |
67
|
|
|
|
68
|
|
|
if (!is_array($objectAttributesData)) { |
69
|
|
|
return $objectAttributesData; |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
// Get and populate item type |
73
|
|
|
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true); |
74
|
|
|
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
75
|
|
|
|
76
|
|
|
// Get and populate relations |
77
|
|
|
$components = $this->getComponents($object, $format, $context); |
78
|
|
|
$objectRelationshipsData = $this->getPopulatedRelations($object, $format, $context, $components['relationships']); |
79
|
|
|
|
80
|
|
|
$item = [ |
81
|
|
|
'id' => $this->iriConverter->getIriFromItem($object), |
82
|
|
|
'type' => $resourceMetadata->getShortName(), |
83
|
|
|
]; |
84
|
|
|
|
85
|
|
|
if ($objectAttributesData) { |
|
|
|
|
86
|
|
|
$item['attributes'] = $objectAttributesData; |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
if ($objectRelationshipsData) { |
|
|
|
|
90
|
|
|
$item['relationships'] = $objectRelationshipsData; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
return ['data' => $item]; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* {@inheritdoc} |
98
|
|
|
*/ |
99
|
|
|
public function supportsDenormalization($data, $type, $format = null) |
100
|
|
|
{ |
101
|
|
|
return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* {@inheritdoc} |
106
|
|
|
*/ |
107
|
|
|
public function denormalize($data, $class, $format = null, array $context = []) |
108
|
|
|
{ |
109
|
|
|
// Avoid issues with proxies if we populated the object |
110
|
|
|
if (isset($data['data']['id']) && !isset($context[self::OBJECT_TO_POPULATE])) { |
111
|
|
View Code Duplication |
if (isset($context['api_allow_update']) && true !== $context['api_allow_update']) { |
|
|
|
|
112
|
|
|
throw new InvalidArgumentException('Update is not allowed for this operation.'); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
$context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getItemFromIri( |
116
|
|
|
$data['data']['id'], |
117
|
|
|
$context + ['fetch_data' => false] |
118
|
|
|
); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
// Merge attributes and relations previous to apply parents denormalizing |
122
|
|
|
$dataToDenormalize = array_merge( |
123
|
|
|
$data['data']['attributes'] ?? [], |
124
|
|
|
$data['data']['relationships'] ?? [] |
125
|
|
|
); |
126
|
|
|
|
127
|
|
|
return parent::denormalize( |
128
|
|
|
$dataToDenormalize, |
129
|
|
|
$class, |
130
|
|
|
$format, |
131
|
|
|
$context |
132
|
|
|
); |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* {@inheritdoc} |
137
|
|
|
*/ |
138
|
|
|
protected function getAttributes($object, $format = null, array $context) |
139
|
|
|
{ |
140
|
|
|
return $this->getComponents($object, $format, $context)['attributes']; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* {@inheritdoc} |
145
|
|
|
*/ |
146
|
|
|
protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []) |
147
|
|
|
{ |
148
|
|
|
parent::setAttributeValue($object, $attribute, is_array($value) && array_key_exists('data', $value) ? $value['data'] : $value, $format, $context); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* {@inheritdoc} |
153
|
|
|
* |
154
|
|
|
* @see http://jsonapi.org/format/#document-resource-object-linkage |
155
|
|
|
*/ |
156
|
|
|
protected function denormalizeRelation(string $attributeName, PropertyMetadata $propertyMetadata, string $className, $value, string $format = null, array $context) |
157
|
|
|
{ |
158
|
|
|
// Give a chance to other normalizers (e.g.: DateTimeNormalizer) |
159
|
|
View Code Duplication |
if (!$this->resourceClassResolver->isResourceClass($className)) { |
|
|
|
|
160
|
|
|
$context['resource_class'] = $className; |
161
|
|
|
|
162
|
|
|
return $this->serializer->denormalize($value, $className, $format, $context); |
|
|
|
|
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
if (!is_array($value) || !isset($value['id'], $value['type'])) { |
166
|
|
|
throw new InvalidArgumentException('Only resource linkage supported currently, see: http://jsonapi.org/format/#document-resource-object-linkage.'); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
try { |
170
|
|
|
return $this->iriConverter->getItemFromIri($value['id'], $context + ['fetch_data' => true]); |
171
|
|
|
} catch (ItemNotFoundException $e) { |
172
|
|
|
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); |
173
|
|
|
} |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* {@inheritdoc} |
178
|
|
|
* |
179
|
|
|
* @see http://jsonapi.org/format/#document-resource-object-linkage |
180
|
|
|
*/ |
181
|
|
|
protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, string $format = null, array $context) |
182
|
|
|
{ |
183
|
|
|
if (null === $relatedObject) { |
184
|
|
|
if (isset($context['operation_type']) && OperationType::SUBRESOURCE === $context['operation_type'] && isset($context['subresource_resources'][$resourceClass])) { |
185
|
|
|
$iri = $this->iriConverter->getItemIriFromResourceClass($resourceClass, $context['subresource_resources'][$resourceClass]); |
186
|
|
|
} else { |
187
|
|
|
unset($context['resource_class']); |
188
|
|
|
|
189
|
|
|
return $this->serializer->normalize($relatedObject, $format, $context); |
190
|
|
|
} |
191
|
|
|
} else { |
192
|
|
|
$iri = $this->iriConverter->getIriFromItem($relatedObject); |
193
|
|
|
|
194
|
|
|
if (isset($context['resources'])) { |
195
|
|
|
$context['resources'][$iri] = $iri; |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
return ['data' => [ |
200
|
|
|
'type' => $this->resourceMetadataFactory->create($resourceClass)->getShortName(), |
201
|
|
|
'id' => $iri, |
202
|
|
|
]]; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* {@inheritdoc} |
207
|
|
|
*/ |
208
|
|
|
protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = []) |
209
|
|
|
{ |
210
|
|
|
return preg_match('/^\\w[-\\w_]*$/', $attribute) && parent::isAllowedAttribute($classOrObject, $attribute, $format, $context); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Gets JSON API components of the resource: attributes, relationships, meta and links. |
215
|
|
|
* |
216
|
|
|
* @param object $object |
217
|
|
|
* @param string|null $format |
218
|
|
|
* @param array $context |
219
|
|
|
* |
220
|
|
|
* @return array |
221
|
|
|
*/ |
222
|
|
|
private function getComponents($object, string $format = null, array $context) |
223
|
|
|
{ |
224
|
|
|
if (isset($this->componentsCache[$context['cache_key']])) { |
225
|
|
|
return $this->componentsCache[$context['cache_key']]; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
$attributes = parent::getAttributes($object, $format, $context); |
|
|
|
|
229
|
|
|
|
230
|
|
|
$options = $this->getFactoryOptions($context); |
231
|
|
|
|
232
|
|
|
$components = [ |
233
|
|
|
'links' => [], |
234
|
|
|
'relationships' => [], |
235
|
|
|
'attributes' => [], |
236
|
|
|
'meta' => [], |
237
|
|
|
]; |
238
|
|
|
|
239
|
|
|
foreach ($attributes as $attribute) { |
240
|
|
|
$propertyMetadata = $this |
241
|
|
|
->propertyMetadataFactory |
242
|
|
|
->create($context['resource_class'], $attribute, $options); |
243
|
|
|
|
244
|
|
|
$type = $propertyMetadata->getType(); |
245
|
|
|
$isOne = $isMany = false; |
246
|
|
|
|
247
|
|
|
if (null !== $type) { |
248
|
|
|
if ($type->isCollection()) { |
249
|
|
|
$isMany = ($type->getCollectionValueType() && $className = $type->getCollectionValueType()->getClassName()) ? $this->resourceClassResolver->isResourceClass($className) : false; |
|
|
|
|
250
|
|
|
} else { |
251
|
|
|
$isOne = ($className = $type->getClassName()) ? $this->resourceClassResolver->isResourceClass($className) : false; |
252
|
|
|
} |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
if (!isset($className) || !$isOne && !$isMany) { |
256
|
|
|
$components['attributes'][] = $attribute; |
257
|
|
|
|
258
|
|
|
continue; |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
$relation = [ |
262
|
|
|
'name' => $attribute, |
263
|
|
|
'type' => $this->resourceMetadataFactory->create($className)->getShortName(), |
264
|
|
|
'cardinality' => $isOne ? 'one' : 'many', |
265
|
|
|
]; |
266
|
|
|
|
267
|
|
|
$components['relationships'][] = $relation; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
return $this->componentsCache[$context['cache_key']] = $components; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* Populates relationships keys. |
275
|
|
|
* |
276
|
|
|
* @param object $object |
277
|
|
|
* @param string|null $format |
278
|
|
|
* @param array $context |
279
|
|
|
* @param array $relationships |
280
|
|
|
* |
281
|
|
|
* @throws InvalidArgumentException |
282
|
|
|
* |
283
|
|
|
* @return array |
284
|
|
|
*/ |
285
|
|
|
private function getPopulatedRelations($object, string $format = null, array $context, array $relationships): array |
286
|
|
|
{ |
287
|
|
|
$data = []; |
288
|
|
|
|
289
|
|
|
foreach ($relationships as $relationshipDataArray) { |
290
|
|
|
$relationshipName = $relationshipDataArray['name']; |
291
|
|
|
|
292
|
|
|
$attributeValue = $this->getAttributeValue($object, $relationshipName, $format, $context); |
293
|
|
|
|
294
|
|
|
if ($this->nameConverter) { |
295
|
|
|
$relationshipName = $this->nameConverter->normalize($relationshipName); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
if (!$attributeValue) { |
299
|
|
|
continue; |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
$data[$relationshipName] = [ |
303
|
|
|
'data' => [], |
304
|
|
|
]; |
305
|
|
|
|
306
|
|
|
// Many to one relationship |
307
|
|
|
if ('one' === $relationshipDataArray['cardinality']) { |
308
|
|
|
$data[$relationshipName] = $attributeValue; |
309
|
|
|
|
310
|
|
|
continue; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
// Many to many relationship |
314
|
|
|
foreach ($attributeValue as $attributeValueElement) { |
315
|
|
|
if (!isset($attributeValueElement['data'])) { |
316
|
|
|
throw new InvalidArgumentException(sprintf('The JSON API attribute \'%s\' must contain a "data" key.', $relationshipName)); |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
$data[$relationshipName]['data'][] = $attributeValueElement['data']; |
320
|
|
|
} |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
return $data; |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* Gets the cache key to use. |
328
|
|
|
* |
329
|
|
|
* @param string|null $format |
330
|
|
|
* @param array $context |
331
|
|
|
* |
332
|
|
|
* @return bool|string |
333
|
|
|
*/ |
334
|
|
View Code Duplication |
private function getJsonApiCacheKey(string $format = null, array $context) |
|
|
|
|
335
|
|
|
{ |
336
|
|
|
try { |
337
|
|
|
return md5($format.serialize($context)); |
338
|
|
|
} catch (\Exception $exception) { |
339
|
|
|
// The context cannot be serialized, skip the cache |
340
|
|
|
return false; |
341
|
|
|
} |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.