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

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