vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php line 192

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.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. namespace Symfony\Component\Serializer\Normalizer;
  11. use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException as PropertyAccessInvalidArgumentException;
  12. use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
  13. use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
  14. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  15. use Symfony\Component\PropertyInfo\Type;
  16. use Symfony\Component\Serializer\Encoder\CsvEncoder;
  17. use Symfony\Component\Serializer\Encoder\JsonEncoder;
  18. use Symfony\Component\Serializer\Encoder\XmlEncoder;
  19. use Symfony\Component\Serializer\Exception\ExtraAttributesException;
  20. use Symfony\Component\Serializer\Exception\InvalidArgumentException;
  21. use Symfony\Component\Serializer\Exception\LogicException;
  22. use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
  23. use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
  24. use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
  25. use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
  26. use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
  27. use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
  28. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  29. /**
  30.  * Base class for a normalizer dealing with objects.
  31.  *
  32.  * @author Kévin Dunglas <dunglas@gmail.com>
  33.  */
  34. abstract class AbstractObjectNormalizer extends AbstractNormalizer
  35. {
  36.     /**
  37.      * Set to true to respect the max depth metadata on fields.
  38.      */
  39.     public const ENABLE_MAX_DEPTH 'enable_max_depth';
  40.     /**
  41.      * How to track the current depth in the context.
  42.      */
  43.     public const DEPTH_KEY_PATTERN 'depth_%s::%s';
  44.     /**
  45.      * While denormalizing, we can verify that types match.
  46.      *
  47.      * You can disable this by setting this flag to true.
  48.      */
  49.     public const DISABLE_TYPE_ENFORCEMENT 'disable_type_enforcement';
  50.     /**
  51.      * Flag to control whether fields with the value `null` should be output
  52.      * when normalizing or omitted.
  53.      */
  54.     public const SKIP_NULL_VALUES 'skip_null_values';
  55.     /**
  56.      * Flag to control whether uninitialized PHP>=7.4 typed class properties
  57.      * should be excluded when normalizing.
  58.      */
  59.     public const SKIP_UNINITIALIZED_VALUES 'skip_uninitialized_values';
  60.     /**
  61.      * Callback to allow to set a value for an attribute when the max depth has
  62.      * been reached.
  63.      *
  64.      * If no callback is given, the attribute is skipped. If a callable is
  65.      * given, its return value is used (even if null).
  66.      *
  67.      * The arguments are:
  68.      *
  69.      * - mixed  $attributeValue value of this field
  70.      * - object $object         the whole object being normalized
  71.      * - string $attributeName  name of the attribute being normalized
  72.      * - string $format         the requested format
  73.      * - array  $context        the serialization context
  74.      */
  75.     public const MAX_DEPTH_HANDLER 'max_depth_handler';
  76.     /**
  77.      * Specify which context key are not relevant to determine which attributes
  78.      * of an object to (de)normalize.
  79.      */
  80.     public const EXCLUDE_FROM_CACHE_KEY 'exclude_from_cache_key';
  81.     /**
  82.      * Flag to tell the denormalizer to also populate existing objects on
  83.      * attributes of the main object.
  84.      *
  85.      * Setting this to true is only useful if you also specify the root object
  86.      * in OBJECT_TO_POPULATE.
  87.      */
  88.     public const DEEP_OBJECT_TO_POPULATE 'deep_object_to_populate';
  89.     /**
  90.      * Flag to control whether an empty object should be kept as an object (in
  91.      * JSON: {}) or converted to a list (in JSON: []).
  92.      */
  93.     public const PRESERVE_EMPTY_OBJECTS 'preserve_empty_objects';
  94.     private $propertyTypeExtractor;
  95.     private $typesCache = [];
  96.     private $attributesCache = [];
  97.     private $objectClassResolver;
  98.     /**
  99.      * @var ClassDiscriminatorResolverInterface|null
  100.      */
  101.     protected $classDiscriminatorResolver;
  102.     public function __construct(?ClassMetadataFactoryInterface $classMetadataFactory null, ?NameConverterInterface $nameConverter null, ?PropertyTypeExtractorInterface $propertyTypeExtractor null, ?ClassDiscriminatorResolverInterface $classDiscriminatorResolver null, ?callable $objectClassResolver null, array $defaultContext = [])
  103.     {
  104.         parent::__construct($classMetadataFactory$nameConverter$defaultContext);
  105.         if (isset($this->defaultContext[self::MAX_DEPTH_HANDLER]) && !\is_callable($this->defaultContext[self::MAX_DEPTH_HANDLER])) {
  106.             throw new InvalidArgumentException(sprintf('The "%s" given in the default context is not callable.'self::MAX_DEPTH_HANDLER));
  107.         }
  108.         $this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] = array_merge($this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] ?? [], [self::CIRCULAR_REFERENCE_LIMIT_COUNTERS]);
  109.         $this->propertyTypeExtractor $propertyTypeExtractor;
  110.         if (null === $classDiscriminatorResolver && null !== $classMetadataFactory) {
  111.             $classDiscriminatorResolver = new ClassDiscriminatorFromClassMetadata($classMetadataFactory);
  112.         }
  113.         $this->classDiscriminatorResolver $classDiscriminatorResolver;
  114.         $this->objectClassResolver $objectClassResolver;
  115.     }
  116.     /**
  117.      * {@inheritdoc}
  118.      */
  119.     public function supportsNormalization($data, ?string $format null)
  120.     {
  121.         return \is_object($data) && !$data instanceof \Traversable;
  122.     }
  123.     /**
  124.      * {@inheritdoc}
  125.      */
  126.     public function normalize($object, ?string $format null, array $context = [])
  127.     {
  128.         $context['_read_attributes'] = true;
  129.         if (!isset($context['cache_key'])) {
  130.             $context['cache_key'] = $this->getCacheKey($format$context);
  131.         }
  132.         $this->validateCallbackContext($context);
  133.         if ($this->isCircularReference($object$context)) {
  134.             return $this->handleCircularReference($object$format$context);
  135.         }
  136.         $data = [];
  137.         $stack = [];
  138.         $attributes $this->getAttributes($object$format$context);
  139.         $class $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  140.         $attributesMetadata $this->classMetadataFactory $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
  141.         if (isset($context[self::MAX_DEPTH_HANDLER])) {
  142.             $maxDepthHandler $context[self::MAX_DEPTH_HANDLER];
  143.             if (!\is_callable($maxDepthHandler)) {
  144.                 throw new InvalidArgumentException(sprintf('The "%s" given in the context is not callable.'self::MAX_DEPTH_HANDLER));
  145.             }
  146.         } else {
  147.             $maxDepthHandler null;
  148.         }
  149.         foreach ($attributes as $attribute) {
  150.             $maxDepthReached false;
  151.             if (null !== $attributesMetadata && ($maxDepthReached $this->isMaxDepthReached($attributesMetadata$class$attribute$context)) && !$maxDepthHandler) {
  152.                 continue;
  153.             }
  154.             $attributeContext $this->getAttributeNormalizationContext($object$attribute$context);
  155.             $discriminatorProperty null;
  156.             if (null !== $this->classDiscriminatorResolver && null !== $mapping $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
  157.                 $discriminatorProperty $mapping->getTypeProperty();
  158.             }
  159.             try {
  160.                 $attributeValue $attribute === $discriminatorProperty
  161.                     $this->classDiscriminatorResolver->getTypeForMappedObject($object)
  162.                     : $this->getAttributeValue($object$attribute$format$attributeContext);
  163.             } catch (UninitializedPropertyException $e) {
  164.                 if ($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) {
  165.                     continue;
  166.                 }
  167.                 throw $e;
  168.             } catch (\Error $e) {
  169.                 if (($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) && $this->isUninitializedValueError($e)) {
  170.                     continue;
  171.                 }
  172.                 throw $e;
  173.             }
  174.             if ($maxDepthReached) {
  175.                 $attributeValue $maxDepthHandler($attributeValue$object$attribute$format$attributeContext);
  176.             }
  177.             $attributeValue $this->applyCallbacks($attributeValue$object$attribute$format$attributeContext);
  178.             if (null !== $attributeValue && !\is_scalar($attributeValue)) {
  179.                 $stack[$attribute] = $attributeValue;
  180.             }
  181.             $data $this->updateData($data$attribute$attributeValue$class$format$attributeContext);
  182.         }
  183.         foreach ($stack as $attribute => $attributeValue) {
  184.             if (!$this->serializer instanceof NormalizerInterface) {
  185.                 throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer.'$attribute));
  186.             }
  187.             $attributeContext $this->getAttributeNormalizationContext($object$attribute$context);
  188.             $childContext $this->createChildContext($attributeContext$attribute$format);
  189.             $data $this->updateData($data$attribute$this->serializer->normalize($attributeValue$format$childContext), $class$format$attributeContext);
  190.         }
  191.         if (isset($context[self::PRESERVE_EMPTY_OBJECTS]) && !\count($data)) {
  192.             return new \ArrayObject();
  193.         }
  194.         return $data;
  195.     }
  196.     /**
  197.      * Computes the normalization context merged with current one. Metadata always wins over global context, as more specific.
  198.      */
  199.     private function getAttributeNormalizationContext(object $objectstring $attribute, array $context): array
  200.     {
  201.         if (null === $metadata $this->getAttributeMetadata($object$attribute)) {
  202.             return $context;
  203.         }
  204.         return array_merge($context$metadata->getNormalizationContextForGroups($this->getGroups($context)));
  205.     }
  206.     /**
  207.      * Computes the denormalization context merged with current one. Metadata always wins over global context, as more specific.
  208.      */
  209.     private function getAttributeDenormalizationContext(string $classstring $attribute, array $context): array
  210.     {
  211.         $context['deserialization_path'] = ($context['deserialization_path'] ?? false) ? $context['deserialization_path'].'.'.$attribute $attribute;
  212.         if (null === $metadata $this->getAttributeMetadata($class$attribute)) {
  213.             return $context;
  214.         }
  215.         return array_merge($context$metadata->getDenormalizationContextForGroups($this->getGroups($context)));
  216.     }
  217.     private function getAttributeMetadata($objectOrClassstring $attribute): ?AttributeMetadataInterface
  218.     {
  219.         if (!$this->classMetadataFactory) {
  220.             return null;
  221.         }
  222.         return $this->classMetadataFactory->getMetadataFor($objectOrClass)->getAttributesMetadata()[$attribute] ?? null;
  223.     }
  224.     /**
  225.      * {@inheritdoc}
  226.      */
  227.     protected function instantiateObject(array &$datastring $class, array &$context, \ReflectionClass $reflectionClass$allowedAttributes, ?string $format null)
  228.     {
  229.         if (null !== $object $this->extractObjectToPopulate($class$contextself::OBJECT_TO_POPULATE)) {
  230.             unset($context[self::OBJECT_TO_POPULATE]);
  231.             return $object;
  232.         }
  233.         if ($this->classDiscriminatorResolver && $mapping $this->classDiscriminatorResolver->getMappingForClass($class)) {
  234.             if (!isset($data[$mapping->getTypeProperty()])) {
  235.                 throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".'$mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false);
  236.             }
  237.             $type $data[$mapping->getTypeProperty()];
  238.             if (null === ($mappedClass $mapping->getClassForType($type))) {
  239.                 throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.'$type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true);
  240.             }
  241.             if ($mappedClass !== $class) {
  242.                 return $this->instantiateObject($data$mappedClass$context, new \ReflectionClass($mappedClass), $allowedAttributes$format);
  243.             }
  244.         }
  245.         return parent::instantiateObject($data$class$context$reflectionClass$allowedAttributes$format);
  246.     }
  247.     /**
  248.      * Gets and caches attributes for the given object, format and context.
  249.      *
  250.      * @return string[]
  251.      */
  252.     protected function getAttributes(object $object, ?string $format, array $context)
  253.     {
  254.         $class $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  255.         $key $class.'-'.$context['cache_key'];
  256.         if (isset($this->attributesCache[$key])) {
  257.             return $this->attributesCache[$key];
  258.         }
  259.         $allowedAttributes $this->getAllowedAttributes($object$contexttrue);
  260.         if (false !== $allowedAttributes) {
  261.             if ($context['cache_key']) {
  262.                 $this->attributesCache[$key] = $allowedAttributes;
  263.             }
  264.             return $allowedAttributes;
  265.         }
  266.         $attributes $this->extractAttributes($object$format$context);
  267.         if ($this->classDiscriminatorResolver && $mapping $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
  268.             array_unshift($attributes$mapping->getTypeProperty());
  269.         }
  270.         if ($context['cache_key'] && \stdClass::class !== $class) {
  271.             $this->attributesCache[$key] = $attributes;
  272.         }
  273.         return $attributes;
  274.     }
  275.     /**
  276.      * Extracts attributes to normalize from the class of the given object, format and context.
  277.      *
  278.      * @return string[]
  279.      */
  280.     abstract protected function extractAttributes(object $object, ?string $format null, array $context = []);
  281.     /**
  282.      * Gets the attribute value.
  283.      *
  284.      * @return mixed
  285.      */
  286.     abstract protected function getAttributeValue(object $objectstring $attribute, ?string $format null, array $context = []);
  287.     /**
  288.      * {@inheritdoc}
  289.      */
  290.     public function supportsDenormalization($datastring $type, ?string $format null)
  291.     {
  292.         return class_exists($type) || (interface_exists($typefalse) && $this->classDiscriminatorResolver && null !== $this->classDiscriminatorResolver->getMappingForClass($type));
  293.     }
  294.     /**
  295.      * {@inheritdoc}
  296.      */
  297.     public function denormalize($datastring $type, ?string $format null, array $context = [])
  298.     {
  299.         $context['_read_attributes'] = false;
  300.         if (!isset($context['cache_key'])) {
  301.             $context['cache_key'] = $this->getCacheKey($format$context);
  302.         }
  303.         $this->validateCallbackContext($context);
  304.         if (null === $data && isset($context['value_type']) && $context['value_type'] instanceof Type && $context['value_type']->isNullable()) {
  305.             return null;
  306.         }
  307.         if (XmlEncoder::FORMAT === $format && !\is_array($data)) {
  308.             $data = ['#' => $data];
  309.         }
  310.         $allowedAttributes $this->getAllowedAttributes($type$contexttrue);
  311.         $normalizedData $this->prepareForDenormalization($data);
  312.         $extraAttributes = [];
  313.         $reflectionClass = new \ReflectionClass($type);
  314.         $object $this->instantiateObject($normalizedData$type$context$reflectionClass$allowedAttributes$format);
  315.         $resolvedClass $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  316.         foreach ($normalizedData as $attribute => $value) {
  317.             if ($this->nameConverter) {
  318.                 $attribute $this->nameConverter->denormalize($attribute$resolvedClass$format$context);
  319.             }
  320.             $attributeContext $this->getAttributeDenormalizationContext($resolvedClass$attribute$context);
  321.             if ((false !== $allowedAttributes && !\in_array($attribute$allowedAttributes)) || !$this->isAllowedAttribute($resolvedClass$attribute$format$context)) {
  322.                 if (!($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES])) {
  323.                     $extraAttributes[] = $attribute;
  324.                 }
  325.                 continue;
  326.             }
  327.             if ($attributeContext[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) {
  328.                 $discriminatorProperty null;
  329.                 if (null !== $this->classDiscriminatorResolver && null !== $mapping $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
  330.                     $discriminatorProperty $mapping->getTypeProperty();
  331.                 }
  332.                 try {
  333.                     $attributeContext[self::OBJECT_TO_POPULATE] = $attribute === $discriminatorProperty
  334.                         $this->classDiscriminatorResolver->getTypeForMappedObject($object)
  335.                         : $this->getAttributeValue($object$attribute$format$attributeContext);
  336.                 } catch (NoSuchPropertyException $e) {
  337.                 } catch (UninitializedPropertyException $e) {
  338.                     if (!($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true)) {
  339.                         throw $e;
  340.                     }
  341.                 } catch (\Error $e) {
  342.                     if (!(($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) && $this->isUninitializedValueError($e))) {
  343.                         throw $e;
  344.                     }
  345.                 }
  346.             }
  347.             $types $this->getTypes($resolvedClass$attribute);
  348.             if (null !== $types) {
  349.                 try {
  350.                     $value $this->validateAndDenormalize($types$resolvedClass$attribute$value$format$attributeContext);
  351.                 } catch (NotNormalizableValueException $exception) {
  352.                     if (isset($context['not_normalizable_value_exceptions'])) {
  353.                         $context['not_normalizable_value_exceptions'][] = $exception;
  354.                         continue;
  355.                     }
  356.                     throw $exception;
  357.                 }
  358.             }
  359.             $value $this->applyCallbacks($value$resolvedClass$attribute$format$attributeContext);
  360.             try {
  361.                 $this->setAttributeValue($object$attribute$value$format$attributeContext);
  362.             } catch (PropertyAccessInvalidArgumentException $e) {
  363.                 $exception NotNormalizableValueException::createForUnexpectedDataType(
  364.                     sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute$type),
  365.                     $data,
  366.                     ['unknown'],
  367.                     $attributeContext['deserialization_path'] ?? null,
  368.                     false,
  369.                     $e->getCode(),
  370.                     $e
  371.                 );
  372.                 if (isset($context['not_normalizable_value_exceptions'])) {
  373.                     $context['not_normalizable_value_exceptions'][] = $exception;
  374.                     continue;
  375.                 }
  376.                 throw $exception;
  377.             }
  378.         }
  379.         if ($extraAttributes) {
  380.             throw new ExtraAttributesException($extraAttributes);
  381.         }
  382.         return $object;
  383.     }
  384.     /**
  385.      * Sets attribute value.
  386.      */
  387.     abstract protected function setAttributeValue(object $objectstring $attribute$value, ?string $format null, array $context = []);
  388.     /**
  389.      * Validates the submitted data and denormalizes it.
  390.      *
  391.      * @param Type[] $types
  392.      * @param mixed  $data
  393.      *
  394.      * @return mixed
  395.      *
  396.      * @throws NotNormalizableValueException
  397.      * @throws ExtraAttributesException
  398.      * @throws MissingConstructorArgumentsException
  399.      * @throws LogicException
  400.      */
  401.     private function validateAndDenormalize(array $typesstring $currentClassstring $attribute$data, ?string $format, array $context)
  402.     {
  403.         $expectedTypes = [];
  404.         $isUnionType = \count($types) > 1;
  405.         $e null;
  406.         $extraAttributesException null;
  407.         $missingConstructorArgumentException null;
  408.         $isNullable false;
  409.         foreach ($types as $type) {
  410.             if (null === $data && $type->isNullable()) {
  411.                 return null;
  412.             }
  413.             $collectionValueType $type->isCollection() ? $type->getCollectionValueTypes()[0] ?? null null;
  414.             // Fix a collection that contains the only one element
  415.             // This is special to xml format only
  416.             if ('xml' === $format && null !== $collectionValueType && (!\is_array($data) || !\is_int(key($data)))) {
  417.                 $data = [$data];
  418.             }
  419.             // This try-catch should cover all NotNormalizableValueException (and all return branches after the first
  420.             // exception) so we could try denormalizing all types of an union type. If the target type is not an union
  421.             // type, we will just re-throw the catched exception.
  422.             // In the case of no denormalization succeeds with an union type, it will fall back to the default exception
  423.             // with the acceptable types list.
  424.             try {
  425.                 // In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine,
  426.                 // if a value is meant to be a string, float, int or a boolean value from the serialized representation.
  427.                 // That's why we have to transform the values, if one of these non-string basic datatypes is expected.
  428.                 $builtinType $type->getBuiltinType();
  429.                 if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) {
  430.                     if ('' === $data) {
  431.                         if (Type::BUILTIN_TYPE_ARRAY === $builtinType) {
  432.                             return [];
  433.                         }
  434.                         if (Type::BUILTIN_TYPE_STRING === $builtinType) {
  435.                             return '';
  436.                         }
  437.                         // Don't return null yet because Object-types that come first may accept empty-string too
  438.                         $isNullable $isNullable ?: $type->isNullable();
  439.                     }
  440.                     switch ($builtinType) {
  441.                         case Type::BUILTIN_TYPE_BOOL:
  442.                             // according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
  443.                             if ('false' === $data || '0' === $data) {
  444.                                 $data false;
  445.                             } elseif ('true' === $data || '1' === $data) {
  446.                                 $data true;
  447.                             } else {
  448.                                 throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).'$attribute$currentClass$data), $data, [Type::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? null);
  449.                             }
  450.                             break;
  451.                         case Type::BUILTIN_TYPE_INT:
  452.                             if (ctype_digit($data) || isset($data[0]) && '-' === $data[0] && ctype_digit(substr($data1))) {
  453.                                 $data = (int) $data;
  454.                             } else {
  455.                                 throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).'$attribute$currentClass$data), $data, [Type::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null);
  456.                             }
  457.                             break;
  458.                         case Type::BUILTIN_TYPE_FLOAT:
  459.                             if (is_numeric($data)) {
  460.                                 return (float) $data;
  461.                             }
  462.                             switch ($data) {
  463.                                 case 'NaN':
  464.                                     return \NAN;
  465.                                 case 'INF':
  466.                                     return \INF;
  467.                                 case '-INF':
  468.                                     return -\INF;
  469.                                 default:
  470.                                     throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).'$attribute$currentClass$data), $data, [Type::BUILTIN_TYPE_FLOAT], $context['deserialization_path'] ?? null);
  471.                             }
  472.                     }
  473.                 }
  474.                 if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
  475.                     $builtinType Type::BUILTIN_TYPE_OBJECT;
  476.                     $class $collectionValueType->getClassName().'[]';
  477.                     if (\count($collectionKeyType $type->getCollectionKeyTypes()) > 0) {
  478.                         $context['key_type'] = \count($collectionKeyType) > $collectionKeyType $collectionKeyType[0];
  479.                     }
  480.                     $context['value_type'] = $collectionValueType;
  481.                 } elseif ($type->isCollection() && \count($collectionValueType $type->getCollectionValueTypes()) > && Type::BUILTIN_TYPE_ARRAY === $collectionValueType[0]->getBuiltinType()) {
  482.                     // get inner type for any nested array
  483.                     [$innerType] = $collectionValueType;
  484.                     // note that it will break for any other builtinType
  485.                     $dimensions '[]';
  486.                     while (\count($innerType->getCollectionValueTypes()) > && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) {
  487.                         $dimensions .= '[]';
  488.                         [$innerType] = $innerType->getCollectionValueTypes();
  489.                     }
  490.                     if (null !== $innerType->getClassName()) {
  491.                         // the builtinType is the inner one and the class is the class followed by []...[]
  492.                         $builtinType $innerType->getBuiltinType();
  493.                         $class $innerType->getClassName().$dimensions;
  494.                     } else {
  495.                         // default fallback (keep it as array)
  496.                         $builtinType $type->getBuiltinType();
  497.                         $class $type->getClassName();
  498.                     }
  499.                 } else {
  500.                     $builtinType $type->getBuiltinType();
  501.                     $class $type->getClassName();
  502.                 }
  503.                 $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class $class $builtinType] = true;
  504.                 if (Type::BUILTIN_TYPE_OBJECT === $builtinType && null !== $class) {
  505.                     if (!$this->serializer instanceof DenormalizerInterface) {
  506.                         throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.'$attribute$class));
  507.                     }
  508.                     $childContext $this->createChildContext($context$attribute$format);
  509.                     if ($this->serializer->supportsDenormalization($data$class$format$childContext)) {
  510.                         return $this->serializer->denormalize($data$class$format$childContext);
  511.                     }
  512.                 }
  513.                 // JSON only has a Number type corresponding to both int and float PHP types.
  514.                 // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
  515.                 // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
  516.                 // PHP's json_decode automatically converts Numbers without a decimal part to integers.
  517.                 // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
  518.                 // a float is expected.
  519.                 if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && null !== $format && str_contains($formatJsonEncoder::FORMAT)) {
  520.                     return (float) $data;
  521.                 }
  522.                 if (Type::BUILTIN_TYPE_FALSE === $builtinType && false === $data) {
  523.                     return $data;
  524.                 }
  525.                 if (('is_'.$builtinType)($data)) {
  526.                     return $data;
  527.                 }
  528.             } catch (NotNormalizableValueException $e) {
  529.                 if (!$isUnionType && !$isNullable) {
  530.                     throw $e;
  531.                 }
  532.             } catch (ExtraAttributesException $e) {
  533.                 if (!$isUnionType && !$isNullable) {
  534.                     throw $e;
  535.                 }
  536.                 if (!$extraAttributesException) {
  537.                     $extraAttributesException $e;
  538.                 }
  539.             } catch (MissingConstructorArgumentsException $e) {
  540.                 if (!$isUnionType && !$isNullable) {
  541.                     throw $e;
  542.                 }
  543.                 if (!$missingConstructorArgumentException) {
  544.                     $missingConstructorArgumentException $e;
  545.                 }
  546.             }
  547.         }
  548.         if ($isNullable) {
  549.             return null;
  550.         }
  551.         if ($extraAttributesException) {
  552.             throw $extraAttributesException;
  553.         }
  554.         if ($missingConstructorArgumentException) {
  555.             throw $missingConstructorArgumentException;
  556.         }
  557.         if (!$isUnionType && $e) {
  558.             throw $e;
  559.         }
  560.         if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) {
  561.             return $data;
  562.         }
  563.         throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).'$attribute$currentClassimplode('", "'array_keys($expectedTypes)), get_debug_type($data)), $dataarray_keys($expectedTypes), $context['deserialization_path'] ?? $attribute);
  564.     }
  565.     /**
  566.      * @internal
  567.      */
  568.     protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameterstring $parameterName$parameterData, array $context, ?string $format null)
  569.     {
  570.         if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $types $this->getTypes($class->getName(), $parameterName)) {
  571.             return parent::denormalizeParameter($class$parameter$parameterName$parameterData$context$format);
  572.         }
  573.         $parameterData $this->validateAndDenormalize($types$class->getName(), $parameterName$parameterData$format$context);
  574.         return $this->applyCallbacks($parameterData$class->getName(), $parameterName$format$context);
  575.     }
  576.     /**
  577.      * @return Type[]|null
  578.      */
  579.     private function getTypes(string $currentClassstring $attribute): ?array
  580.     {
  581.         if (null === $this->propertyTypeExtractor) {
  582.             return null;
  583.         }
  584.         $key $currentClass.'::'.$attribute;
  585.         if (isset($this->typesCache[$key])) {
  586.             return false === $this->typesCache[$key] ? null $this->typesCache[$key];
  587.         }
  588.         if (null !== $types $this->propertyTypeExtractor->getTypes($currentClass$attribute)) {
  589.             return $this->typesCache[$key] = $types;
  590.         }
  591.         if (null !== $this->classDiscriminatorResolver && null !== $discriminatorMapping $this->classDiscriminatorResolver->getMappingForClass($currentClass)) {
  592.             if ($discriminatorMapping->getTypeProperty() === $attribute) {
  593.                 return $this->typesCache[$key] = [
  594.                     new Type(Type::BUILTIN_TYPE_STRING),
  595.                 ];
  596.             }
  597.             foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
  598.                 if (null !== $types $this->propertyTypeExtractor->getTypes($mappedClass$attribute)) {
  599.                     return $this->typesCache[$key] = $types;
  600.                 }
  601.             }
  602.         }
  603.         $this->typesCache[$key] = false;
  604.         return null;
  605.     }
  606.     /**
  607.      * Sets an attribute and apply the name converter if necessary.
  608.      *
  609.      * @param mixed $attributeValue
  610.      */
  611.     private function updateData(array $datastring $attribute$attributeValuestring $class, ?string $format, array $context): array
  612.     {
  613.         if (null === $attributeValue && ($context[self::SKIP_NULL_VALUES] ?? $this->defaultContext[self::SKIP_NULL_VALUES] ?? false)) {
  614.             return $data;
  615.         }
  616.         if ($this->nameConverter) {
  617.             $attribute $this->nameConverter->normalize($attribute$class$format$context);
  618.         }
  619.         $data[$attribute] = $attributeValue;
  620.         return $data;
  621.     }
  622.     /**
  623.      * Is the max depth reached for the given attribute?
  624.      *
  625.      * @param AttributeMetadataInterface[] $attributesMetadata
  626.      */
  627.     private function isMaxDepthReached(array $attributesMetadatastring $classstring $attribute, array &$context): bool
  628.     {
  629.         $enableMaxDepth $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false;
  630.         if (
  631.             !$enableMaxDepth ||
  632.             !isset($attributesMetadata[$attribute]) ||
  633.             null === $maxDepth $attributesMetadata[$attribute]->getMaxDepth()
  634.         ) {
  635.             return false;
  636.         }
  637.         $key sprintf(self::DEPTH_KEY_PATTERN$class$attribute);
  638.         if (!isset($context[$key])) {
  639.             $context[$key] = 1;
  640.             return false;
  641.         }
  642.         if ($context[$key] === $maxDepth) {
  643.             return true;
  644.         }
  645.         ++$context[$key];
  646.         return false;
  647.     }
  648.     /**
  649.      * Overwritten to update the cache key for the child.
  650.      *
  651.      * We must not mix up the attribute cache between parent and children.
  652.      *
  653.      * {@inheritdoc}
  654.      *
  655.      * @internal
  656.      */
  657.     protected function createChildContext(array $parentContextstring $attribute, ?string $format): array
  658.     {
  659.         $context parent::createChildContext($parentContext$attribute$format);
  660.         if ($context['cache_key'] ?? false) {
  661.             $context['cache_key'] .= '-'.$attribute;
  662.         } elseif (false !== ($context['cache_key'] ?? null)) {
  663.             $context['cache_key'] = $this->getCacheKey($format$context);
  664.         }
  665.         return $context;
  666.     }
  667.     /**
  668.      * Builds the cache key for the attributes cache.
  669.      *
  670.      * The key must be different for every option in the context that could change which attributes should be handled.
  671.      *
  672.      * @return bool|string
  673.      */
  674.     private function getCacheKey(?string $format, array $context)
  675.     {
  676.         foreach ($context[self::EXCLUDE_FROM_CACHE_KEY] ?? $this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] as $key) {
  677.             unset($context[$key]);
  678.         }
  679.         unset($context[self::EXCLUDE_FROM_CACHE_KEY]);
  680.         unset($context[self::OBJECT_TO_POPULATE]);
  681.         unset($context['cache_key']); // avoid artificially different keys
  682.         try {
  683.             return md5($format.serialize([
  684.                 'context' => $context,
  685.                 'ignored' => $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES],
  686.             ]));
  687.         } catch (\Exception $e) {
  688.             // The context cannot be serialized, skip the cache
  689.             return false;
  690.         }
  691.     }
  692.     /**
  693.      * This error may occur when specific object normalizer implementation gets attribute value
  694.      * by accessing a public uninitialized property or by calling a method accessing such property.
  695.      */
  696.     private function isUninitializedValueError(\Error $e): bool
  697.     {
  698.         return \PHP_VERSION_ID >= 70400
  699.             && str_starts_with($e->getMessage(), 'Typed property')
  700.             && str_ends_with($e->getMessage(), 'must not be accessed before initialization');
  701.     }
  702. }