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

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