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\Serializer; |
15
|
|
|
|
16
|
|
|
use ApiPlatform\Core\Api\IriConverterInterface; |
17
|
|
|
use ApiPlatform\Core\Api\ResourceClassResolverInterface; |
18
|
|
|
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; |
19
|
|
|
use ApiPlatform\Core\DataTransformer\DataTransformerInterface; |
20
|
|
|
use ApiPlatform\Core\Exception\InvalidArgumentException; |
21
|
|
|
use ApiPlatform\Core\Exception\InvalidValueException; |
22
|
|
|
use ApiPlatform\Core\Exception\ItemNotFoundException; |
23
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
24
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
25
|
|
|
use ApiPlatform\Core\Metadata\Property\PropertyMetadata; |
26
|
|
|
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; |
27
|
|
|
use ApiPlatform\Core\Util\ClassInfoTrait; |
28
|
|
|
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; |
29
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccess; |
30
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; |
31
|
|
|
use Symfony\Component\PropertyInfo\Type; |
32
|
|
|
use Symfony\Component\Serializer\Exception\LogicException; |
33
|
|
|
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; |
34
|
|
|
use Symfony\Component\Serializer\Exception\RuntimeException; |
35
|
|
|
use Symfony\Component\Serializer\Exception\UnexpectedValueException; |
36
|
|
|
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; |
37
|
|
|
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; |
38
|
|
|
use Symfony\Component\Serializer\NameConverter\NameConverterInterface; |
39
|
|
|
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; |
40
|
|
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; |
41
|
|
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Base item normalizer. |
45
|
|
|
* |
46
|
|
|
* @author Kévin Dunglas <[email protected]> |
47
|
|
|
*/ |
48
|
|
|
abstract class AbstractItemNormalizer extends AbstractObjectNormalizer |
49
|
|
|
{ |
50
|
|
|
use ClassInfoTrait; |
51
|
|
|
use ContextTrait; |
52
|
|
|
use InputOutputMetadataTrait; |
53
|
|
|
|
54
|
|
|
protected $propertyNameCollectionFactory; |
55
|
|
|
protected $propertyMetadataFactory; |
56
|
|
|
protected $iriConverter; |
57
|
|
|
protected $resourceClassResolver; |
58
|
|
|
protected $propertyAccessor; |
59
|
|
|
protected $itemDataProvider; |
60
|
|
|
protected $allowPlainIdentifiers; |
61
|
|
|
protected $dataTransformers = []; |
62
|
|
|
protected $localCache = []; |
63
|
|
|
|
64
|
|
|
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, array $defaultContext = [], iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null) |
65
|
|
|
{ |
66
|
|
|
if (!isset($defaultContext['circular_reference_handler'])) { |
67
|
|
|
$defaultContext['circular_reference_handler'] = function ($object) { |
68
|
|
|
return $this->iriConverter->getIriFromItem($object); |
69
|
|
|
}; |
70
|
|
|
} |
71
|
|
|
if (!interface_exists(AdvancedNameConverterInterface::class)) { |
72
|
|
|
$this->setCircularReferenceHandler($defaultContext['circular_reference_handler']); |
|
|
|
|
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
parent::__construct($classMetadataFactory, $nameConverter, null, null, \Closure::fromCallable([$this, 'getObjectClass']), $defaultContext); |
76
|
|
|
|
77
|
|
|
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory; |
78
|
|
|
$this->propertyMetadataFactory = $propertyMetadataFactory; |
79
|
|
|
$this->iriConverter = $iriConverter; |
80
|
|
|
$this->resourceClassResolver = $resourceClassResolver; |
81
|
|
|
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); |
82
|
|
|
$this->itemDataProvider = $itemDataProvider; |
83
|
|
|
$this->allowPlainIdentifiers = $allowPlainIdentifiers; |
84
|
|
|
$this->dataTransformers = $dataTransformers; |
85
|
|
|
$this->resourceMetadataFactory = $resourceMetadataFactory; |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* {@inheritdoc} |
90
|
|
|
*/ |
91
|
|
|
public function supportsNormalization($data, $format = null) |
92
|
|
|
{ |
93
|
|
|
if (!\is_object($data) || $data instanceof \Traversable) { |
94
|
|
|
return false; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
return $this->resourceClassResolver->isResourceClass($this->getObjectClass($data)); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* {@inheritdoc} |
102
|
|
|
*/ |
103
|
|
|
public function hasCacheableSupportsMethod(): bool |
104
|
|
|
{ |
105
|
|
|
return true; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* {@inheritdoc} |
110
|
|
|
* |
111
|
|
|
* @throws LogicException |
112
|
|
|
*/ |
113
|
|
|
public function normalize($object, $format = null, array $context = []) |
114
|
|
|
{ |
115
|
|
|
if ($object !== $transformed = $this->transformOutput($object, $context)) { |
116
|
|
|
if (!$this->serializer instanceof NormalizerInterface) { |
117
|
|
|
throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer'); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
$context['api_normalize'] = true; |
121
|
|
|
$context['api_resource'] = $object; |
122
|
|
|
unset($context['output']); |
123
|
|
|
unset($context['resource_class']); |
124
|
|
|
|
125
|
|
|
return $this->serializer->normalize($transformed, $format, $context); |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null); |
129
|
|
|
$context = $this->initContext($resourceClass, $context); |
130
|
|
|
$iri = $context['iri'] ?? $this->iriConverter->getIriFromItem($object); |
131
|
|
|
$context['iri'] = $iri; |
132
|
|
|
$context['api_normalize'] = true; |
133
|
|
|
|
134
|
|
|
/* |
135
|
|
|
* When true, converts the normalized data array of a resource into an |
136
|
|
|
* IRI, if the normalized data array is empty. |
137
|
|
|
* |
138
|
|
|
* This is useful when traversing from a non-resource towards an attribute |
139
|
|
|
* which is a resource, as we do not have the benefit of {@see PropertyMetadata::isReadableLink}. |
140
|
|
|
* |
141
|
|
|
* It must not be propagated to subresources, as {@see PropertyMetadata::isReadableLink} |
142
|
|
|
* should take effect. |
143
|
|
|
*/ |
144
|
|
|
$emptyResourceAsIri = $context['api_empty_resource_as_iri'] ?? false; |
145
|
|
|
unset($context['api_empty_resource_as_iri']); |
146
|
|
|
|
147
|
|
|
if (isset($context['resources'])) { |
148
|
|
|
$context['resources'][$iri] = $iri; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
$data = parent::normalize($object, $format, $context); |
152
|
|
|
if ($emptyResourceAsIri && \is_array($data) && 0 === \count($data)) { |
153
|
|
|
return $iri; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
return $data; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* {@inheritdoc} |
161
|
|
|
*/ |
162
|
|
|
public function supportsDenormalization($data, $type, $format = null) |
163
|
|
|
{ |
164
|
|
|
return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* {@inheritdoc} |
169
|
|
|
*/ |
170
|
|
|
public function denormalize($data, $class, $format = null, array $context = []) |
171
|
|
|
{ |
172
|
|
|
$resourceClass = $this->resourceClassResolver->getResourceClass(null, $class); |
173
|
|
|
$context['api_denormalize'] = true; |
174
|
|
|
$context['resource_class'] = $resourceClass; |
175
|
|
|
|
176
|
|
|
if (null !== ($inputClass = $this->getInputClass($resourceClass, $context)) && null !== ($dataTransformer = $this->getDataTransformer($data, $resourceClass, $context))) { |
177
|
|
|
$dataTransformerContext = $context; |
178
|
|
|
|
179
|
|
|
unset($context['input']); |
180
|
|
|
unset($context['resource_class']); |
181
|
|
|
|
182
|
|
|
if (!$this->serializer instanceof DenormalizerInterface) { |
183
|
|
|
throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer'); |
184
|
|
|
} |
185
|
|
|
$denormalizedInput = $this->serializer->denormalize($data, $inputClass, $format, $context); |
186
|
|
|
if (!\is_object($denormalizedInput)) { |
187
|
|
|
throw new \UnexpectedValueException('Expected denormalized input to be an object.'); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
return $dataTransformer->transform($denormalizedInput, $resourceClass, $dataTransformerContext); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
$supportsPlainIdentifiers = $this->supportsPlainIdentifiers(); |
194
|
|
|
|
195
|
|
|
if (\is_string($data)) { |
196
|
|
|
try { |
197
|
|
|
return $this->iriConverter->getItemFromIri($data, $context + ['fetch_data' => true]); |
198
|
|
|
} catch (ItemNotFoundException $e) { |
199
|
|
|
if (!$supportsPlainIdentifiers) { |
200
|
|
|
throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); |
201
|
|
|
} |
202
|
|
|
} catch (InvalidArgumentException $e) { |
203
|
|
|
if (!$supportsPlainIdentifiers) { |
204
|
|
|
throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e); |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
if (!\is_array($data)) { |
210
|
|
|
if (!$supportsPlainIdentifiers) { |
211
|
|
|
throw new UnexpectedValueException(sprintf('Expected IRI or document for resource "%s", "%s" given.', $resourceClass, \gettype($data))); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
$item = $this->itemDataProvider->getItem($resourceClass, $data, null, $context + ['fetch_data' => true]); |
|
|
|
|
215
|
|
|
if (null === $item) { |
216
|
|
|
throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $resourceClass, $data)); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
return $item; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
return parent::denormalize($data, $resourceClass, $format, $context); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Method copy-pasted from symfony/serializer. |
227
|
|
|
* Remove it after symfony/serializer version update @link https://github.com/symfony/symfony/pull/28263. |
228
|
|
|
* |
229
|
|
|
* {@inheritdoc} |
230
|
|
|
* |
231
|
|
|
* @internal |
232
|
|
|
*/ |
233
|
|
|
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null) |
234
|
|
|
{ |
235
|
|
|
if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) { |
236
|
|
|
unset($context[static::OBJECT_TO_POPULATE]); |
237
|
|
|
|
238
|
|
|
return $object; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { |
242
|
|
|
if (!isset($data[$mapping->getTypeProperty()])) { |
243
|
|
|
throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping->getTypeProperty(), $class)); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
$type = $data[$mapping->getTypeProperty()]; |
247
|
|
|
if (null === ($mappedClass = $mapping->getClassForType($type))) { |
248
|
|
|
throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"', $type, $class)); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
$class = $mappedClass; |
252
|
|
|
$reflectionClass = new \ReflectionClass($class); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
$constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes); |
256
|
|
|
if ($constructor) { |
|
|
|
|
257
|
|
|
$constructorParameters = $constructor->getParameters(); |
258
|
|
|
|
259
|
|
|
$params = []; |
260
|
|
|
foreach ($constructorParameters as $constructorParameter) { |
261
|
|
|
$paramName = $constructorParameter->name; |
262
|
|
|
$key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName; |
|
|
|
|
263
|
|
|
|
264
|
|
|
$allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true)); |
265
|
|
|
$ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context); |
266
|
|
|
if ($constructorParameter->isVariadic()) { |
267
|
|
|
if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) { |
268
|
|
|
if (!\is_array($data[$paramName])) { |
269
|
|
|
throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because the variadic parameter %s can only accept an array.', $class, $constructorParameter->name)); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
$params = array_merge($params, $data[$paramName]); |
273
|
|
|
} |
274
|
|
|
} elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) { |
275
|
|
|
$params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $context, $format); |
276
|
|
|
|
277
|
|
|
// Don't run set for a parameter passed to the constructor |
278
|
|
|
unset($data[$key]); |
279
|
|
|
} elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) { |
280
|
|
|
$params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key]; |
281
|
|
|
} elseif ($constructorParameter->isDefaultValueAvailable()) { |
282
|
|
|
$params[] = $constructorParameter->getDefaultValue(); |
283
|
|
|
} else { |
284
|
|
|
throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name)); |
285
|
|
|
} |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
if ($constructor->isConstructor()) { |
289
|
|
|
return $reflectionClass->newInstanceArgs($params); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
return $constructor->invokeArgs(null, $params); |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
return new $class(); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* {@inheritdoc} |
300
|
|
|
*/ |
301
|
|
|
protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array &$context, string $format = null) |
|
|
|
|
302
|
|
|
{ |
303
|
|
|
return $this->createAttributeValue($constructorParameter->name, $parameterData, $format, $context); |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
/** |
307
|
|
|
* {@inheritdoc} |
308
|
|
|
* |
309
|
|
|
* Unused in this context. |
310
|
|
|
*/ |
311
|
|
|
protected function extractAttributes($object, $format = null, array $context = []) |
312
|
|
|
{ |
313
|
|
|
return []; |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
/** |
317
|
|
|
* {@inheritdoc} |
318
|
|
|
*/ |
319
|
|
|
protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false) |
320
|
|
|
{ |
321
|
|
|
$options = $this->getFactoryOptions($context); |
322
|
|
|
$propertyNames = $this->propertyNameCollectionFactory->create($context['resource_class'], $options); |
323
|
|
|
|
324
|
|
|
$allowedAttributes = []; |
325
|
|
|
foreach ($propertyNames as $propertyName) { |
326
|
|
|
$propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $propertyName, $options); |
327
|
|
|
|
328
|
|
|
if ( |
329
|
|
|
$this->isAllowedAttribute($classOrObject, $propertyName, null, $context) && |
330
|
|
|
( |
331
|
|
|
isset($context['api_normalize']) && $propertyMetadata->isReadable() || |
332
|
|
|
isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable()) |
333
|
|
|
) |
334
|
|
|
) { |
335
|
|
|
$allowedAttributes[] = $propertyName; |
336
|
|
|
} |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
return $allowedAttributes; |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
/** |
343
|
|
|
* {@inheritdoc} |
344
|
|
|
*/ |
345
|
|
|
protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []) |
346
|
|
|
{ |
347
|
|
|
$this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context)); |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* Validates the type of the value. Allows using integers as floats for JSON formats. |
352
|
|
|
* |
353
|
|
|
* @throws InvalidArgumentException |
354
|
|
|
*/ |
355
|
|
|
protected function validateType(string $attribute, Type $type, $value, string $format = null) |
356
|
|
|
{ |
357
|
|
|
$builtinType = $type->getBuiltinType(); |
358
|
|
|
if (Type::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && false !== strpos($format, 'json')) { |
359
|
|
|
$isValid = \is_float($value) || \is_int($value); |
360
|
|
|
} else { |
361
|
|
|
$isValid = \call_user_func('is_'.$builtinType, $value); |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
if (!$isValid) { |
365
|
|
|
throw new InvalidArgumentException(sprintf('The type of the "%s" attribute must be "%s", "%s" given.', $attribute, $builtinType, \gettype($value))); |
366
|
|
|
} |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* Denormalizes a collection of objects. |
371
|
|
|
* |
372
|
|
|
* @throws InvalidArgumentException |
373
|
|
|
*/ |
374
|
|
|
protected function denormalizeCollection(string $attribute, PropertyMetadata $propertyMetadata, Type $type, string $className, $value, ?string $format, array $context): array |
375
|
|
|
{ |
376
|
|
|
if (!\is_array($value)) { |
377
|
|
|
throw new InvalidArgumentException(sprintf('The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value))); |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
$collectionKeyType = $type->getCollectionKeyType(); |
381
|
|
|
$collectionKeyBuiltinType = null === $collectionKeyType ? null : $collectionKeyType->getBuiltinType(); |
382
|
|
|
|
383
|
|
|
$values = []; |
384
|
|
|
foreach ($value as $index => $obj) { |
385
|
|
|
if (null !== $collectionKeyBuiltinType && !\call_user_func('is_'.$collectionKeyBuiltinType, $index)) { |
386
|
|
|
throw new InvalidArgumentException(sprintf('The type of the key "%s" must be "%s", "%s" given.', $index, $collectionKeyBuiltinType, \gettype($index))); |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
$values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $this->createChildContext($context, $attribute, $format)); |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
return $values; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
/** |
396
|
|
|
* Denormalizes a relation. |
397
|
|
|
* |
398
|
|
|
* @throws LogicException |
399
|
|
|
* @throws UnexpectedValueException |
400
|
|
|
* @throws ItemNotFoundException |
401
|
|
|
* |
402
|
|
|
* @return object|null |
403
|
|
|
*/ |
404
|
|
|
protected function denormalizeRelation(string $attributeName, PropertyMetadata $propertyMetadata, string $className, $value, ?string $format, array $context) |
405
|
|
|
{ |
406
|
|
|
$supportsPlainIdentifiers = $this->supportsPlainIdentifiers(); |
407
|
|
|
|
408
|
|
|
if (\is_string($value)) { |
409
|
|
|
try { |
410
|
|
|
return $this->iriConverter->getItemFromIri($value, $context + ['fetch_data' => true]); |
411
|
|
|
} catch (ItemNotFoundException $e) { |
412
|
|
|
if (!$supportsPlainIdentifiers) { |
413
|
|
|
throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); |
414
|
|
|
} |
415
|
|
|
} catch (InvalidArgumentException $e) { |
416
|
|
|
if (!$supportsPlainIdentifiers) { |
417
|
|
|
throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e); |
418
|
|
|
} |
419
|
|
|
} |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
if ($propertyMetadata->isWritableLink()) { |
423
|
|
|
$context['api_allow_update'] = true; |
424
|
|
|
|
425
|
|
|
if (!$this->serializer instanceof DenormalizerInterface) { |
426
|
|
|
throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class)); |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
try { |
430
|
|
|
$item = $this->serializer->denormalize($value, $className, $format, $context); |
431
|
|
|
if (!\is_object($item) && null !== $item) { |
432
|
|
|
throw new \UnexpectedValueException('Expected item to be an object or null.'); |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
return $item; |
436
|
|
|
} catch (InvalidValueException $e) { |
437
|
|
|
if (!$supportsPlainIdentifiers) { |
438
|
|
|
throw $e; |
439
|
|
|
} |
440
|
|
|
} |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
if (!\is_array($value)) { |
444
|
|
|
if (!$supportsPlainIdentifiers) { |
445
|
|
|
throw new UnexpectedValueException(sprintf('Expected IRI or nested document for attribute "%s", "%s" given.', $attributeName, \gettype($value))); |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
$item = $this->itemDataProvider->getItem($className, $value, null, $context + ['fetch_data' => true]); |
449
|
|
|
if (null === $item) { |
450
|
|
|
throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $className, $value)); |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
return $item; |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
throw new UnexpectedValueException(sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.', $attributeName)); |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
/** |
460
|
|
|
* Gets the options for the property name collection / property metadata factories. |
461
|
|
|
*/ |
462
|
|
|
protected function getFactoryOptions(array $context): array |
463
|
|
|
{ |
464
|
|
|
$options = []; |
465
|
|
|
|
466
|
|
|
if (isset($context[self::GROUPS])) { |
467
|
|
|
/* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */ |
468
|
|
|
$options['serializer_groups'] = (array) $context[self::GROUPS]; |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
if (isset($context['collection_operation_name'])) { |
472
|
|
|
$options['collection_operation_name'] = $context['collection_operation_name']; |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
if (isset($context['item_operation_name'])) { |
476
|
|
|
$options['item_operation_name'] = $context['item_operation_name']; |
477
|
|
|
} |
478
|
|
|
|
479
|
|
|
return $options; |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
/** |
483
|
|
|
* Creates the context to use when serializing a relation. |
484
|
|
|
* |
485
|
|
|
* @deprecated since version 2.1, to be removed in 3.0. |
486
|
|
|
*/ |
487
|
|
|
protected function createRelationSerializationContext(string $resourceClass, array $context): array |
|
|
|
|
488
|
|
|
{ |
489
|
|
|
@trigger_error(sprintf('The method %s() is deprecated since 2.1 and will be removed in 3.0.', __METHOD__), E_USER_DEPRECATED); |
490
|
|
|
|
491
|
|
|
return $context; |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
/** |
495
|
|
|
* {@inheritdoc} |
496
|
|
|
* |
497
|
|
|
* @throws NoSuchPropertyException |
498
|
|
|
* @throws UnexpectedValueException |
499
|
|
|
* @throws LogicException |
500
|
|
|
*/ |
501
|
|
|
protected function getAttributeValue($object, $attribute, $format = null, array $context = []) |
502
|
|
|
{ |
503
|
|
|
$context['api_attribute'] = $attribute; |
504
|
|
|
$propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context)); |
505
|
|
|
|
506
|
|
|
try { |
507
|
|
|
$attributeValue = $this->propertyAccessor->getValue($object, $attribute); |
508
|
|
|
} catch (NoSuchPropertyException $e) { |
509
|
|
|
if (!$propertyMetadata->hasChildInherited()) { |
510
|
|
|
throw $e; |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
$attributeValue = null; |
514
|
|
|
} |
515
|
|
|
|
516
|
|
|
$type = $propertyMetadata->getType(); |
517
|
|
|
|
518
|
|
|
if ( |
519
|
|
|
$type && |
520
|
|
|
$type->isCollection() && |
521
|
|
|
($collectionValueType = $type->getCollectionValueType()) && |
522
|
|
|
($className = $collectionValueType->getClassName()) && |
523
|
|
|
$this->resourceClassResolver->isResourceClass($className) |
524
|
|
|
) { |
525
|
|
|
if (!is_iterable($attributeValue)) { |
526
|
|
|
throw new UnexpectedValueException('Unexpected non-iterable value for to-many relation.'); |
527
|
|
|
} |
528
|
|
|
|
529
|
|
|
$resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className); |
530
|
|
|
$childContext = $this->createChildContext($context, $attribute, $format); |
531
|
|
|
$childContext['resource_class'] = $resourceClass; |
532
|
|
|
unset($childContext['iri']); |
533
|
|
|
|
534
|
|
|
return $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext); |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
if ( |
538
|
|
|
$type && |
539
|
|
|
($className = $type->getClassName()) && |
540
|
|
|
$this->resourceClassResolver->isResourceClass($className) |
541
|
|
|
) { |
542
|
|
|
if (!\is_object($attributeValue) && null !== $attributeValue) { |
543
|
|
|
throw new UnexpectedValueException('Unexpected non-object value for to-one relation.'); |
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
$resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className); |
547
|
|
|
$childContext = $this->createChildContext($context, $attribute, $format); |
548
|
|
|
$childContext['resource_class'] = $resourceClass; |
549
|
|
|
unset($childContext['iri']); |
550
|
|
|
|
551
|
|
|
return $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext); |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
if (!$this->serializer instanceof NormalizerInterface) { |
555
|
|
|
throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class)); |
556
|
|
|
} |
557
|
|
|
|
558
|
|
|
unset($context['resource_class']); |
559
|
|
|
|
560
|
|
|
return $this->serializer->normalize($attributeValue, $format, $context); |
561
|
|
|
} |
562
|
|
|
|
563
|
|
|
/** |
564
|
|
|
* Normalizes a collection of relations (to-many). |
565
|
|
|
* |
566
|
|
|
* @param iterable $attributeValue |
567
|
|
|
* |
568
|
|
|
* @throws UnexpectedValueException |
569
|
|
|
*/ |
570
|
|
|
protected function normalizeCollectionOfRelations(PropertyMetadata $propertyMetadata, $attributeValue, string $resourceClass, ?string $format, array $context): array |
571
|
|
|
{ |
572
|
|
|
$value = []; |
573
|
|
|
foreach ($attributeValue as $index => $obj) { |
574
|
|
|
if (!\is_object($obj) && null !== $obj) { |
575
|
|
|
throw new UnexpectedValueException('Unexpected non-object element in to-many relation.'); |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
$value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context); |
579
|
|
|
} |
580
|
|
|
|
581
|
|
|
return $value; |
582
|
|
|
} |
583
|
|
|
|
584
|
|
|
/** |
585
|
|
|
* Normalizes a relation. |
586
|
|
|
* |
587
|
|
|
* @param object|null $relatedObject |
588
|
|
|
* |
589
|
|
|
* @throws LogicException |
590
|
|
|
* @throws UnexpectedValueException |
591
|
|
|
* |
592
|
|
|
* @return string|array|\ArrayObject|null IRI or normalized object data |
593
|
|
|
*/ |
594
|
|
|
protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, ?string $format, array $context) |
595
|
|
|
{ |
596
|
|
|
if (null === $relatedObject || !empty($context['attributes']) || $propertyMetadata->isReadableLink()) { |
597
|
|
|
if (!$this->serializer instanceof NormalizerInterface) { |
598
|
|
|
throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class)); |
599
|
|
|
} |
600
|
|
|
|
601
|
|
|
$normalizedRelatedObject = $this->serializer->normalize($relatedObject, $format, $context); |
602
|
|
|
if (!\is_string($normalizedRelatedObject) && !\is_array($normalizedRelatedObject) && !$normalizedRelatedObject instanceof \ArrayObject && null !== $normalizedRelatedObject) { |
603
|
|
|
throw new UnexpectedValueException('Expected normalized relation to be an IRI, array, \ArrayObject or null'); |
604
|
|
|
} |
605
|
|
|
|
606
|
|
|
return $normalizedRelatedObject; |
607
|
|
|
} |
608
|
|
|
|
609
|
|
|
$iri = $this->iriConverter->getIriFromItem($relatedObject); |
610
|
|
|
if (isset($context['resources'])) { |
611
|
|
|
$context['resources'][$iri] = $iri; |
612
|
|
|
} |
613
|
|
|
if (isset($context['resources_to_push']) && $propertyMetadata->getAttribute('push', false)) { |
614
|
|
|
$context['resources_to_push'][$iri] = $iri; |
615
|
|
|
} |
616
|
|
|
|
617
|
|
|
return $iri; |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
/** |
621
|
|
|
* Finds the first supported data transformer if any. |
622
|
|
|
* |
623
|
|
|
* @param object|array $data object on normalize / array on denormalize |
624
|
|
|
*/ |
625
|
|
|
protected function getDataTransformer($data, string $to, array $context = []): ?DataTransformerInterface |
626
|
|
|
{ |
627
|
|
|
foreach ($this->dataTransformers as $dataTransformer) { |
628
|
|
|
if ($dataTransformer->supportsTransformation($data, $to, $context)) { |
629
|
|
|
return $dataTransformer; |
630
|
|
|
} |
631
|
|
|
} |
632
|
|
|
|
633
|
|
|
return null; |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
/** |
637
|
|
|
* For a given resource, it returns an output representation if any |
638
|
|
|
* If not, the resource is returned. |
639
|
|
|
*/ |
640
|
|
|
protected function transformOutput($object, array $context = []) |
641
|
|
|
{ |
642
|
|
|
$outputClass = $this->getOutputClass($this->getObjectClass($object), $context); |
643
|
|
|
if (null !== $outputClass && null !== $dataTransformer = $this->getDataTransformer($object, $outputClass, $context)) { |
644
|
|
|
return $dataTransformer->transform($object, $outputClass, $context); |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
return $object; |
648
|
|
|
} |
649
|
|
|
|
650
|
|
|
private function createAttributeValue($attribute, $value, $format = null, array $context = []) |
651
|
|
|
{ |
652
|
|
|
$propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context)); |
653
|
|
|
$type = $propertyMetadata->getType(); |
654
|
|
|
|
655
|
|
|
if (null === $type) { |
656
|
|
|
// No type provided, blindly return the value |
657
|
|
|
return $value; |
658
|
|
|
} |
659
|
|
|
|
660
|
|
|
if (null === $value && $type->isNullable()) { |
661
|
|
|
return $value; |
662
|
|
|
} |
663
|
|
|
|
664
|
|
|
if ( |
665
|
|
|
$type->isCollection() && |
666
|
|
|
null !== ($collectionValueType = $type->getCollectionValueType()) && |
667
|
|
|
null !== ($className = $collectionValueType->getClassName()) && |
668
|
|
|
$this->resourceClassResolver->isResourceClass($className) |
669
|
|
|
) { |
670
|
|
|
$resourceClass = $this->resourceClassResolver->getResourceClass(null, $className); |
671
|
|
|
$context['resource_class'] = $resourceClass; |
672
|
|
|
|
673
|
|
|
return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $resourceClass, $value, $format, $context); |
674
|
|
|
} |
675
|
|
|
|
676
|
|
|
if ( |
677
|
|
|
null !== ($className = $type->getClassName()) && |
678
|
|
|
$this->resourceClassResolver->isResourceClass($className) |
679
|
|
|
) { |
680
|
|
|
$resourceClass = $this->resourceClassResolver->getResourceClass(null, $className); |
681
|
|
|
$childContext = $this->createChildContext($context, $attribute, $format); |
682
|
|
|
$childContext['resource_class'] = $resourceClass; |
683
|
|
|
|
684
|
|
|
return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext); |
685
|
|
|
} |
686
|
|
|
|
687
|
|
|
if ( |
688
|
|
|
$type->isCollection() && |
689
|
|
|
null !== ($collectionValueType = $type->getCollectionValueType()) && |
690
|
|
|
null !== ($className = $collectionValueType->getClassName()) |
691
|
|
|
) { |
692
|
|
|
if (!$this->serializer instanceof DenormalizerInterface) { |
693
|
|
|
throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class)); |
694
|
|
|
} |
695
|
|
|
|
696
|
|
|
unset($context['resource_class']); |
697
|
|
|
|
698
|
|
|
return $this->serializer->denormalize($value, $className.'[]', $format, $context); |
699
|
|
|
} |
700
|
|
|
|
701
|
|
|
if (null !== $className = $type->getClassName()) { |
702
|
|
|
if (!$this->serializer instanceof DenormalizerInterface) { |
703
|
|
|
throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class)); |
704
|
|
|
} |
705
|
|
|
|
706
|
|
|
unset($context['resource_class']); |
707
|
|
|
|
708
|
|
|
return $this->serializer->denormalize($value, $className, $format, $context); |
709
|
|
|
} |
710
|
|
|
|
711
|
|
|
if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) { |
712
|
|
|
return $value; |
713
|
|
|
} |
714
|
|
|
|
715
|
|
|
$this->validateType($attribute, $type, $value, $format); |
716
|
|
|
|
717
|
|
|
return $value; |
718
|
|
|
} |
719
|
|
|
|
720
|
|
|
/** |
721
|
|
|
* Sets a value of the object using the PropertyAccess component. |
722
|
|
|
* |
723
|
|
|
* @param object $object |
724
|
|
|
*/ |
725
|
|
|
private function setValue($object, string $attributeName, $value) |
726
|
|
|
{ |
727
|
|
|
try { |
728
|
|
|
$this->propertyAccessor->setValue($object, $attributeName, $value); |
729
|
|
|
} catch (NoSuchPropertyException $exception) { |
730
|
|
|
// Properties not found are ignored |
731
|
|
|
} |
732
|
|
|
} |
733
|
|
|
|
734
|
|
|
private function supportsPlainIdentifiers(): bool |
735
|
|
|
{ |
736
|
|
|
return $this->allowPlainIdentifiers && null !== $this->itemDataProvider; |
737
|
|
|
} |
738
|
|
|
} |
739
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.