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

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\PropertyInfo\PropertyTypeExtractorInterface;
  14. use Symfony\Component\PropertyInfo\Type;
  15. use Symfony\Component\Serializer\Encoder\JsonEncoder;
  16. use Symfony\Component\Serializer\Exception\ExtraAttributesException;
  17. use Symfony\Component\Serializer\Exception\LogicException;
  18. use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
  19. use Symfony\Component\Serializer\Exception\RuntimeException;
  20. use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
  21. use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
  22. use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
  23. use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
  24. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  25. /**
  26.  * Base class for a normalizer dealing with objects.
  27.  *
  28.  * @author Kévin Dunglas <dunglas@gmail.com>
  29.  */
  30. abstract class AbstractObjectNormalizer extends AbstractNormalizer
  31. {
  32.     /**
  33.      * Set to true to respect the max depth metadata on fields.
  34.      */
  35.     public const ENABLE_MAX_DEPTH 'enable_max_depth';
  36.     /**
  37.      * How to track the current depth in the context.
  38.      */
  39.     public const DEPTH_KEY_PATTERN 'depth_%s::%s';
  40.     /**
  41.      * While denormalizing, we can verify that types match.
  42.      *
  43.      * You can disable this by setting this flag to true.
  44.      */
  45.     public const DISABLE_TYPE_ENFORCEMENT 'disable_type_enforcement';
  46.     /**
  47.      * Flag to control whether fields with the value `null` should be output
  48.      * when normalizing or omitted.
  49.      */
  50.     public const SKIP_NULL_VALUES 'skip_null_values';
  51.     /**
  52.      * Callback to allow to set a value for an attribute when the max depth has
  53.      * been reached.
  54.      *
  55.      * If no callback is given, the attribute is skipped. If a callable is
  56.      * given, its return value is used (even if null).
  57.      *
  58.      * The arguments are:
  59.      *
  60.      * - mixed  $attributeValue value of this field
  61.      * - object $object         the whole object being normalized
  62.      * - string $attributeName  name of the attribute being normalized
  63.      * - string $format         the requested format
  64.      * - array  $context        the serialization context
  65.      */
  66.     public const MAX_DEPTH_HANDLER 'max_depth_handler';
  67.     /**
  68.      * Specify which context key are not relevant to determine which attributes
  69.      * of an object to (de)normalize.
  70.      */
  71.     public const EXCLUDE_FROM_CACHE_KEY 'exclude_from_cache_key';
  72.     /**
  73.      * Flag to tell the denormalizer to also populate existing objects on
  74.      * attributes of the main object.
  75.      *
  76.      * Setting this to true is only useful if you also specify the root object
  77.      * in OBJECT_TO_POPULATE.
  78.      */
  79.     public const DEEP_OBJECT_TO_POPULATE 'deep_object_to_populate';
  80.     public const PRESERVE_EMPTY_OBJECTS 'preserve_empty_objects';
  81.     private $propertyTypeExtractor;
  82.     private $typesCache = [];
  83.     private $attributesCache = [];
  84.     /**
  85.      * @deprecated since Symfony 4.2
  86.      *
  87.      * @var callable|null
  88.      */
  89.     private $maxDepthHandler;
  90.     private $objectClassResolver;
  91.     /**
  92.      * @var ClassDiscriminatorResolverInterface|null
  93.      */
  94.     protected $classDiscriminatorResolver;
  95.     public function __construct(ClassMetadataFactoryInterface $classMetadataFactory nullNameConverterInterface $nameConverter nullPropertyTypeExtractorInterface $propertyTypeExtractor nullClassDiscriminatorResolverInterface $classDiscriminatorResolver null, callable $objectClassResolver null, array $defaultContext = [])
  96.     {
  97.         parent::__construct($classMetadataFactory$nameConverter$defaultContext);
  98.         if (isset($this->defaultContext[self::MAX_DEPTH_HANDLER]) && !\is_callable($this->defaultContext[self::MAX_DEPTH_HANDLER])) {
  99.             throw new InvalidArgumentException(sprintf('The "%s" given in the default context is not callable.'self::MAX_DEPTH_HANDLER));
  100.         }
  101.         $this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] = [self::CIRCULAR_REFERENCE_LIMIT_COUNTERS];
  102.         $this->propertyTypeExtractor $propertyTypeExtractor;
  103.         if (null === $classDiscriminatorResolver && null !== $classMetadataFactory) {
  104.             $classDiscriminatorResolver = new ClassDiscriminatorFromClassMetadata($classMetadataFactory);
  105.         }
  106.         $this->classDiscriminatorResolver $classDiscriminatorResolver;
  107.         $this->objectClassResolver $objectClassResolver;
  108.     }
  109.     /**
  110.      * {@inheritdoc}
  111.      */
  112.     public function supportsNormalization($data$format null)
  113.     {
  114.         return \is_object($data) && !$data instanceof \Traversable;
  115.     }
  116.     /**
  117.      * {@inheritdoc}
  118.      */
  119.     public function normalize($object$format null, array $context = [])
  120.     {
  121.         if (!isset($context['cache_key'])) {
  122.             $context['cache_key'] = $this->getCacheKey($format$context);
  123.         }
  124.         if (isset($context[self::CALLBACKS])) {
  125.             if (!\is_array($context[self::CALLBACKS])) {
  126.                 throw new InvalidArgumentException(sprintf('The "%s" context option must be an array of callables.'self::CALLBACKS));
  127.             }
  128.             foreach ($context[self::CALLBACKS] as $attribute => $callback) {
  129.                 if (!\is_callable($callback)) {
  130.                     throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s" context option.'$attributeself::CALLBACKS));
  131.                 }
  132.             }
  133.         }
  134.         if ($this->isCircularReference($object$context)) {
  135.             return $this->handleCircularReference($object$format$context);
  136.         }
  137.         $data = [];
  138.         $stack = [];
  139.         $attributes $this->getAttributes($object$format$context);
  140.         $class $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  141.         $attributesMetadata $this->classMetadataFactory $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
  142.         if (isset($context[self::MAX_DEPTH_HANDLER])) {
  143.             $maxDepthHandler $context[self::MAX_DEPTH_HANDLER];
  144.             if (!\is_callable($maxDepthHandler)) {
  145.                 throw new InvalidArgumentException(sprintf('The "%s" given in the context is not callable.'self::MAX_DEPTH_HANDLER));
  146.             }
  147.         } else {
  148.             // already validated in constructor resp by type declaration of setMaxDepthHandler
  149.             $maxDepthHandler $this->defaultContext[self::MAX_DEPTH_HANDLER] ?? $this->maxDepthHandler;
  150.         }
  151.         foreach ($attributes as $attribute) {
  152.             $maxDepthReached false;
  153.             if (null !== $attributesMetadata && ($maxDepthReached $this->isMaxDepthReached($attributesMetadata$class$attribute$context)) && !$maxDepthHandler) {
  154.                 continue;
  155.             }
  156.             $attributeValue $this->getAttributeValue($object$attribute$format$context);
  157.             if ($maxDepthReached) {
  158.                 $attributeValue $maxDepthHandler($attributeValue$object$attribute$format$context);
  159.             }
  160.             /**
  161.              * @var callable|null
  162.              */
  163.             $callback $context[self::CALLBACKS][$attribute] ?? $this->defaultContext[self::CALLBACKS][$attribute] ?? $this->callbacks[$attribute] ?? null;
  164.             if ($callback) {
  165.                 $attributeValue $callback($attributeValue$object$attribute$format$context);
  166.             }
  167.             if (null !== $attributeValue && !is_scalar($attributeValue)) {
  168.                 $stack[$attribute] = $attributeValue;
  169.             }
  170.             $data $this->updateData($data$attribute$attributeValue$class$format$context);
  171.         }
  172.         foreach ($stack as $attribute => $attributeValue) {
  173.             if (!$this->serializer instanceof NormalizerInterface) {
  174.                 throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer.'$attribute));
  175.             }
  176.             $data $this->updateData($data$attribute$this->serializer->normalize($attributeValue$format$this->createChildContext($context$attribute$format)), $class$format$context);
  177.         }
  178.         if (isset($context[self::PRESERVE_EMPTY_OBJECTS]) && !\count($data)) {
  179.             return new \ArrayObject();
  180.         }
  181.         return $data;
  182.     }
  183.     /**
  184.      * {@inheritdoc}
  185.      */
  186.     protected function instantiateObject(array &$data$class, array &$context, \ReflectionClass $reflectionClass$allowedAttributesstring $format null)
  187.     {
  188.         if ($this->classDiscriminatorResolver && $mapping $this->classDiscriminatorResolver->getMappingForClass($class)) {
  189.             if (!isset($data[$mapping->getTypeProperty()])) {
  190.                 throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s".'$mapping->getTypeProperty(), $class));
  191.             }
  192.             $type $data[$mapping->getTypeProperty()];
  193.             if (null === ($mappedClass $mapping->getClassForType($type))) {
  194.                 throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s".'$type$class));
  195.             }
  196.             if ($mappedClass !== $class) {
  197.                 return $this->instantiateObject($data$mappedClass$context, new \ReflectionClass($mappedClass), $allowedAttributes$format);
  198.             }
  199.         }
  200.         return parent::instantiateObject($data$class$context$reflectionClass$allowedAttributes$format);
  201.     }
  202.     /**
  203.      * Gets and caches attributes for the given object, format and context.
  204.      *
  205.      * @param object      $object
  206.      * @param string|null $format
  207.      *
  208.      * @return string[]
  209.      */
  210.     protected function getAttributes($object$format, array $context)
  211.     {
  212.         $class $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  213.         $key $class.'-'.$context['cache_key'];
  214.         if (isset($this->attributesCache[$key])) {
  215.             return $this->attributesCache[$key];
  216.         }
  217.         $allowedAttributes $this->getAllowedAttributes($object$contexttrue);
  218.         if (false !== $allowedAttributes) {
  219.             if ($context['cache_key']) {
  220.                 $this->attributesCache[$key] = $allowedAttributes;
  221.             }
  222.             return $allowedAttributes;
  223.         }
  224.         $attributes $this->extractAttributes($object$format$context);
  225.         if ($this->classDiscriminatorResolver && $mapping $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
  226.             array_unshift($attributes$mapping->getTypeProperty());
  227.         }
  228.         if ($context['cache_key']) {
  229.             $this->attributesCache[$key] = $attributes;
  230.         }
  231.         return $attributes;
  232.     }
  233.     /**
  234.      * Extracts attributes to normalize from the class of the given object, format and context.
  235.      *
  236.      * @param object      $object
  237.      * @param string|null $format
  238.      *
  239.      * @return string[]
  240.      */
  241.     abstract protected function extractAttributes($object$format null, array $context = []);
  242.     /**
  243.      * Gets the attribute value.
  244.      *
  245.      * @param object      $object
  246.      * @param string      $attribute
  247.      * @param string|null $format
  248.      *
  249.      * @return mixed
  250.      */
  251.     abstract protected function getAttributeValue($object$attribute$format null, array $context = []);
  252.     /**
  253.      * Sets a handler function that will be called when the max depth is reached.
  254.      *
  255.      * @deprecated since Symfony 4.2
  256.      */
  257.     public function setMaxDepthHandler(?callable $handler): void
  258.     {
  259.         @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "max_depth_handler" key of the context instead.'__METHOD__), E_USER_DEPRECATED);
  260.         $this->maxDepthHandler $handler;
  261.     }
  262.     /**
  263.      * {@inheritdoc}
  264.      */
  265.     public function supportsDenormalization($data$type$format null)
  266.     {
  267.         return class_exists($type) || (interface_exists($typefalse) && $this->classDiscriminatorResolver && null !== $this->classDiscriminatorResolver->getMappingForClass($type));
  268.     }
  269.     /**
  270.      * {@inheritdoc}
  271.      */
  272.     public function denormalize($data$type$format null, array $context = [])
  273.     {
  274.         if (!isset($context['cache_key'])) {
  275.             $context['cache_key'] = $this->getCacheKey($format$context);
  276.         }
  277.         $allowedAttributes $this->getAllowedAttributes($type$contexttrue);
  278.         $normalizedData $this->prepareForDenormalization($data);
  279.         $extraAttributes = [];
  280.         $reflectionClass = new \ReflectionClass($type);
  281.         $object $this->instantiateObject($normalizedData$type$context$reflectionClass$allowedAttributes$format);
  282.         $resolvedClass $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  283.         foreach ($normalizedData as $attribute => $value) {
  284.             if ($this->nameConverter) {
  285.                 $attribute $this->nameConverter->denormalize($attribute$resolvedClass$format$context);
  286.             }
  287.             if ((false !== $allowedAttributes && !\in_array($attribute$allowedAttributes)) || !$this->isAllowedAttribute($resolvedClass$attribute$format$context)) {
  288.                 if (!($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES])) {
  289.                     $extraAttributes[] = $attribute;
  290.                 }
  291.                 continue;
  292.             }
  293.             if ($context[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) {
  294.                 try {
  295.                     $context[self::OBJECT_TO_POPULATE] = $this->getAttributeValue($object$attribute$format$context);
  296.                 } catch (NoSuchPropertyException $e) {
  297.                 }
  298.             }
  299.             $value $this->validateAndDenormalize($resolvedClass$attribute$value$format$context);
  300.             try {
  301.                 $this->setAttributeValue($object$attribute$value$format$context);
  302.             } catch (InvalidArgumentException $e) {
  303.                 throw new NotNormalizableValueException(sprintf('Failed to denormalize attribute "%s" value for class "%s": '$attribute$type).$e->getMessage(), $e->getCode(), $e);
  304.             }
  305.         }
  306.         if (!empty($extraAttributes)) {
  307.             throw new ExtraAttributesException($extraAttributes);
  308.         }
  309.         return $object;
  310.     }
  311.     /**
  312.      * Sets attribute value.
  313.      *
  314.      * @param object      $object
  315.      * @param string      $attribute
  316.      * @param mixed       $value
  317.      * @param string|null $format
  318.      */
  319.     abstract protected function setAttributeValue($object$attribute$value$format null, array $context = []);
  320.     /**
  321.      * Validates the submitted data and denormalizes it.
  322.      *
  323.      * @param mixed $data
  324.      *
  325.      * @return mixed
  326.      *
  327.      * @throws NotNormalizableValueException
  328.      * @throws LogicException
  329.      */
  330.     private function validateAndDenormalize(string $currentClassstring $attribute$data, ?string $format, array $context)
  331.     {
  332.         if (null === $types $this->getTypes($currentClass$attribute)) {
  333.             return $data;
  334.         }
  335.         $expectedTypes = [];
  336.         foreach ($types as $type) {
  337.             if (null === $data && $type->isNullable()) {
  338.                 return null;
  339.             }
  340.             $collectionValueType $type->isCollection() ? $type->getCollectionValueType() : null;
  341.             // Fix a collection that contains the only one element
  342.             // This is special to xml format only
  343.             if ('xml' === $format && null !== $collectionValueType && (!\is_array($data) || !\is_int(key($data)))) {
  344.                 $data = [$data];
  345.             }
  346.             if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
  347.                 $builtinType Type::BUILTIN_TYPE_OBJECT;
  348.                 $class $collectionValueType->getClassName().'[]';
  349.                 if (null !== $collectionKeyType $type->getCollectionKeyType()) {
  350.                     $context['key_type'] = $collectionKeyType;
  351.                 }
  352.             } elseif ($type->isCollection() && null !== ($collectionValueType $type->getCollectionValueType()) && Type::BUILTIN_TYPE_ARRAY === $collectionValueType->getBuiltinType()) {
  353.                 // get inner type for any nested array
  354.                 $innerType $collectionValueType;
  355.                 // note that it will break for any other builtinType
  356.                 $dimensions '[]';
  357.                 while (null !== $innerType->getCollectionValueType() && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) {
  358.                     $dimensions .= '[]';
  359.                     $innerType $innerType->getCollectionValueType();
  360.                 }
  361.                 if (null !== $innerType->getClassName()) {
  362.                     // the builtinType is the inner one and the class is the class followed by []...[]
  363.                     $builtinType $innerType->getBuiltinType();
  364.                     $class $innerType->getClassName().$dimensions;
  365.                 } else {
  366.                     // default fallback (keep it as array)
  367.                     $builtinType $type->getBuiltinType();
  368.                     $class $type->getClassName();
  369.                 }
  370.             } else {
  371.                 $builtinType $type->getBuiltinType();
  372.                 $class $type->getClassName();
  373.             }
  374.             $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class $class $builtinType] = true;
  375.             if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
  376.                 if (!$this->serializer instanceof DenormalizerInterface) {
  377.                     throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.'$attribute$class));
  378.                 }
  379.                 $childContext $this->createChildContext($context$attribute$format);
  380.                 if ($this->serializer->supportsDenormalization($data$class$format$childContext)) {
  381.                     return $this->serializer->denormalize($data$class$format$childContext);
  382.                 }
  383.             }
  384.             // JSON only has a Number type corresponding to both int and float PHP types.
  385.             // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
  386.             // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
  387.             // PHP's json_decode automatically converts Numbers without a decimal part to integers.
  388.             // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
  389.             // a float is expected.
  390.             if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && false !== strpos($formatJsonEncoder::FORMAT)) {
  391.                 return (float) $data;
  392.             }
  393.             if (('is_'.$builtinType)($data)) {
  394.                 return $data;
  395.             }
  396.         }
  397.         if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) {
  398.             return $data;
  399.         }
  400.         throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).'$attribute$currentClassimplode('", "'array_keys($expectedTypes)), \gettype($data)));
  401.     }
  402.     /**
  403.      * @internal
  404.      */
  405.     protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter$parameterName$parameterData, array $context$format null)
  406.     {
  407.         if (null === $this->propertyTypeExtractor || null === $this->propertyTypeExtractor->getTypes($class->getName(), $parameterName)) {
  408.             return parent::denormalizeParameter($class$parameter$parameterName$parameterData$context$format);
  409.         }
  410.         return $this->validateAndDenormalize($class->getName(), $parameterName$parameterData$format$context);
  411.     }
  412.     /**
  413.      * @return Type[]|null
  414.      */
  415.     private function getTypes(string $currentClassstring $attribute): ?array
  416.     {
  417.         if (null === $this->propertyTypeExtractor) {
  418.             return null;
  419.         }
  420.         $key $currentClass.'::'.$attribute;
  421.         if (isset($this->typesCache[$key])) {
  422.             return false === $this->typesCache[$key] ? null $this->typesCache[$key];
  423.         }
  424.         if (null !== $types $this->propertyTypeExtractor->getTypes($currentClass$attribute)) {
  425.             return $this->typesCache[$key] = $types;
  426.         }
  427.         if (null !== $this->classDiscriminatorResolver && null !== $discriminatorMapping $this->classDiscriminatorResolver->getMappingForClass($currentClass)) {
  428.             if ($discriminatorMapping->getTypeProperty() === $attribute) {
  429.                 return $this->typesCache[$key] = [
  430.                     new Type(Type::BUILTIN_TYPE_STRING),
  431.                 ];
  432.             }
  433.             foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
  434.                 if (null !== $types $this->propertyTypeExtractor->getTypes($mappedClass$attribute)) {
  435.                     return $this->typesCache[$key] = $types;
  436.                 }
  437.             }
  438.         }
  439.         $this->typesCache[$key] = false;
  440.         return null;
  441.     }
  442.     /**
  443.      * Sets an attribute and apply the name converter if necessary.
  444.      *
  445.      * @param mixed $attributeValue
  446.      */
  447.     private function updateData(array $datastring $attribute$attributeValuestring $class, ?string $format, array $context): array
  448.     {
  449.         if (null === $attributeValue && ($context[self::SKIP_NULL_VALUES] ?? $this->defaultContext[self::SKIP_NULL_VALUES] ?? false)) {
  450.             return $data;
  451.         }
  452.         if ($this->nameConverter) {
  453.             $attribute $this->nameConverter->normalize($attribute$class$format$context);
  454.         }
  455.         $data[$attribute] = $attributeValue;
  456.         return $data;
  457.     }
  458.     /**
  459.      * Is the max depth reached for the given attribute?
  460.      *
  461.      * @param AttributeMetadataInterface[] $attributesMetadata
  462.      */
  463.     private function isMaxDepthReached(array $attributesMetadatastring $classstring $attribute, array &$context): bool
  464.     {
  465.         $enableMaxDepth $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false;
  466.         if (
  467.             !$enableMaxDepth ||
  468.             !isset($attributesMetadata[$attribute]) ||
  469.             null === $maxDepth $attributesMetadata[$attribute]->getMaxDepth()
  470.         ) {
  471.             return false;
  472.         }
  473.         $key sprintf(self::DEPTH_KEY_PATTERN$class$attribute);
  474.         if (!isset($context[$key])) {
  475.             $context[$key] = 1;
  476.             return false;
  477.         }
  478.         if ($context[$key] === $maxDepth) {
  479.             return true;
  480.         }
  481.         ++$context[$key];
  482.         return false;
  483.     }
  484.     /**
  485.      * Overwritten to update the cache key for the child.
  486.      *
  487.      * We must not mix up the attribute cache between parent and children.
  488.      *
  489.      * {@inheritdoc}
  490.      *
  491.      * @param string|null $format
  492.      *
  493.      * @internal
  494.      */
  495.     protected function createChildContext(array $parentContext$attribute/*, ?string $format */): array
  496.     {
  497.         if (\func_num_args() >= 3) {
  498.             $format func_get_arg(2);
  499.         } else {
  500.             @trigger_error(sprintf('Method "%s::%s()" will have a third "?string $format" argument in version 5.0; not defining it is deprecated since Symfony 4.3.', static::class, __FUNCTION__), E_USER_DEPRECATED);
  501.             $format null;
  502.         }
  503.         $context parent::createChildContext($parentContext$attribute$format);
  504.         $context['cache_key'] = $this->getCacheKey($format$context);
  505.         return $context;
  506.     }
  507.     /**
  508.      * Builds the cache key for the attributes cache.
  509.      *
  510.      * The key must be different for every option in the context that could change which attributes should be handled.
  511.      *
  512.      * @return bool|string
  513.      */
  514.     private function getCacheKey(?string $format, array $context)
  515.     {
  516.         foreach ($context[self::EXCLUDE_FROM_CACHE_KEY] ?? $this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] as $key) {
  517.             unset($context[$key]);
  518.         }
  519.         unset($context[self::EXCLUDE_FROM_CACHE_KEY]);
  520.         unset($context[self::OBJECT_TO_POPULATE]);
  521.         unset($context['cache_key']); // avoid artificially different keys
  522.         try {
  523.             return md5($format.serialize([
  524.                 'context' => $context,
  525.                 'ignored' => $this->ignoredAttributes,
  526.                 'camelized' => $this->camelizedAttributes,
  527.             ]));
  528.         } catch (\Exception $exception) {
  529.             // The context cannot be serialized, skip the cache
  530.             return false;
  531.         }
  532.     }
  533. }