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

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.             $class $mappedClass;
  197.             $reflectionClass = new \ReflectionClass($class);
  198.         }
  199.         return parent::instantiateObject($data$class$context$reflectionClass$allowedAttributes$format);
  200.     }
  201.     /**
  202.      * Gets and caches attributes for the given object, format and context.
  203.      *
  204.      * @param object      $object
  205.      * @param string|null $format
  206.      *
  207.      * @return string[]
  208.      */
  209.     protected function getAttributes($object$format null, array $context)
  210.     {
  211.         $class $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  212.         $key $class.'-'.$context['cache_key'];
  213.         if (isset($this->attributesCache[$key])) {
  214.             return $this->attributesCache[$key];
  215.         }
  216.         $allowedAttributes $this->getAllowedAttributes($object$contexttrue);
  217.         if (false !== $allowedAttributes) {
  218.             if ($context['cache_key']) {
  219.                 $this->attributesCache[$key] = $allowedAttributes;
  220.             }
  221.             return $allowedAttributes;
  222.         }
  223.         $attributes $this->extractAttributes($object$format$context);
  224.         if ($this->classDiscriminatorResolver && $mapping $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
  225.             array_unshift($attributes$mapping->getTypeProperty());
  226.         }
  227.         if ($context['cache_key']) {
  228.             $this->attributesCache[$key] = $attributes;
  229.         }
  230.         return $attributes;
  231.     }
  232.     /**
  233.      * Extracts attributes to normalize from the class of the given object, format and context.
  234.      *
  235.      * @param object      $object
  236.      * @param string|null $format
  237.      *
  238.      * @return string[]
  239.      */
  240.     abstract protected function extractAttributes($object$format null, array $context = []);
  241.     /**
  242.      * Gets the attribute value.
  243.      *
  244.      * @param object      $object
  245.      * @param string      $attribute
  246.      * @param string|null $format
  247.      *
  248.      * @return mixed
  249.      */
  250.     abstract protected function getAttributeValue($object$attribute$format null, array $context = []);
  251.     /**
  252.      * Sets a handler function that will be called when the max depth is reached.
  253.      *
  254.      * @deprecated since Symfony 4.2
  255.      */
  256.     public function setMaxDepthHandler(?callable $handler): void
  257.     {
  258.         @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);
  259.         $this->maxDepthHandler $handler;
  260.     }
  261.     /**
  262.      * {@inheritdoc}
  263.      */
  264.     public function supportsDenormalization($data$type$format null)
  265.     {
  266.         return class_exists($type) || (interface_exists($typefalse) && $this->classDiscriminatorResolver && null !== $this->classDiscriminatorResolver->getMappingForClass($type));
  267.     }
  268.     /**
  269.      * {@inheritdoc}
  270.      */
  271.     public function denormalize($data$type$format null, array $context = [])
  272.     {
  273.         if (!isset($context['cache_key'])) {
  274.             $context['cache_key'] = $this->getCacheKey($format$context);
  275.         }
  276.         $allowedAttributes $this->getAllowedAttributes($type$contexttrue);
  277.         $normalizedData $this->prepareForDenormalization($data);
  278.         $extraAttributes = [];
  279.         $reflectionClass = new \ReflectionClass($type);
  280.         $object $this->instantiateObject($normalizedData$type$context$reflectionClass$allowedAttributes$format);
  281.         $resolvedClass $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  282.         foreach ($normalizedData as $attribute => $value) {
  283.             if ($this->nameConverter) {
  284.                 $attribute $this->nameConverter->denormalize($attribute$resolvedClass$format$context);
  285.             }
  286.             if ((false !== $allowedAttributes && !\in_array($attribute$allowedAttributes)) || !$this->isAllowedAttribute($resolvedClass$attribute$format$context)) {
  287.                 if (!($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES])) {
  288.                     $extraAttributes[] = $attribute;
  289.                 }
  290.                 continue;
  291.             }
  292.             if ($context[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) {
  293.                 try {
  294.                     $context[self::OBJECT_TO_POPULATE] = $this->getAttributeValue($object$attribute$format$context);
  295.                 } catch (NoSuchPropertyException $e) {
  296.                 }
  297.             }
  298.             $value $this->validateAndDenormalize($resolvedClass$attribute$value$format$context);
  299.             try {
  300.                 $this->setAttributeValue($object$attribute$value$format$context);
  301.             } catch (InvalidArgumentException $e) {
  302.                 throw new NotNormalizableValueException(sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute$type), $e->getCode(), $e);
  303.             }
  304.         }
  305.         if (!empty($extraAttributes)) {
  306.             throw new ExtraAttributesException($extraAttributes);
  307.         }
  308.         return $object;
  309.     }
  310.     /**
  311.      * Sets attribute value.
  312.      *
  313.      * @param object      $object
  314.      * @param string      $attribute
  315.      * @param mixed       $value
  316.      * @param string|null $format
  317.      */
  318.     abstract protected function setAttributeValue($object$attribute$value$format null, array $context = []);
  319.     /**
  320.      * Validates the submitted data and denormalizes it.
  321.      *
  322.      * @param mixed $data
  323.      *
  324.      * @return mixed
  325.      *
  326.      * @throws NotNormalizableValueException
  327.      * @throws LogicException
  328.      */
  329.     private function validateAndDenormalize(string $currentClassstring $attribute$data, ?string $format, array $context)
  330.     {
  331.         if (null === $types $this->getTypes($currentClass$attribute)) {
  332.             return $data;
  333.         }
  334.         $expectedTypes = [];
  335.         foreach ($types as $type) {
  336.             if (null === $data && $type->isNullable()) {
  337.                 return null;
  338.             }
  339.             $collectionValueType $type->isCollection() ? $type->getCollectionValueType() : null;
  340.             // Fix a collection that contains the only one element
  341.             // This is special to xml format only
  342.             if ('xml' === $format && null !== $collectionValueType && (!\is_array($data) || !\is_int(key($data)))) {
  343.                 $data = [$data];
  344.             }
  345.             if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
  346.                 $builtinType Type::BUILTIN_TYPE_OBJECT;
  347.                 $class $collectionValueType->getClassName().'[]';
  348.                 if (null !== $collectionKeyType $type->getCollectionKeyType()) {
  349.                     $context['key_type'] = $collectionKeyType;
  350.                 }
  351.             } elseif ($type->isCollection() && null !== ($collectionValueType $type->getCollectionValueType()) && Type::BUILTIN_TYPE_ARRAY === $collectionValueType->getBuiltinType()) {
  352.                 // get inner type for any nested array
  353.                 $innerType $collectionValueType;
  354.                 // note that it will break for any other builtinType
  355.                 $dimensions '[]';
  356.                 while (null !== $innerType->getCollectionValueType() && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) {
  357.                     $dimensions .= '[]';
  358.                     $innerType $innerType->getCollectionValueType();
  359.                 }
  360.                 if (null !== $innerType->getClassName()) {
  361.                     // the builtinType is the inner one and the class is the class followed by []...[]
  362.                     $builtinType $innerType->getBuiltinType();
  363.                     $class $innerType->getClassName().$dimensions;
  364.                 } else {
  365.                     // default fallback (keep it as array)
  366.                     $builtinType $type->getBuiltinType();
  367.                     $class $type->getClassName();
  368.                 }
  369.             } else {
  370.                 $builtinType $type->getBuiltinType();
  371.                 $class $type->getClassName();
  372.             }
  373.             $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class $class $builtinType] = true;
  374.             if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
  375.                 if (!$this->serializer instanceof DenormalizerInterface) {
  376.                     throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.'$attribute$class));
  377.                 }
  378.                 $childContext $this->createChildContext($context$attribute$format);
  379.                 if ($this->serializer->supportsDenormalization($data$class$format$childContext)) {
  380.                     return $this->serializer->denormalize($data$class$format$childContext);
  381.                 }
  382.             }
  383.             // JSON only has a Number type corresponding to both int and float PHP types.
  384.             // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
  385.             // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
  386.             // PHP's json_decode automatically converts Numbers without a decimal part to integers.
  387.             // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
  388.             // a float is expected.
  389.             if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && false !== strpos($formatJsonEncoder::FORMAT)) {
  390.                 return (float) $data;
  391.             }
  392.             if (('is_'.$builtinType)($data)) {
  393.                 return $data;
  394.             }
  395.         }
  396.         if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) {
  397.             return $data;
  398.         }
  399.         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)));
  400.     }
  401.     /**
  402.      * @internal
  403.      */
  404.     protected function denormalizeParameter(\ReflectionClass $class\ReflectionParameter $parameter$parameterName$parameterData, array $context$format null)
  405.     {
  406.         if (null === $this->propertyTypeExtractor || null === $this->propertyTypeExtractor->getTypes($class->getName(), $parameterName)) {
  407.             return parent::denormalizeParameter($class$parameter$parameterName$parameterData$context$format);
  408.         }
  409.         return $this->validateAndDenormalize($class->getName(), $parameterName$parameterData$format$context);
  410.     }
  411.     /**
  412.      * @return Type[]|null
  413.      */
  414.     private function getTypes(string $currentClassstring $attribute): ?array
  415.     {
  416.         if (null === $this->propertyTypeExtractor) {
  417.             return null;
  418.         }
  419.         $key $currentClass.'::'.$attribute;
  420.         if (isset($this->typesCache[$key])) {
  421.             return false === $this->typesCache[$key] ? null $this->typesCache[$key];
  422.         }
  423.         if (null !== $types $this->propertyTypeExtractor->getTypes($currentClass$attribute)) {
  424.             return $this->typesCache[$key] = $types;
  425.         }
  426.         if (null !== $this->classDiscriminatorResolver && null !== $discriminatorMapping $this->classDiscriminatorResolver->getMappingForClass($currentClass)) {
  427.             if ($discriminatorMapping->getTypeProperty() === $attribute) {
  428.                 return $this->typesCache[$key] = [
  429.                     new Type(Type::BUILTIN_TYPE_STRING),
  430.                 ];
  431.             }
  432.             foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
  433.                 if (null !== $types $this->propertyTypeExtractor->getTypes($mappedClass$attribute)) {
  434.                     return $this->typesCache[$key] = $types;
  435.                 }
  436.             }
  437.         }
  438.         $this->typesCache[$key] = false;
  439.         return null;
  440.     }
  441.     /**
  442.      * Sets an attribute and apply the name converter if necessary.
  443.      *
  444.      * @param mixed $attributeValue
  445.      */
  446.     private function updateData(array $datastring $attribute$attributeValuestring $class, ?string $format, array $context): array
  447.     {
  448.         if (null === $attributeValue && ($context[self::SKIP_NULL_VALUES] ?? $this->defaultContext[self::SKIP_NULL_VALUES] ?? false)) {
  449.             return $data;
  450.         }
  451.         if ($this->nameConverter) {
  452.             $attribute $this->nameConverter->normalize($attribute$class$format$context);
  453.         }
  454.         $data[$attribute] = $attributeValue;
  455.         return $data;
  456.     }
  457.     /**
  458.      * Is the max depth reached for the given attribute?
  459.      *
  460.      * @param AttributeMetadataInterface[] $attributesMetadata
  461.      */
  462.     private function isMaxDepthReached(array $attributesMetadatastring $classstring $attribute, array &$context): bool
  463.     {
  464.         $enableMaxDepth $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false;
  465.         if (
  466.             !$enableMaxDepth ||
  467.             !isset($attributesMetadata[$attribute]) ||
  468.             null === $maxDepth $attributesMetadata[$attribute]->getMaxDepth()
  469.         ) {
  470.             return false;
  471.         }
  472.         $key sprintf(self::DEPTH_KEY_PATTERN$class$attribute);
  473.         if (!isset($context[$key])) {
  474.             $context[$key] = 1;
  475.             return false;
  476.         }
  477.         if ($context[$key] === $maxDepth) {
  478.             return true;
  479.         }
  480.         ++$context[$key];
  481.         return false;
  482.     }
  483.     /**
  484.      * Overwritten to update the cache key for the child.
  485.      *
  486.      * We must not mix up the attribute cache between parent and children.
  487.      *
  488.      * {@inheritdoc}
  489.      *
  490.      * @param string|null $format
  491.      *
  492.      * @internal
  493.      */
  494.     protected function createChildContext(array $parentContext$attribute/*, ?string $format */): array
  495.     {
  496.         if (\func_num_args() >= 3) {
  497.             $format func_get_arg(2);
  498.         } else {
  499.             @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);
  500.             $format null;
  501.         }
  502.         $context parent::createChildContext($parentContext$attribute$format);
  503.         $context['cache_key'] = $this->getCacheKey($format$context);
  504.         return $context;
  505.     }
  506.     /**
  507.      * Builds the cache key for the attributes cache.
  508.      *
  509.      * The key must be different for every option in the context that could change which attributes should be handled.
  510.      *
  511.      * @return bool|string
  512.      */
  513.     private function getCacheKey(?string $format, array $context)
  514.     {
  515.         foreach ($context[self::EXCLUDE_FROM_CACHE_KEY] ?? $this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] as $key) {
  516.             unset($context[$key]);
  517.         }
  518.         unset($context[self::EXCLUDE_FROM_CACHE_KEY]);
  519.         unset($context[self::OBJECT_TO_POPULATE]);
  520.         unset($context['cache_key']); // avoid artificially different keys
  521.         try {
  522.             return md5($format.serialize([
  523.                 'context' => $context,
  524.                 'ignored' => $this->ignoredAttributes,
  525.                 'camelized' => $this->camelizedAttributes,
  526.             ]));
  527.         } catch (\Exception $exception) {
  528.             // The context cannot be serialized, skip the cache
  529.             return false;
  530.         }
  531.     }
  532. }