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

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 nullNameConverterInterface $nameConverter nullPropertyTypeExtractorInterface $propertyTypeExtractor nullClassDiscriminatorResolverInterface $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($datastring $format null)
  120.     {
  121.         return \is_object($data) && !$data instanceof \Traversable;
  122.     }
  123.     /**
  124.      * {@inheritdoc}
  125.      */
  126.     public function normalize($objectstring $format null, array $context = [])
  127.     {
  128.         if (!isset($context['cache_key'])) {
  129.             $context['cache_key'] = $this->getCacheKey($format$context);
  130.         }
  131.         $this->validateCallbackContext($context);
  132.         if ($this->isCircularReference($object$context)) {
  133.             return $this->handleCircularReference($object$format$context);
  134.         }
  135.         $data = [];
  136.         $stack = [];
  137.         $attributes $this->getAttributes($object$format$context);
  138.         $class $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  139.         $attributesMetadata $this->classMetadataFactory $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
  140.         if (isset($context[self::MAX_DEPTH_HANDLER])) {
  141.             $maxDepthHandler $context[self::MAX_DEPTH_HANDLER];
  142.             if (!\is_callable($maxDepthHandler)) {
  143.                 throw new InvalidArgumentException(sprintf('The "%s" given in the context is not callable.'self::MAX_DEPTH_HANDLER));
  144.             }
  145.         } else {
  146.             $maxDepthHandler null;
  147.         }
  148.         foreach ($attributes as $attribute) {
  149.             $maxDepthReached false;
  150.             if (null !== $attributesMetadata && ($maxDepthReached $this->isMaxDepthReached($attributesMetadata$class$attribute$context)) && !$maxDepthHandler) {
  151.                 continue;
  152.             }
  153.             $attributeContext $this->getAttributeNormalizationContext($object$attribute$context);
  154.             try {
  155.                 $attributeValue $this->getAttributeValue($object$attribute$format$attributeContext);
  156.             } catch (UninitializedPropertyException $e) {
  157.                 if ($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) {
  158.                     continue;
  159.                 }
  160.                 throw $e;
  161.             } catch (\Error $e) {
  162.                 if (($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) && $this->isUninitializedValueError($e)) {
  163.                     continue;
  164.                 }
  165.                 throw $e;
  166.             }
  167.             if ($maxDepthReached) {
  168.                 $attributeValue $maxDepthHandler($attributeValue$object$attribute$format$attributeContext);
  169.             }
  170.             $attributeValue $this->applyCallbacks($attributeValue$object$attribute$format$attributeContext);
  171.             if (null !== $attributeValue && !\is_scalar($attributeValue)) {
  172.                 $stack[$attribute] = $attributeValue;
  173.             }
  174.             $data $this->updateData($data$attribute$attributeValue$class$format$attributeContext);
  175.         }
  176.         foreach ($stack as $attribute => $attributeValue) {
  177.             if (!$this->serializer instanceof NormalizerInterface) {
  178.                 throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer.'$attribute));
  179.             }
  180.             $attributeContext $this->getAttributeNormalizationContext($object$attribute$context);
  181.             $childContext $this->createChildContext($attributeContext$attribute$format);
  182.             $data $this->updateData($data$attribute$this->serializer->normalize($attributeValue$format$childContext), $class$format$attributeContext);
  183.         }
  184.         if (isset($context[self::PRESERVE_EMPTY_OBJECTS]) && !\count($data)) {
  185.             return new \ArrayObject();
  186.         }
  187.         return $data;
  188.     }
  189.     /**
  190.      * Computes the normalization context merged with current one. Metadata always wins over global context, as more specific.
  191.      */
  192.     private function getAttributeNormalizationContext(object $objectstring $attribute, array $context): array
  193.     {
  194.         if (null === $metadata $this->getAttributeMetadata($object$attribute)) {
  195.             return $context;
  196.         }
  197.         return array_merge($context$metadata->getNormalizationContextForGroups($this->getGroups($context)));
  198.     }
  199.     /**
  200.      * Computes the denormalization context merged with current one. Metadata always wins over global context, as more specific.
  201.      */
  202.     private function getAttributeDenormalizationContext(string $classstring $attribute, array $context): array
  203.     {
  204.         $context['deserialization_path'] = ($context['deserialization_path'] ?? false) ? $context['deserialization_path'].'.'.$attribute $attribute;
  205.         if (null === $metadata $this->getAttributeMetadata($class$attribute)) {
  206.             return $context;
  207.         }
  208.         return array_merge($context$metadata->getDenormalizationContextForGroups($this->getGroups($context)));
  209.     }
  210.     private function getAttributeMetadata($objectOrClassstring $attribute): ?AttributeMetadataInterface
  211.     {
  212.         if (!$this->classMetadataFactory) {
  213.             return null;
  214.         }
  215.         return $this->classMetadataFactory->getMetadataFor($objectOrClass)->getAttributesMetadata()[$attribute] ?? null;
  216.     }
  217.     /**
  218.      * {@inheritdoc}
  219.      */
  220.     protected function instantiateObject(array &$datastring $class, array &$context\ReflectionClass $reflectionClass$allowedAttributesstring $format null)
  221.     {
  222.         if (null !== $object $this->extractObjectToPopulate($class$contextself::OBJECT_TO_POPULATE)) {
  223.             unset($context[self::OBJECT_TO_POPULATE]);
  224.             return $object;
  225.         }
  226.         if ($this->classDiscriminatorResolver && $mapping $this->classDiscriminatorResolver->getMappingForClass($class)) {
  227.             if (!isset($data[$mapping->getTypeProperty()])) {
  228.                 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);
  229.             }
  230.             $type $data[$mapping->getTypeProperty()];
  231.             if (null === ($mappedClass $mapping->getClassForType($type))) {
  232.                 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);
  233.             }
  234.             if ($mappedClass !== $class) {
  235.                 return $this->instantiateObject($data$mappedClass$context, new \ReflectionClass($mappedClass), $allowedAttributes$format);
  236.             }
  237.         }
  238.         return parent::instantiateObject($data$class$context$reflectionClass$allowedAttributes$format);
  239.     }
  240.     /**
  241.      * Gets and caches attributes for the given object, format and context.
  242.      *
  243.      * @return string[]
  244.      */
  245.     protected function getAttributes(object $object, ?string $format, array $context)
  246.     {
  247.         $class $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  248.         $key $class.'-'.$context['cache_key'];
  249.         if (isset($this->attributesCache[$key])) {
  250.             return $this->attributesCache[$key];
  251.         }
  252.         $allowedAttributes $this->getAllowedAttributes($object$contexttrue);
  253.         if (false !== $allowedAttributes) {
  254.             if ($context['cache_key']) {
  255.                 $this->attributesCache[$key] = $allowedAttributes;
  256.             }
  257.             return $allowedAttributes;
  258.         }
  259.         $attributes $this->extractAttributes($object$format$context);
  260.         if ($this->classDiscriminatorResolver && $mapping $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
  261.             array_unshift($attributes$mapping->getTypeProperty());
  262.         }
  263.         if ($context['cache_key'] && \stdClass::class !== $class) {
  264.             $this->attributesCache[$key] = $attributes;
  265.         }
  266.         return $attributes;
  267.     }
  268.     /**
  269.      * Extracts attributes to normalize from the class of the given object, format and context.
  270.      *
  271.      * @return string[]
  272.      */
  273.     abstract protected function extractAttributes(object $objectstring $format null, array $context = []);
  274.     /**
  275.      * Gets the attribute value.
  276.      *
  277.      * @return mixed
  278.      */
  279.     abstract protected function getAttributeValue(object $objectstring $attributestring $format null, array $context = []);
  280.     /**
  281.      * {@inheritdoc}
  282.      */
  283.     public function supportsDenormalization($datastring $typestring $format null)
  284.     {
  285.         return class_exists($type) || (interface_exists($typefalse) && $this->classDiscriminatorResolver && null !== $this->classDiscriminatorResolver->getMappingForClass($type));
  286.     }
  287.     /**
  288.      * {@inheritdoc}
  289.      */
  290.     public function denormalize($datastring $typestring $format null, array $context = [])
  291.     {
  292.         if (!isset($context['cache_key'])) {
  293.             $context['cache_key'] = $this->getCacheKey($format$context);
  294.         }
  295.         $this->validateCallbackContext($context);
  296.         if (null === $data && isset($context['value_type']) && $context['value_type'] instanceof Type && $context['value_type']->isNullable()) {
  297.             return null;
  298.         }
  299.         $allowedAttributes $this->getAllowedAttributes($type$contexttrue);
  300.         $normalizedData $this->prepareForDenormalization($data);
  301.         $extraAttributes = [];
  302.         $reflectionClass = new \ReflectionClass($type);
  303.         $object $this->instantiateObject($normalizedData$type$context$reflectionClass$allowedAttributes$format);
  304.         $resolvedClass $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  305.         foreach ($normalizedData as $attribute => $value) {
  306.             if ($this->nameConverter) {
  307.                 $attribute $this->nameConverter->denormalize($attribute$resolvedClass$format$context);
  308.             }
  309.             $attributeContext $this->getAttributeDenormalizationContext($resolvedClass$attribute$context);
  310.             if ((false !== $allowedAttributes && !\in_array($attribute$allowedAttributes)) || !$this->isAllowedAttribute($resolvedClass$attribute$format$context)) {
  311.                 if (!($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES])) {
  312.                     $extraAttributes[] = $attribute;
  313.                 }
  314.                 continue;
  315.             }
  316.             if ($attributeContext[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) {
  317.                 try {
  318.                     $attributeContext[self::OBJECT_TO_POPULATE] = $this->getAttributeValue($object$attribute$format$attributeContext);
  319.                 } catch (NoSuchPropertyException $e) {
  320.                 }
  321.             }
  322.             $types $this->getTypes($resolvedClass$attribute);
  323.             if (null !== $types) {
  324.                 try {
  325.                     $value $this->validateAndDenormalize($types$resolvedClass$attribute$value$format$attributeContext);
  326.                 } catch (NotNormalizableValueException $exception) {
  327.                     if (isset($context['not_normalizable_value_exceptions'])) {
  328.                         $context['not_normalizable_value_exceptions'][] = $exception;
  329.                         continue;
  330.                     }
  331.                     throw $exception;
  332.                 }
  333.             }
  334.             $value $this->applyCallbacks($value$resolvedClass$attribute$format$attributeContext);
  335.             try {
  336.                 $this->setAttributeValue($object$attribute$value$format$attributeContext);
  337.             } catch (PropertyAccessInvalidArgumentException $e) {
  338.                 $exception NotNormalizableValueException::createForUnexpectedDataType(
  339.                     sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute$type),
  340.                     $data,
  341.                     ['unknown'],
  342.                     $context['deserialization_path'] ?? null,
  343.                     false,
  344.                     $e->getCode(),
  345.                     $e
  346.                 );
  347.                 if (isset($context['not_normalizable_value_exceptions'])) {
  348.                     $context['not_normalizable_value_exceptions'][] = $exception;
  349.                     continue;
  350.                 }
  351.                 throw $exception;
  352.             }
  353.         }
  354.         if ($extraAttributes) {
  355.             throw new ExtraAttributesException($extraAttributes);
  356.         }
  357.         return $object;
  358.     }
  359.     /**
  360.      * Sets attribute value.
  361.      */
  362.     abstract protected function setAttributeValue(object $objectstring $attribute$valuestring $format null, array $context = []);
  363.     /**
  364.      * Validates the submitted data and denormalizes it.
  365.      *
  366.      * @param Type[] $types
  367.      * @param mixed  $data
  368.      *
  369.      * @return mixed
  370.      *
  371.      * @throws NotNormalizableValueException
  372.      * @throws ExtraAttributesException
  373.      * @throws MissingConstructorArgumentsException
  374.      * @throws LogicException
  375.      */
  376.     private function validateAndDenormalize(array $typesstring $currentClassstring $attribute$data, ?string $format, array $context)
  377.     {
  378.         $expectedTypes = [];
  379.         $isUnionType \count($types) > 1;
  380.         $extraAttributesException null;
  381.         $missingConstructorArgumentException null;
  382.         foreach ($types as $type) {
  383.             if (null === $data && $type->isNullable()) {
  384.                 return null;
  385.             }
  386.             $collectionValueType $type->isCollection() ? $type->getCollectionValueTypes()[0] ?? null null;
  387.             // Fix a collection that contains the only one element
  388.             // This is special to xml format only
  389.             if ('xml' === $format && null !== $collectionValueType && (!\is_array($data) || !\is_int(key($data)))) {
  390.                 $data = [$data];
  391.             }
  392.             // This try-catch should cover all NotNormalizableValueException (and all return branches after the first
  393.             // exception) so we could try denormalizing all types of an union type. If the target type is not an union
  394.             // type, we will just re-throw the catched exception.
  395.             // In the case of no denormalization succeeds with an union type, it will fall back to the default exception
  396.             // with the acceptable types list.
  397.             try {
  398.                 // In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine,
  399.                 // if a value is meant to be a string, float, int or a boolean value from the serialized representation.
  400.                 // That's why we have to transform the values, if one of these non-string basic datatypes is expected.
  401.                 if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) {
  402.                     if ('' === $data) {
  403.                         if (Type::BUILTIN_TYPE_ARRAY === $builtinType $type->getBuiltinType()) {
  404.                             return [];
  405.                         }
  406.                         if ($type->isNullable() && \in_array($builtinType, [Type::BUILTIN_TYPE_BOOLType::BUILTIN_TYPE_INTType::BUILTIN_TYPE_FLOAT], true)) {
  407.                             return null;
  408.                         }
  409.                     }
  410.                     switch ($builtinType ?? $type->getBuiltinType()) {
  411.                         case Type::BUILTIN_TYPE_BOOL:
  412.                             // according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
  413.                             if ('false' === $data || '0' === $data) {
  414.                                 $data false;
  415.                             } elseif ('true' === $data || '1' === $data) {
  416.                                 $data true;
  417.                             } else {
  418.                                 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);
  419.                             }
  420.                             break;
  421.                         case Type::BUILTIN_TYPE_INT:
  422.                             if (ctype_digit($data) || '-' === $data[0] && ctype_digit(substr($data1))) {
  423.                                 $data = (int) $data;
  424.                             } else {
  425.                                 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);
  426.                             }
  427.                             break;
  428.                         case Type::BUILTIN_TYPE_FLOAT:
  429.                             if (is_numeric($data)) {
  430.                                 return (float) $data;
  431.                             }
  432.                             switch ($data) {
  433.                                 case 'NaN':
  434.                                     return \NAN;
  435.                                 case 'INF':
  436.                                     return \INF;
  437.                                 case '-INF':
  438.                                     return -\INF;
  439.                                 default:
  440.                                     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);
  441.                             }
  442.                     }
  443.                 }
  444.                 if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
  445.                     $builtinType Type::BUILTIN_TYPE_OBJECT;
  446.                     $class $collectionValueType->getClassName().'[]';
  447.                     if (\count($collectionKeyType $type->getCollectionKeyTypes()) > 0) {
  448.                         $context['key_type'] = \count($collectionKeyType) > $collectionKeyType $collectionKeyType[0];
  449.                     }
  450.                     $context['value_type'] = $collectionValueType;
  451.                 } elseif ($type->isCollection() && \count($collectionValueType $type->getCollectionValueTypes()) > && Type::BUILTIN_TYPE_ARRAY === $collectionValueType[0]->getBuiltinType()) {
  452.                     // get inner type for any nested array
  453.                     [$innerType] = $collectionValueType;
  454.                     // note that it will break for any other builtinType
  455.                     $dimensions '[]';
  456.                     while (\count($innerType->getCollectionValueTypes()) > && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) {
  457.                         $dimensions .= '[]';
  458.                         [$innerType] = $innerType->getCollectionValueTypes();
  459.                     }
  460.                     if (null !== $innerType->getClassName()) {
  461.                         // the builtinType is the inner one and the class is the class followed by []...[]
  462.                         $builtinType $innerType->getBuiltinType();
  463.                         $class $innerType->getClassName().$dimensions;
  464.                     } else {
  465.                         // default fallback (keep it as array)
  466.                         $builtinType $type->getBuiltinType();
  467.                         $class $type->getClassName();
  468.                     }
  469.                 } else {
  470.                     $builtinType $type->getBuiltinType();
  471.                     $class $type->getClassName();
  472.                 }
  473.                 $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class $class $builtinType] = true;
  474.                 if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
  475.                     if (!$this->serializer instanceof DenormalizerInterface) {
  476.                         throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.'$attribute$class));
  477.                     }
  478.                     $childContext $this->createChildContext($context$attribute$format);
  479.                     if ($this->serializer->supportsDenormalization($data$class$format$childContext)) {
  480.                         return $this->serializer->denormalize($data$class$format$childContext);
  481.                     }
  482.                 }
  483.                 // JSON only has a Number type corresponding to both int and float PHP types.
  484.                 // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
  485.                 // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
  486.                 // PHP's json_decode automatically converts Numbers without a decimal part to integers.
  487.                 // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
  488.                 // a float is expected.
  489.                 if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && null !== $format && str_contains($formatJsonEncoder::FORMAT)) {
  490.                     return (float) $data;
  491.                 }
  492.                 if (Type::BUILTIN_TYPE_FALSE === $builtinType && false === $data) {
  493.                     return $data;
  494.                 }
  495.                 if (('is_'.$builtinType)($data)) {
  496.                     return $data;
  497.                 }
  498.             } catch (NotNormalizableValueException $e) {
  499.                 if (!$isUnionType) {
  500.                     throw $e;
  501.                 }
  502.             } catch (ExtraAttributesException $e) {
  503.                 if (!$isUnionType) {
  504.                     throw $e;
  505.                 }
  506.                 if (!$extraAttributesException) {
  507.                     $extraAttributesException $e;
  508.                 }
  509.             } catch (MissingConstructorArgumentsException $e) {
  510.                 if (!$isUnionType) {
  511.                     throw $e;
  512.                 }
  513.                 if (!$missingConstructorArgumentException) {
  514.                     $missingConstructorArgumentException $e;
  515.                 }
  516.             }
  517.         }
  518.         if ($extraAttributesException) {
  519.             throw $extraAttributesException;
  520.         }
  521.         if ($missingConstructorArgumentException) {
  522.             throw $missingConstructorArgumentException;
  523.         }
  524.         if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) {
  525.             return $data;
  526.         }
  527.         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);
  528.     }
  529.     /**
  530.      * @internal
  531.      */
  532.     protected function denormalizeParameter(\ReflectionClass $class\ReflectionParameter $parameterstring $parameterName$parameterData, array $contextstring $format null)
  533.     {
  534.         if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $types $this->getTypes($class->getName(), $parameterName)) {
  535.             return parent::denormalizeParameter($class$parameter$parameterName$parameterData$context$format);
  536.         }
  537.         $parameterData $this->validateAndDenormalize($types$class->getName(), $parameterName$parameterData$format$context);
  538.         return $this->applyCallbacks($parameterData$class->getName(), $parameterName$format$context);
  539.     }
  540.     /**
  541.      * @return Type[]|null
  542.      */
  543.     private function getTypes(string $currentClassstring $attribute): ?array
  544.     {
  545.         if (null === $this->propertyTypeExtractor) {
  546.             return null;
  547.         }
  548.         $key $currentClass.'::'.$attribute;
  549.         if (isset($this->typesCache[$key])) {
  550.             return false === $this->typesCache[$key] ? null $this->typesCache[$key];
  551.         }
  552.         if (null !== $types $this->propertyTypeExtractor->getTypes($currentClass$attribute)) {
  553.             return $this->typesCache[$key] = $types;
  554.         }
  555.         if (null !== $this->classDiscriminatorResolver && null !== $discriminatorMapping $this->classDiscriminatorResolver->getMappingForClass($currentClass)) {
  556.             if ($discriminatorMapping->getTypeProperty() === $attribute) {
  557.                 return $this->typesCache[$key] = [
  558.                     new Type(Type::BUILTIN_TYPE_STRING),
  559.                 ];
  560.             }
  561.             foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
  562.                 if (null !== $types $this->propertyTypeExtractor->getTypes($mappedClass$attribute)) {
  563.                     return $this->typesCache[$key] = $types;
  564.                 }
  565.             }
  566.         }
  567.         $this->typesCache[$key] = false;
  568.         return null;
  569.     }
  570.     /**
  571.      * Sets an attribute and apply the name converter if necessary.
  572.      *
  573.      * @param mixed $attributeValue
  574.      */
  575.     private function updateData(array $datastring $attribute$attributeValuestring $class, ?string $format, array $context): array
  576.     {
  577.         if (null === $attributeValue && ($context[self::SKIP_NULL_VALUES] ?? $this->defaultContext[self::SKIP_NULL_VALUES] ?? false)) {
  578.             return $data;
  579.         }
  580.         if ($this->nameConverter) {
  581.             $attribute $this->nameConverter->normalize($attribute$class$format$context);
  582.         }
  583.         $data[$attribute] = $attributeValue;
  584.         return $data;
  585.     }
  586.     /**
  587.      * Is the max depth reached for the given attribute?
  588.      *
  589.      * @param AttributeMetadataInterface[] $attributesMetadata
  590.      */
  591.     private function isMaxDepthReached(array $attributesMetadatastring $classstring $attribute, array &$context): bool
  592.     {
  593.         $enableMaxDepth $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false;
  594.         if (
  595.             !$enableMaxDepth ||
  596.             !isset($attributesMetadata[$attribute]) ||
  597.             null === $maxDepth $attributesMetadata[$attribute]->getMaxDepth()
  598.         ) {
  599.             return false;
  600.         }
  601.         $key sprintf(self::DEPTH_KEY_PATTERN$class$attribute);
  602.         if (!isset($context[$key])) {
  603.             $context[$key] = 1;
  604.             return false;
  605.         }
  606.         if ($context[$key] === $maxDepth) {
  607.             return true;
  608.         }
  609.         ++$context[$key];
  610.         return false;
  611.     }
  612.     /**
  613.      * Overwritten to update the cache key for the child.
  614.      *
  615.      * We must not mix up the attribute cache between parent and children.
  616.      *
  617.      * {@inheritdoc}
  618.      *
  619.      * @internal
  620.      */
  621.     protected function createChildContext(array $parentContextstring $attribute, ?string $format): array
  622.     {
  623.         $context parent::createChildContext($parentContext$attribute$format);
  624.         $context['cache_key'] = $this->getCacheKey($format$context);
  625.         return $context;
  626.     }
  627.     /**
  628.      * Builds the cache key for the attributes cache.
  629.      *
  630.      * The key must be different for every option in the context that could change which attributes should be handled.
  631.      *
  632.      * @return bool|string
  633.      */
  634.     private function getCacheKey(?string $format, array $context)
  635.     {
  636.         foreach ($context[self::EXCLUDE_FROM_CACHE_KEY] ?? $this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] as $key) {
  637.             unset($context[$key]);
  638.         }
  639.         unset($context[self::EXCLUDE_FROM_CACHE_KEY]);
  640.         unset($context[self::OBJECT_TO_POPULATE]);
  641.         unset($context['cache_key']); // avoid artificially different keys
  642.         try {
  643.             return md5($format.serialize([
  644.                 'context' => $context,
  645.                 'ignored' => $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES],
  646.             ]));
  647.         } catch (\Exception $e) {
  648.             // The context cannot be serialized, skip the cache
  649.             return false;
  650.         }
  651.     }
  652.     /**
  653.      * This error may occur when specific object normalizer implementation gets attribute value
  654.      * by accessing a public uninitialized property or by calling a method accessing such property.
  655.      */
  656.     private function isUninitializedValueError(\Error $e): bool
  657.     {
  658.         return \PHP_VERSION_ID >= 70400
  659.             && str_starts_with($e->getMessage(), 'Typed property')
  660.             && str_ends_with($e->getMessage(), 'must not be accessed before initialization');
  661.     }
  662. }