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

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