vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php line 91

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the API Platform project.
  4. *
  5. * (c) Kévin Dunglas <dunglas@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. declare(strict_types=1);
  11. namespace ApiPlatform\Serializer;
  12. use ApiPlatform\Api\IriConverterInterface;
  13. use ApiPlatform\Api\UrlGeneratorInterface;
  14. use ApiPlatform\Core\Api\IriConverterInterface as LegacyIriConverterInterface;
  15. use ApiPlatform\Core\Bridge\Symfony\Messenger\DataTransformer as MessengerDataTransformer;
  16. use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
  17. use ApiPlatform\Core\DataTransformer\DataTransformerInitializerInterface;
  18. use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
  19. use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface as LegacyPropertyMetadataFactoryInterface;
  20. use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
  21. use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
  22. use ApiPlatform\Exception\InvalidArgumentException;
  23. use ApiPlatform\Exception\InvalidValueException;
  24. use ApiPlatform\Exception\ItemNotFoundException;
  25. use ApiPlatform\Metadata\ApiProperty;
  26. use ApiPlatform\Metadata\CollectionOperationInterface;
  27. use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
  28. use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
  29. use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
  30. use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface;
  31. use ApiPlatform\Util\ClassInfoTrait;
  32. use ApiPlatform\Util\CloneTrait;
  33. use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
  34. use Symfony\Component\PropertyAccess\PropertyAccess;
  35. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  36. use Symfony\Component\PropertyInfo\Type;
  37. use Symfony\Component\Serializer\Encoder\CsvEncoder;
  38. use Symfony\Component\Serializer\Encoder\XmlEncoder;
  39. use Symfony\Component\Serializer\Exception\LogicException;
  40. use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
  41. use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
  42. use Symfony\Component\Serializer\Exception\RuntimeException;
  43. use Symfony\Component\Serializer\Exception\UnexpectedValueException;
  44. use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
  45. use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
  46. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  47. use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
  48. use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
  49. use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
  50. /**
  51. * Base item normalizer.
  52. *
  53. * @author Kévin Dunglas <dunglas@gmail.com>
  54. */
  55. abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
  56. {
  57. use ClassInfoTrait;
  58. use CloneTrait;
  59. use ContextTrait;
  60. use InputOutputMetadataTrait;
  61. use OperationContextTrait;
  62. public const IS_TRANSFORMED_TO_SAME_CLASS = 'is_transformed_to_same_class';
  63. /**
  64. * @var PropertyNameCollectionFactoryInterface
  65. */
  66. protected $propertyNameCollectionFactory;
  67. /**
  68. * @var LegacyPropertyMetadataFactoryInterface|PropertyMetadataFactoryInterface
  69. */
  70. protected $propertyMetadataFactory;
  71. protected $resourceMetadataFactory;
  72. /**
  73. * @var LegacyIriConverterInterface|IriConverterInterface
  74. */
  75. protected $iriConverter;
  76. protected $resourceClassResolver;
  77. protected $resourceAccessChecker;
  78. protected $propertyAccessor;
  79. protected $itemDataProvider;
  80. protected $allowPlainIdentifiers;
  81. protected $dataTransformers = [];
  82. protected $localCache = [];
  83. public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, array $defaultContext = [], iterable $dataTransformers = [], $resourceMetadataFactory = null, ResourceAccessCheckerInterface $resourceAccessChecker = null)
  84. {
  85. if (!isset($defaultContext['circular_reference_handler'])) {
  86. $defaultContext['circular_reference_handler'] = function ($object) {
  87. return $this->iriConverter instanceof LegacyIriConverterInterface ? $this->iriConverter->getIriFromItem($object) : $this->iriConverter->getIriFromResource($object);
  88. };
  89. }
  90. if (!interface_exists(AdvancedNameConverterInterface::class) && method_exists($this, 'setCircularReferenceHandler')) {
  91. $this->setCircularReferenceHandler($defaultContext['circular_reference_handler']);
  92. }
  93. parent::__construct($classMetadataFactory, $nameConverter, null, null, \Closure::fromCallable([$this, 'getObjectClass']), $defaultContext);
  94. $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
  95. $this->propertyMetadataFactory = $propertyMetadataFactory;
  96. if ($iriConverter instanceof LegacyIriConverterInterface) {
  97. trigger_deprecation('api-platform/core', '2.7', sprintf('Use an implementation of "%s" instead of "%s".', IriConverterInterface::class, LegacyIriConverterInterface::class));
  98. }
  99. $this->iriConverter = $iriConverter;
  100. $this->resourceClassResolver = $resourceClassResolver;
  101. $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
  102. $this->itemDataProvider = $itemDataProvider;
  103. if (true === $allowPlainIdentifiers) {
  104. @trigger_error(sprintf('Allowing plain identifiers as argument of "%s" is deprecated since API Platform 2.7 and will not be possible anymore in API Platform 3.', self::class), \E_USER_DEPRECATED);
  105. }
  106. $this->allowPlainIdentifiers = $allowPlainIdentifiers;
  107. $this->dataTransformers = $dataTransformers;
  108. // Just skip our data transformer to trigger a proper deprecation
  109. $customDataTransformers = array_filter(\is_array($dataTransformers) ? $dataTransformers : iterator_to_array($dataTransformers), function ($dataTransformer) {
  110. return !$dataTransformer instanceof MessengerDataTransformer;
  111. });
  112. if (\count($customDataTransformers)) {
  113. trigger_deprecation('api-platform/core', '2.7', 'The DataTransformer pattern is deprecated, use a Provider or a Processor and either use your input or return a new output there.');
  114. }
  115. if ($resourceMetadataFactory && !$resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) {
  116. trigger_deprecation('api-platform/core', '2.7', sprintf('Use "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, ResourceMetadataFactoryInterface::class));
  117. }
  118. $this->resourceMetadataFactory = $resourceMetadataFactory;
  119. $this->resourceAccessChecker = $resourceAccessChecker;
  120. }
  121. public function supportsNormalization($data, $format = null, array $context = []): bool
  122. {
  123. if (!\is_object($data) || is_iterable($data)) {
  124. return false;
  125. }
  126. $class = $this->getObjectClass($data);
  127. if (($context['output']['class'] ?? null) === $class) {
  128. return true;
  129. }
  130. return $this->resourceClassResolver->isResourceClass($class);
  131. }
  132. public function hasCacheableSupportsMethod(): bool
  133. {
  134. return true;
  135. }
  136. /**
  137. * @param mixed|null $format
  138. *
  139. * @throws LogicException
  140. *
  141. * @return array|string|int|float|bool|\ArrayObject|null
  142. */
  143. public function normalize($object, $format = null, array $context = [])
  144. {
  145. $resourceClass = $this->getObjectClass($object);
  146. if (!($isTransformed = isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS])) && $outputClass = $this->getOutputClass($resourceClass, $context)) {
  147. if (!$this->serializer instanceof NormalizerInterface) {
  148. throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer');
  149. }
  150. // Data transformers are deprecated, this is removed from 3.0
  151. if ($dataTransformer = $this->getDataTransformer($object, $outputClass, $context)) {
  152. $transformed = $dataTransformer->transform($object, $outputClass, $context);
  153. if ($object === $transformed) {
  154. $context[self::IS_TRANSFORMED_TO_SAME_CLASS] = true;
  155. } else {
  156. $context['api_normalize'] = true;
  157. $context['api_resource'] = $object;
  158. unset($context['output'], $context['resource_class']);
  159. }
  160. return $this->serializer->normalize($transformed, $format, $context);
  161. }
  162. unset($context['output'], $context['operation_name']);
  163. if ($this->resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface && !isset($context['operation'])) {
  164. $context['operation'] = $this->resourceMetadataFactory->create($context['resource_class'])->getOperation();
  165. }
  166. $context['resource_class'] = $outputClass;
  167. $context['api_sub_level'] = true;
  168. $context[self::ALLOW_EXTRA_ATTRIBUTES] = false;
  169. return $this->serializer->normalize($object, $format, $context);
  170. }
  171. if ($isTransformed) {
  172. unset($context[self::IS_TRANSFORMED_TO_SAME_CLASS]);
  173. }
  174. if ($isResourceClass = $this->resourceClassResolver->isResourceClass($resourceClass)) {
  175. $context = $this->initContext($resourceClass, $context);
  176. }
  177. // Never remove this, with `application/json` we don't use our AbstractCollectionNormalizer and we need
  178. // to remove the collection operation from our context or we'll introduce security issues
  179. if (isset($context['operation']) && $context['operation'] instanceof CollectionOperationInterface) {
  180. unset($context['operation_name']);
  181. unset($context['operation']);
  182. unset($context['iri']);
  183. }
  184. $iri = null;
  185. if (isset($context['iri'])) {
  186. $iri = $context['iri'];
  187. } elseif ($this->iriConverter instanceof LegacyIriConverterInterface && $isResourceClass) {
  188. $iri = $this->iriConverter->getIriFromItem($object);
  189. } elseif ($this->iriConverter instanceof IriConverterInterface) {
  190. $iri = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL, $context['operation'] ?? null, $context);
  191. }
  192. $context['iri'] = $iri;
  193. $context['api_normalize'] = true;
  194. /*
  195. * When true, converts the normalized data array of a resource into an
  196. * IRI, if the normalized data array is empty.
  197. *
  198. * This is useful when traversing from a non-resource towards an attribute
  199. * which is a resource, as we do not have the benefit of {@see PropertyMetadata::isReadableLink}.
  200. *
  201. * It must not be propagated to subresources, as {@see PropertyMetadata::isReadableLink}
  202. * should take effect.
  203. */
  204. $emptyResourceAsIri = $context['api_empty_resource_as_iri'] ?? false;
  205. unset($context['api_empty_resource_as_iri']);
  206. if (isset($context['resources'])) {
  207. $context['resources'][$iri] = $iri;
  208. }
  209. $data = parent::normalize($object, $format, $context);
  210. if ($emptyResourceAsIri && \is_array($data) && 0 === \count($data)) {
  211. return $iri;
  212. }
  213. return $data;
  214. }
  215. /**
  216. * @param mixed|null $format
  217. *
  218. * @return bool
  219. */
  220. public function supportsDenormalization($data, $type, $format = null, array $context = [])
  221. {
  222. if (($context['input']['class'] ?? null) === $type) {
  223. return true;
  224. }
  225. return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
  226. }
  227. public function denormalize($data, $class, $format = null, array $context = [])
  228. {
  229. $resourceClass = $class;
  230. if (null !== $inputClass = $this->getInputClass($resourceClass, $context)) {
  231. if (null !== $dataTransformer = $this->getDataTransformer($data, $resourceClass, $context)) {
  232. $dataTransformerContext = $context;
  233. unset($context['input']);
  234. unset($context['resource_class']);
  235. if (!$this->serializer instanceof DenormalizerInterface) {
  236. throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer');
  237. }
  238. if ($dataTransformer instanceof DataTransformerInitializerInterface) {
  239. $context[AbstractObjectNormalizer::OBJECT_TO_POPULATE] = $dataTransformer->initialize($inputClass, $context);
  240. $context[AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE] = true;
  241. }
  242. try {
  243. $denormalizedInput = $this->serializer->denormalize($data, $inputClass, $format, $context);
  244. } catch (NotNormalizableValueException $e) {
  245. throw new UnexpectedValueException('The input data is misformatted.', $e->getCode(), $e);
  246. }
  247. if (!\is_object($denormalizedInput)) {
  248. throw new UnexpectedValueException('Expected denormalized input to be an object.');
  249. }
  250. return $dataTransformer->transform($denormalizedInput, $resourceClass, $dataTransformerContext);
  251. }
  252. unset($context['input']);
  253. unset($context['operation']);
  254. unset($context['operation_name']);
  255. $context['resource_class'] = $inputClass;
  256. if (!$this->serializer instanceof DenormalizerInterface) {
  257. throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer');
  258. }
  259. try {
  260. return $this->serializer->denormalize($data, $inputClass, $format, $context);
  261. } catch (NotNormalizableValueException $e) {
  262. throw new UnexpectedValueException('The input data is misformatted.', $e->getCode(), $e);
  263. }
  264. }
  265. if (null === $objectToPopulate = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
  266. $normalizedData = \is_scalar($data) ? [$data] : $this->prepareForDenormalization($data);
  267. $class = $this->getClassDiscriminatorResolvedClass($normalizedData, $class);
  268. }
  269. $context['api_denormalize'] = true;
  270. if ($this->resourceClassResolver->isResourceClass($class)) {
  271. $resourceClass = $this->resourceClassResolver->getResourceClass($objectToPopulate, $class);
  272. $context['resource_class'] = $resourceClass;
  273. }
  274. $supportsPlainIdentifiers = $this->supportsPlainIdentifiers();
  275. if (\is_string($data)) {
  276. try {
  277. return $this->iriConverter instanceof LegacyIriConverterInterface ? $this->iriConverter->getItemFromIri($data, $context + ['fetch_data' => true]) : $this->iriConverter->getResourceFromIri($data, $context + ['fetch_data' => true]);
  278. } catch (ItemNotFoundException $e) {
  279. if (!$supportsPlainIdentifiers) {
  280. throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
  281. }
  282. } catch (InvalidArgumentException $e) {
  283. if (!$supportsPlainIdentifiers) {
  284. throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e);
  285. }
  286. }
  287. }
  288. if (!\is_array($data)) {
  289. if (!$supportsPlainIdentifiers) {
  290. throw new UnexpectedValueException(sprintf('Expected IRI or document for resource "%s", "%s" given.', $resourceClass, \gettype($data)));
  291. }
  292. $item = $this->itemDataProvider->getItem($resourceClass, $data, null, $context + ['fetch_data' => true]);
  293. if (null === $item) {
  294. throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $resourceClass, $data));
  295. }
  296. return $item;
  297. }
  298. $previousObject = $this->clone($objectToPopulate);
  299. $object = parent::denormalize($data, $resourceClass, $format, $context);
  300. if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
  301. return $object;
  302. }
  303. // Bypass the post-denormalize attribute revert logic if the object could not be
  304. // cloned since we cannot possibly revert any changes made to it.
  305. if (null !== $objectToPopulate && null === $previousObject) {
  306. return $object;
  307. }
  308. $options = $this->getFactoryOptions($context);
  309. $propertyNames = iterator_to_array($this->propertyNameCollectionFactory->create($resourceClass, $options));
  310. // Revert attributes that aren't allowed to be changed after a post-denormalize check
  311. foreach (array_keys($data) as $attribute) {
  312. $attribute = $this->nameConverter ? $this->nameConverter->denormalize((string) $attribute) : $attribute;
  313. if (!\in_array($attribute, $propertyNames, true)) {
  314. continue;
  315. }
  316. if (!$this->canAccessAttributePostDenormalize($object, $previousObject, $attribute, $context)) {
  317. if (null !== $previousObject) {
  318. $this->setValue($object, $attribute, $this->propertyAccessor->getValue($previousObject, $attribute));
  319. } else {
  320. $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $attribute, $options);
  321. $this->setValue($object, $attribute, $propertyMetadata->getDefault());
  322. }
  323. }
  324. }
  325. return $object;
  326. }
  327. /**
  328. * Method copy-pasted from symfony/serializer.
  329. * Remove it after symfony/serializer version update @see https://github.com/symfony/symfony/pull/28263.
  330. *
  331. * @internal
  332. *
  333. * @return object
  334. */
  335. protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
  336. {
  337. if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
  338. unset($context[static::OBJECT_TO_POPULATE]);
  339. return $object;
  340. }
  341. $class = $this->getClassDiscriminatorResolvedClass($data, $class);
  342. $reflectionClass = new \ReflectionClass($class);
  343. $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
  344. if ($constructor) {
  345. $constructorParameters = $constructor->getParameters();
  346. $params = [];
  347. foreach ($constructorParameters as $constructorParameter) {
  348. $paramName = $constructorParameter->name;
  349. $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
  350. $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true));
  351. $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
  352. if ($constructorParameter->isVariadic()) {
  353. if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
  354. if (!\is_array($data[$paramName])) {
  355. 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));
  356. }
  357. $params = array_merge($params, $data[$paramName]);
  358. }
  359. } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
  360. $params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $context, $format);
  361. // Don't run set for a parameter passed to the constructor
  362. unset($data[$key]);
  363. } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
  364. $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
  365. } elseif ($constructorParameter->isDefaultValueAvailable()) {
  366. $params[] = $constructorParameter->getDefaultValue();
  367. } else {
  368. 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));
  369. }
  370. }
  371. if ($constructor->isConstructor()) {
  372. return $reflectionClass->newInstanceArgs($params);
  373. }
  374. return $constructor->invokeArgs(null, $params);
  375. }
  376. return new $class();
  377. }
  378. protected function getClassDiscriminatorResolvedClass(array &$data, string $class): string
  379. {
  380. if (null === $this->classDiscriminatorResolver || (null === $mapping = $this->classDiscriminatorResolver->getMappingForClass($class))) {
  381. return $class;
  382. }
  383. if (!isset($data[$mapping->getTypeProperty()])) {
  384. throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping->getTypeProperty(), $class));
  385. }
  386. $type = $data[$mapping->getTypeProperty()];
  387. if (null === ($mappedClass = $mapping->getClassForType($type))) {
  388. throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"', $type, $class));
  389. }
  390. return $mappedClass;
  391. }
  392. protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array &$context, string $format = null)
  393. {
  394. return $this->createAttributeValue($constructorParameter->name, $parameterData, $format, $context);
  395. }
  396. /**
  397. * {@inheritdoc}
  398. *
  399. * Unused in this context.
  400. *
  401. * @return string[]
  402. */
  403. protected function extractAttributes($object, $format = null, array $context = [])
  404. {
  405. return [];
  406. }
  407. /**
  408. * @return array|bool
  409. */
  410. protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
  411. {
  412. if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
  413. return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
  414. }
  415. $resourceClass = $this->resourceClassResolver->getResourceClass(null, $context['resource_class']); // fix for abstract classes and interfaces
  416. $options = $this->getFactoryOptions($context);
  417. $propertyNames = $this->propertyNameCollectionFactory->create($resourceClass, $options);
  418. $allowedAttributes = [];
  419. foreach ($propertyNames as $propertyName) {
  420. $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
  421. if (
  422. $this->isAllowedAttribute($classOrObject, $propertyName, null, $context)
  423. && (
  424. isset($context['api_normalize']) && $propertyMetadata->isReadable()
  425. || isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable())
  426. )
  427. ) {
  428. $allowedAttributes[] = $propertyName;
  429. }
  430. }
  431. return $allowedAttributes;
  432. }
  433. /**
  434. * @param mixed|null $format
  435. *
  436. * @return bool
  437. */
  438. protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = [])
  439. {
  440. if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
  441. return false;
  442. }
  443. return $this->canAccessAttribute(\is_object($classOrObject) ? $classOrObject : null, $attribute, $context);
  444. }
  445. /**
  446. * Check if access to the attribute is granted.
  447. *
  448. * @param object $object
  449. */
  450. protected function canAccessAttribute($object, string $attribute, array $context = []): bool
  451. {
  452. if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
  453. return true;
  454. }
  455. $options = $this->getFactoryOptions($context);
  456. /** @var PropertyMetadata|ApiProperty */
  457. $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
  458. $security = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getAttribute('security') : $propertyMetadata->getSecurity();
  459. if ($this->resourceAccessChecker && $security) {
  460. return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
  461. 'object' => $object,
  462. ]);
  463. }
  464. return true;
  465. }
  466. /**
  467. * Check if access to the attribute is granted.
  468. *
  469. * @param object $object
  470. * @param object|null $previousObject
  471. */
  472. protected function canAccessAttributePostDenormalize($object, $previousObject, string $attribute, array $context = []): bool
  473. {
  474. $options = $this->getFactoryOptions($context);
  475. /** @var PropertyMetadata|ApiProperty */
  476. $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
  477. $security = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getAttribute('security_post_denormalize') : $propertyMetadata->getSecurityPostDenormalize();
  478. if ($this->resourceAccessChecker && $security) {
  479. return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
  480. 'object' => $object,
  481. 'previous_object' => $previousObject,
  482. ]);
  483. }
  484. return true;
  485. }
  486. protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = [])
  487. {
  488. $this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context));
  489. }
  490. /**
  491. * Validates the type of the value. Allows using integers as floats for JSON formats.
  492. *
  493. * @throws InvalidArgumentException
  494. */
  495. protected function validateType(string $attribute, Type $type, $value, string $format = null)
  496. {
  497. $builtinType = $type->getBuiltinType();
  498. if (Type::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && str_contains($format, 'json')) {
  499. $isValid = \is_float($value) || \is_int($value);
  500. } else {
  501. $isValid = \call_user_func('is_'.$builtinType, $value);
  502. }
  503. if (!$isValid) {
  504. throw new UnexpectedValueException(sprintf('The type of the "%s" attribute must be "%s", "%s" given.', $attribute, $builtinType, \gettype($value)));
  505. }
  506. }
  507. /**
  508. * Denormalizes a collection of objects.
  509. *
  510. * @param ApiProperty|PropertyMetadata $propertyMetadata
  511. *
  512. * @throws InvalidArgumentException
  513. */
  514. protected function denormalizeCollection(string $attribute, $propertyMetadata, Type $type, string $className, $value, ?string $format, array $context): array
  515. {
  516. if (!\is_array($value)) {
  517. throw new InvalidArgumentException(sprintf('The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value)));
  518. }
  519. $collectionKeyType = method_exists(Type::class, 'getCollectionKeyTypes') ? ($type->getCollectionKeyTypes()[0] ?? null) : $type->getCollectionKeyType();
  520. $collectionKeyBuiltinType = null === $collectionKeyType ? null : $collectionKeyType->getBuiltinType();
  521. $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
  522. $values = [];
  523. foreach ($value as $index => $obj) {
  524. if (null !== $collectionKeyBuiltinType && !\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
  525. throw new InvalidArgumentException(sprintf('The type of the key "%s" must be "%s", "%s" given.', $index, $collectionKeyBuiltinType, \gettype($index)));
  526. }
  527. $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $childContext);
  528. }
  529. return $values;
  530. }
  531. /**
  532. * Denormalizes a relation.
  533. *
  534. * @param ApiProperty|PropertyMetadata $propertyMetadata
  535. *
  536. * @throws LogicException
  537. * @throws UnexpectedValueException
  538. * @throws ItemNotFoundException
  539. *
  540. * @return object|null
  541. */
  542. protected function denormalizeRelation(string $attributeName, $propertyMetadata, string $className, $value, ?string $format, array $context)
  543. {
  544. $supportsPlainIdentifiers = $this->supportsPlainIdentifiers();
  545. if (\is_string($value)) {
  546. try {
  547. return $this->iriConverter instanceof LegacyIriConverterInterface ? $this->iriConverter->getItemFromIri($value, $context + ['fetch_data' => true]) : $this->iriConverter->getResourceFromIri($value, $context + ['fetch_data' => true]);
  548. } catch (ItemNotFoundException $e) {
  549. if (!$supportsPlainIdentifiers) {
  550. throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
  551. }
  552. } catch (InvalidArgumentException $e) {
  553. if (!$supportsPlainIdentifiers) {
  554. throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e);
  555. }
  556. }
  557. }
  558. if ($propertyMetadata->isWritableLink()) {
  559. $context['api_allow_update'] = true;
  560. if (!$this->serializer instanceof DenormalizerInterface) {
  561. throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
  562. }
  563. try {
  564. $item = $this->serializer->denormalize($value, $className, $format, $context);
  565. if (!\is_object($item) && null !== $item) {
  566. throw new \UnexpectedValueException('Expected item to be an object or null.');
  567. }
  568. return $item;
  569. } catch (InvalidValueException $e) {
  570. if (!$supportsPlainIdentifiers) {
  571. throw $e;
  572. }
  573. }
  574. }
  575. if (!\is_array($value)) {
  576. if (!$supportsPlainIdentifiers) {
  577. throw new UnexpectedValueException(sprintf('Expected IRI or nested document for attribute "%s", "%s" given.', $attributeName, \gettype($value)));
  578. }
  579. $item = $this->itemDataProvider->getItem($className, $value, null, $context + ['fetch_data' => true]);
  580. if (null === $item) {
  581. throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $className, $value));
  582. }
  583. return $item;
  584. }
  585. throw new UnexpectedValueException(sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.', $attributeName));
  586. }
  587. /**
  588. * Gets the options for the property name collection / property metadata factories.
  589. */
  590. protected function getFactoryOptions(array $context): array
  591. {
  592. $options = [];
  593. if (isset($context[self::GROUPS])) {
  594. /* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */
  595. $options['serializer_groups'] = (array) $context[self::GROUPS];
  596. }
  597. if (isset($context['resource_class']) && $this->resourceClassResolver->isResourceClass($context['resource_class']) && $this->resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) {
  598. $resourceClass = $this->resourceClassResolver->getResourceClass(null, $context['resource_class']); // fix for abstract classes and interfaces
  599. // This is a hot spot, we should avoid calling this here but in many cases we can't
  600. $operation = $context['root_operation'] ?? $context['operation'] ?? $this->resourceMetadataFactory->create($resourceClass)->getOperation($context['root_operation_name'] ?? $context['operation_name'] ?? null);
  601. $options['normalization_groups'] = $operation->getNormalizationContext()['groups'] ?? null;
  602. $options['denormalization_groups'] = $operation->getDenormalizationContext()['groups'] ?? null;
  603. }
  604. if (isset($context['operation_name'])) {
  605. $options['operation_name'] = $context['operation_name'];
  606. }
  607. // Preserve this context here since its deprecated in 2.7 and removed in 3.0.
  608. if (isset($context['collection_operation_name'])) {
  609. $options['collection_operation_name'] = $context['collection_operation_name'];
  610. }
  611. if (isset($context['item_operation_name'])) {
  612. $options['item_operation_name'] = $context['item_operation_name'];
  613. }
  614. return $options;
  615. }
  616. /**
  617. * Creates the context to use when serializing a relation.
  618. *
  619. * @deprecated since version 2.1, to be removed in 3.0.
  620. */
  621. protected function createRelationSerializationContext(string $resourceClass, array $context): array
  622. {
  623. @trigger_error(sprintf('The method %s() is deprecated since 2.1 and will be removed in 3.0.', __METHOD__), \E_USER_DEPRECATED);
  624. return $context;
  625. }
  626. /**
  627. * @param mixed|null $format
  628. *
  629. * @throws UnexpectedValueException
  630. * @throws LogicException
  631. */
  632. protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
  633. {
  634. $context['api_attribute'] = $attribute;
  635. /** @var ApiProperty|PropertyMetadata */
  636. $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
  637. try {
  638. $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
  639. } catch (NoSuchPropertyException $e) {
  640. // BC to be removed in 3.0
  641. if ($propertyMetadata instanceof PropertyMetadata && !$propertyMetadata->hasChildInherited()) {
  642. throw $e;
  643. }
  644. if ($propertyMetadata instanceof ApiProperty) {
  645. throw $e;
  646. }
  647. $attributeValue = null;
  648. }
  649. if ($context['api_denormalize'] ?? false) {
  650. return $attributeValue;
  651. }
  652. $type = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getType() : ($propertyMetadata->getBuiltinTypes()[0] ?? null);
  653. if (
  654. $type
  655. && $type->isCollection()
  656. && ($collectionValueType = method_exists(Type::class, 'getCollectionValueTypes') ? ($type->getCollectionValueTypes()[0] ?? null) : $type->getCollectionValueType())
  657. && ($className = $collectionValueType->getClassName())
  658. && $this->resourceClassResolver->isResourceClass($className)
  659. ) {
  660. if (!is_iterable($attributeValue)) {
  661. throw new UnexpectedValueException('Unexpected non-iterable value for to-many relation.');
  662. }
  663. $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
  664. $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format);
  665. return $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
  666. }
  667. if (
  668. $type
  669. && ($className = $type->getClassName())
  670. && $this->resourceClassResolver->isResourceClass($className)
  671. ) {
  672. if (!\is_object($attributeValue) && null !== $attributeValue) {
  673. throw new UnexpectedValueException('Unexpected non-object value for to-one relation.');
  674. }
  675. $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
  676. $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format);
  677. return $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
  678. }
  679. if (!$this->serializer instanceof NormalizerInterface) {
  680. throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
  681. }
  682. unset($context['resource_class']);
  683. if ($type && $type->getClassName()) {
  684. $childContext = $this->createChildContext($context, $attribute, $format);
  685. unset($childContext['iri'], $childContext['uri_variables']);
  686. if ($propertyMetadata instanceof PropertyMetadata) {
  687. $childContext['output']['iri'] = $propertyMetadata->getIri() ?? false;
  688. } else {
  689. $childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? false;
  690. }
  691. return $this->serializer->normalize($attributeValue, $format, $childContext);
  692. }
  693. return $this->serializer->normalize($attributeValue, $format, $context);
  694. }
  695. /**
  696. * Normalizes a collection of relations (to-many).
  697. *
  698. * @param ApiProperty|PropertyMetadata $propertyMetadata
  699. * @param iterable $attributeValue
  700. *
  701. * @throws UnexpectedValueException
  702. */
  703. protected function normalizeCollectionOfRelations($propertyMetadata, $attributeValue, string $resourceClass, ?string $format, array $context): array
  704. {
  705. $value = [];
  706. foreach ($attributeValue as $index => $obj) {
  707. if (!\is_object($obj) && null !== $obj) {
  708. throw new UnexpectedValueException('Unexpected non-object element in to-many relation.');
  709. }
  710. $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
  711. }
  712. return $value;
  713. }
  714. /**
  715. * Normalizes a relation.
  716. *
  717. * @param ApiProperty|PropertyMetadata $propertyMetadata
  718. * @param object|null $relatedObject
  719. *
  720. * @throws LogicException
  721. * @throws UnexpectedValueException
  722. *
  723. * @return string|array|\ArrayObject|null IRI or normalized object data
  724. */
  725. protected function normalizeRelation($propertyMetadata, $relatedObject, string $resourceClass, ?string $format, array $context)
  726. {
  727. if (null === $relatedObject || !empty($context['attributes']) || $propertyMetadata->isReadableLink()) {
  728. if (!$this->serializer instanceof NormalizerInterface) {
  729. throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
  730. }
  731. $relatedContext = $this->createOperationContext($context, $resourceClass);
  732. $normalizedRelatedObject = $this->serializer->normalize($relatedObject, $format, $relatedContext);
  733. if (!\is_string($normalizedRelatedObject) && !\is_array($normalizedRelatedObject) && !$normalizedRelatedObject instanceof \ArrayObject && null !== $normalizedRelatedObject) {
  734. throw new UnexpectedValueException('Expected normalized relation to be an IRI, array, \ArrayObject or null');
  735. }
  736. return $normalizedRelatedObject;
  737. }
  738. $iri = $this->iriConverter instanceof LegacyIriConverterInterface ? $this->iriConverter->getIriFromItem($relatedObject) : $this->iriConverter->getIriFromResource($relatedObject);
  739. if (isset($context['resources'])) {
  740. $context['resources'][$iri] = $iri;
  741. }
  742. $push = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getAttribute('push', false) : ($propertyMetadata->getPush() ?? false);
  743. if (isset($context['resources_to_push']) && $push) {
  744. $context['resources_to_push'][$iri] = $iri;
  745. }
  746. return $iri;
  747. }
  748. /**
  749. * Finds the first supported data transformer if any.
  750. *
  751. * @param object|array $data object on normalize / array on denormalize
  752. */
  753. protected function getDataTransformer($data, string $to, array $context = []): ?DataTransformerInterface
  754. {
  755. foreach ($this->dataTransformers as $dataTransformer) {
  756. if ($dataTransformer->supportsTransformation($data, $to, $context)) {
  757. return $dataTransformer;
  758. }
  759. }
  760. return null;
  761. }
  762. /**
  763. * For a given resource, it returns an output representation if any
  764. * If not, the resource is returned.
  765. */
  766. protected function transformOutput($object, array $context = [], string $outputClass = null)
  767. {
  768. }
  769. private function createAttributeValue($attribute, $value, $format = null, array $context = [])
  770. {
  771. if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
  772. return $value;
  773. }
  774. /** @var ApiProperty|PropertyMetadata */
  775. $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
  776. $type = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getType() : ($propertyMetadata->getBuiltinTypes()[0] ?? null);
  777. if (null === $type) {
  778. // No type provided, blindly return the value
  779. return $value;
  780. }
  781. if (null === $value && $type->isNullable()) {
  782. return $value;
  783. }
  784. $collectionValueType = method_exists(Type::class, 'getCollectionValueTypes') ? ($type->getCollectionValueTypes()[0] ?? null) : $type->getCollectionValueType();
  785. /* From @see AbstractObjectNormalizer::validateAndDenormalize() */
  786. // Fix a collection that contains the only one element
  787. // This is special to xml format only
  788. if ('xml' === $format && null !== $collectionValueType && (!\is_array($value) || !\is_int(key($value)))) {
  789. $value = [$value];
  790. }
  791. if (
  792. $type->isCollection()
  793. && null !== $collectionValueType
  794. && null !== ($className = $collectionValueType->getClassName())
  795. && $this->resourceClassResolver->isResourceClass($className)
  796. ) {
  797. $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
  798. $context['resource_class'] = $resourceClass;
  799. return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $resourceClass, $value, $format, $context);
  800. }
  801. if (
  802. null !== ($className = $type->getClassName())
  803. && $this->resourceClassResolver->isResourceClass($className)
  804. ) {
  805. $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
  806. $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format);
  807. return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
  808. }
  809. if (
  810. $type->isCollection()
  811. && null !== $collectionValueType
  812. && null !== ($className = $collectionValueType->getClassName())
  813. ) {
  814. if (!$this->serializer instanceof DenormalizerInterface) {
  815. throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
  816. }
  817. unset($context['resource_class']);
  818. return $this->serializer->denormalize($value, $className.'[]', $format, $context);
  819. }
  820. if (null !== $className = $type->getClassName()) {
  821. if (!$this->serializer instanceof DenormalizerInterface) {
  822. throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
  823. }
  824. unset($context['resource_class']);
  825. return $this->serializer->denormalize($value, $className, $format, $context);
  826. }
  827. /* From @see AbstractObjectNormalizer::validateAndDenormalize() */
  828. // In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine,
  829. // if a value is meant to be a string, float, int or a boolean value from the serialized representation.
  830. // That's why we have to transform the values, if one of these non-string basic datatypes is expected.
  831. if (\is_string($value) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) {
  832. if ('' === $value && $type->isNullable() && \in_array($type->getBuiltinType(), [Type::BUILTIN_TYPE_BOOL, Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_FLOAT], true)) {
  833. return null;
  834. }
  835. switch ($type->getBuiltinType()) {
  836. case Type::BUILTIN_TYPE_BOOL:
  837. // according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
  838. if ('false' === $value || '0' === $value) {
  839. $value = false;
  840. } elseif ('true' === $value || '1' === $value) {
  841. $value = true;
  842. } else {
  843. throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $className, $value));
  844. }
  845. break;
  846. case Type::BUILTIN_TYPE_INT:
  847. if (ctype_digit($value) || ('-' === $value[0] && ctype_digit(substr($value, 1)))) {
  848. $value = (int) $value;
  849. } else {
  850. throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $className, $value));
  851. }
  852. break;
  853. case Type::BUILTIN_TYPE_FLOAT:
  854. if (is_numeric($value)) {
  855. return (float) $value;
  856. }
  857. switch ($value) {
  858. case 'NaN':
  859. return \NAN;
  860. case 'INF':
  861. return \INF;
  862. case '-INF':
  863. return -\INF;
  864. default:
  865. throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $className, $value));
  866. }
  867. }
  868. }
  869. if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) {
  870. return $value;
  871. }
  872. $this->validateType($attribute, $type, $value, $format);
  873. return $value;
  874. }
  875. /**
  876. * Sets a value of the object using the PropertyAccess component.
  877. *
  878. * @param object $object
  879. */
  880. private function setValue($object, string $attributeName, $value)
  881. {
  882. try {
  883. $this->propertyAccessor->setValue($object, $attributeName, $value);
  884. } catch (NoSuchPropertyException $exception) {
  885. // Properties not found are ignored
  886. }
  887. }
  888. /**
  889. * TODO: to remove in 3.0.
  890. *
  891. * @deprecated since 2.7
  892. */
  893. private function supportsPlainIdentifiers(): bool
  894. {
  895. return $this->allowPlainIdentifiers && null !== $this->itemDataProvider;
  896. }
  897. }
  898. class_alias(AbstractItemNormalizer::class, \ApiPlatform\Core\Serializer\AbstractItemNormalizer::class);