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

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\Mapping\AttributeMetadataInterface;
  19. use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
  20. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  21. /**
  22.  * Base class for a normalizer dealing with objects.
  23.  *
  24.  * @author Kévin Dunglas <dunglas@gmail.com>
  25.  */
  26. abstract class AbstractObjectNormalizer extends AbstractNormalizer
  27. {
  28.     const ENABLE_MAX_DEPTH 'enable_max_depth';
  29.     const DEPTH_KEY_PATTERN 'depth_%s::%s';
  30.     const DISABLE_TYPE_ENFORCEMENT 'disable_type_enforcement';
  31.     private $propertyTypeExtractor;
  32.     private $attributesCache = [];
  33.     private $cache = [];
  34.     public function __construct(ClassMetadataFactoryInterface $classMetadataFactory nullNameConverterInterface $nameConverter nullPropertyTypeExtractorInterface $propertyTypeExtractor null)
  35.     {
  36.         parent::__construct($classMetadataFactory$nameConverter);
  37.         $this->propertyTypeExtractor $propertyTypeExtractor;
  38.     }
  39.     /**
  40.      * {@inheritdoc}
  41.      */
  42.     public function supportsNormalization($data$format null)
  43.     {
  44.         return \is_object($data) && !$data instanceof \Traversable;
  45.     }
  46.     /**
  47.      * {@inheritdoc}
  48.      */
  49.     public function normalize($object$format null, array $context = [])
  50.     {
  51.         if (!isset($context['cache_key'])) {
  52.             $context['cache_key'] = $this->getCacheKey($format$context);
  53.         }
  54.         if ($this->isCircularReference($object$context)) {
  55.             return $this->handleCircularReference($object);
  56.         }
  57.         $data = [];
  58.         $stack = [];
  59.         $attributes $this->getAttributes($object$format$context);
  60.         $class \get_class($object);
  61.         $attributesMetadata $this->classMetadataFactory $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
  62.         foreach ($attributes as $attribute) {
  63.             if (null !== $attributesMetadata && $this->isMaxDepthReached($attributesMetadata$class$attribute$context)) {
  64.                 continue;
  65.             }
  66.             $attributeValue $this->getAttributeValue($object$attribute$format$context);
  67.             if (isset($this->callbacks[$attribute])) {
  68.                 $attributeValue \call_user_func($this->callbacks[$attribute], $attributeValue);
  69.             }
  70.             if (null !== $attributeValue && !is_scalar($attributeValue)) {
  71.                 $stack[$attribute] = $attributeValue;
  72.             }
  73.             $data $this->updateData($data$attribute$attributeValue);
  74.         }
  75.         foreach ($stack as $attribute => $attributeValue) {
  76.             if (!$this->serializer instanceof NormalizerInterface) {
  77.                 throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer'$attribute));
  78.             }
  79.             $data $this->updateData($data$attribute$this->serializer->normalize($attributeValue$format$this->createChildContext($context$attribute$format)));
  80.         }
  81.         return $data;
  82.     }
  83.     /**
  84.      * Gets and caches attributes for the given object, format and context.
  85.      *
  86.      * @param object      $object
  87.      * @param string|null $format
  88.      *
  89.      * @return string[]
  90.      */
  91.     protected function getAttributes($object$format null, array $context)
  92.     {
  93.         $class \get_class($object);
  94.         $key $class.'-'.$context['cache_key'];
  95.         if (isset($this->attributesCache[$key])) {
  96.             return $this->attributesCache[$key];
  97.         }
  98.         $allowedAttributes $this->getAllowedAttributes($object$contexttrue);
  99.         if (false !== $allowedAttributes) {
  100.             if ($context['cache_key']) {
  101.                 $this->attributesCache[$key] = $allowedAttributes;
  102.             }
  103.             return $allowedAttributes;
  104.         }
  105.         $attributes $this->extractAttributes($object$format$context);
  106.         if ($context['cache_key']) {
  107.             $this->attributesCache[$key] = $attributes;
  108.         }
  109.         return $attributes;
  110.     }
  111.     /**
  112.      * Extracts attributes to normalize from the class of the given object, format and context.
  113.      *
  114.      * @param object      $object
  115.      * @param string|null $format
  116.      *
  117.      * @return string[]
  118.      */
  119.     abstract protected function extractAttributes($object$format null, array $context = []);
  120.     /**
  121.      * Gets the attribute value.
  122.      *
  123.      * @param object      $object
  124.      * @param string      $attribute
  125.      * @param string|null $format
  126.      *
  127.      * @return mixed
  128.      */
  129.     abstract protected function getAttributeValue($object$attribute$format null, array $context = []);
  130.     /**
  131.      * {@inheritdoc}
  132.      */
  133.     public function supportsDenormalization($data$type$format null)
  134.     {
  135.         return isset($this->cache[$type]) ? $this->cache[$type] : $this->cache[$type] = class_exists($type);
  136.     }
  137.     /**
  138.      * {@inheritdoc}
  139.      */
  140.     public function denormalize($data$type$format null, array $context = [])
  141.     {
  142.         if (!isset($context['cache_key'])) {
  143.             $context['cache_key'] = $this->getCacheKey($format$context);
  144.         }
  145.         $allowedAttributes $this->getAllowedAttributes($type$contexttrue);
  146.         $normalizedData $this->prepareForDenormalization($data);
  147.         $extraAttributes = [];
  148.         $reflectionClass = new \ReflectionClass($type);
  149.         $object $this->instantiateObject($normalizedData$type$context$reflectionClass$allowedAttributes$format);
  150.         foreach ($normalizedData as $attribute => $value) {
  151.             if ($this->nameConverter) {
  152.                 $attribute $this->nameConverter->denormalize($attribute);
  153.             }
  154.             if ((false !== $allowedAttributes && !\in_array($attribute$allowedAttributes)) || !$this->isAllowedAttribute($type$attribute$format$context)) {
  155.                 if (isset($context[self::ALLOW_EXTRA_ATTRIBUTES]) && !$context[self::ALLOW_EXTRA_ATTRIBUTES]) {
  156.                     $extraAttributes[] = $attribute;
  157.                 }
  158.                 continue;
  159.             }
  160.             $value $this->validateAndDenormalize($type$attribute$value$format$context);
  161.             try {
  162.                 $this->setAttributeValue($object$attribute$value$format$context);
  163.             } catch (InvalidArgumentException $e) {
  164.                 throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
  165.             }
  166.         }
  167.         if (!empty($extraAttributes)) {
  168.             throw new ExtraAttributesException($extraAttributes);
  169.         }
  170.         return $object;
  171.     }
  172.     /**
  173.      * Sets attribute value.
  174.      *
  175.      * @param object      $object
  176.      * @param string      $attribute
  177.      * @param mixed       $value
  178.      * @param string|null $format
  179.      */
  180.     abstract protected function setAttributeValue($object$attribute$value$format null, array $context = []);
  181.     /**
  182.      * Validates the submitted data and denormalizes it.
  183.      *
  184.      * @param string      $currentClass
  185.      * @param string      $attribute
  186.      * @param mixed       $data
  187.      * @param string|null $format
  188.      *
  189.      * @return mixed
  190.      *
  191.      * @throws NotNormalizableValueException
  192.      * @throws LogicException
  193.      */
  194.     private function validateAndDenormalize($currentClass$attribute$data$format, array $context)
  195.     {
  196.         if (null === $this->propertyTypeExtractor || null === $types $this->propertyTypeExtractor->getTypes($currentClass$attribute)) {
  197.             return $data;
  198.         }
  199.         $expectedTypes = [];
  200.         foreach ($types as $type) {
  201.             if (null === $data && $type->isNullable()) {
  202.                 return null;
  203.             }
  204.             $collectionValueType $type->isCollection() ? $type->getCollectionValueType() : null;
  205.             // Fix a collection that contains the only one element
  206.             // This is special to xml format only
  207.             if ('xml' === $format && null !== $collectionValueType && (!\is_array($data) || !\is_int(key($data)))) {
  208.                 $data = [$data];
  209.             }
  210.             if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
  211.                 $builtinType Type::BUILTIN_TYPE_OBJECT;
  212.                 $class $collectionValueType->getClassName().'[]';
  213.                 if (null !== $collectionKeyType $type->getCollectionKeyType()) {
  214.                     $context['key_type'] = $collectionKeyType;
  215.                 }
  216.             } else {
  217.                 $builtinType $type->getBuiltinType();
  218.                 $class $type->getClassName();
  219.             }
  220.             $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class $class $builtinType] = true;
  221.             if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
  222.                 if (!$this->serializer instanceof DenormalizerInterface) {
  223.                     throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer'$attribute$class));
  224.                 }
  225.                 $childContext $this->createChildContext($context$attribute$format);
  226.                 if ($this->serializer->supportsDenormalization($data$class$format$childContext)) {
  227.                     return $this->serializer->denormalize($data$class$format$childContext);
  228.                 }
  229.             }
  230.             // JSON only has a Number type corresponding to both int and float PHP types.
  231.             // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
  232.             // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
  233.             // PHP's json_decode automatically converts Numbers without a decimal part to integers.
  234.             // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
  235.             // a float is expected.
  236.             if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && false !== strpos($formatJsonEncoder::FORMAT)) {
  237.                 return (float) $data;
  238.             }
  239.             if (\call_user_func('is_'.$builtinType$data)) {
  240.                 return $data;
  241.             }
  242.         }
  243.         if (!empty($context[self::DISABLE_TYPE_ENFORCEMENT])) {
  244.             return $data;
  245.         }
  246.         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)));
  247.     }
  248.     /**
  249.      * @internal
  250.      */
  251.     protected function denormalizeParameter(\ReflectionClass $class\ReflectionParameter $parameter$parameterName$parameterData, array $context$format null)
  252.     {
  253.         if (null === $this->propertyTypeExtractor || null === $types $this->propertyTypeExtractor->getTypes($class->getName(), $parameterName)) {
  254.             return parent::denormalizeParameter($class$parameter$parameterName$parameterData$context$format);
  255.         }
  256.         return $this->validateAndDenormalize($class->getName(), $parameterName$parameterData$format$context);
  257.     }
  258.     /**
  259.      * Sets an attribute and apply the name converter if necessary.
  260.      *
  261.      * @param string $attribute
  262.      * @param mixed  $attributeValue
  263.      *
  264.      * @return array
  265.      */
  266.     private function updateData(array $data$attribute$attributeValue)
  267.     {
  268.         if ($this->nameConverter) {
  269.             $attribute $this->nameConverter->normalize($attribute);
  270.         }
  271.         $data[$attribute] = $attributeValue;
  272.         return $data;
  273.     }
  274.     /**
  275.      * Is the max depth reached for the given attribute?
  276.      *
  277.      * @param AttributeMetadataInterface[] $attributesMetadata
  278.      * @param string                       $class
  279.      * @param string                       $attribute
  280.      *
  281.      * @return bool
  282.      */
  283.     private function isMaxDepthReached(array $attributesMetadata$class$attribute, array &$context)
  284.     {
  285.         if (
  286.             !isset($context[static::ENABLE_MAX_DEPTH]) ||
  287.             !$context[static::ENABLE_MAX_DEPTH] ||
  288.             !isset($attributesMetadata[$attribute]) ||
  289.             null === $maxDepth $attributesMetadata[$attribute]->getMaxDepth()
  290.         ) {
  291.             return false;
  292.         }
  293.         $key sprintf(static::DEPTH_KEY_PATTERN$class$attribute);
  294.         if (!isset($context[$key])) {
  295.             $context[$key] = 1;
  296.             return false;
  297.         }
  298.         if ($context[$key] === $maxDepth) {
  299.             return true;
  300.         }
  301.         ++$context[$key];
  302.         return false;
  303.     }
  304.     /**
  305.      * Overwritten to update the cache key for the child.
  306.      *
  307.      * We must not mix up the attribute cache between parent and children.
  308.      *
  309.      * {@inheritdoc}
  310.      */
  311.     protected function createChildContext(array $parentContext$attribute/*, string $format = null */)
  312.     {
  313.         if (\func_num_args() >= 3) {
  314.             $format func_get_arg(2);
  315.         } else {
  316.             // will be deprecated in version 4
  317.             $format null;
  318.         }
  319.         $context parent::createChildContext($parentContext$attribute$format);
  320.         // format is already included in the cache_key of the parent.
  321.         $context['cache_key'] = $this->getCacheKey($format$context);
  322.         return $context;
  323.     }
  324.     /**
  325.      * Builds the cache key for the attributes cache.
  326.      *
  327.      * The key must be different for every option in the context that could change which attributes should be handled.
  328.      *
  329.      * @param string|null $format
  330.      *
  331.      * @return bool|string
  332.      */
  333.     private function getCacheKey($format, array $context)
  334.     {
  335.         unset($context['cache_key']); // avoid artificially different keys
  336.         try {
  337.             return md5($format.serialize([
  338.                 'context' => $context,
  339.                 'ignored' => $this->ignoredAttributes,
  340.                 'camelized' => $this->camelizedAttributes,
  341.             ]));
  342.         } catch (\Exception $exception) {
  343.             // The context cannot be serialized, skip the cache
  344.             return false;
  345.         }
  346.     }
  347. }