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

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