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

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