vendor/symfony/serializer/Normalizer/AbstractNormalizer.php line 254

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\Serializer\Exception\CircularReferenceException;
  12. use Symfony\Component\Serializer\Exception\InvalidArgumentException;
  13. use Symfony\Component\Serializer\Exception\LogicException;
  14. use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
  15. use Symfony\Component\Serializer\Exception\RuntimeException;
  16. use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
  17. use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
  18. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  19. use Symfony\Component\Serializer\SerializerAwareInterface;
  20. use Symfony\Component\Serializer\SerializerAwareTrait;
  21. /**
  22.  * Normalizer implementation.
  23.  *
  24.  * @author Kévin Dunglas <dunglas@gmail.com>
  25.  */
  26. abstract class AbstractNormalizer implements NormalizerInterfaceDenormalizerInterfaceSerializerAwareInterfaceCacheableSupportsMethodInterface
  27. {
  28.     use ObjectToPopulateTrait;
  29.     use SerializerAwareTrait;
  30.     /* constants to configure the context */
  31.     /**
  32.      * How many loops of circular reference to allow while normalizing.
  33.      *
  34.      * The default value of 1 means that when we encounter the same object a
  35.      * second time, we consider that a circular reference.
  36.      *
  37.      * You can raise this value for special cases, e.g. in combination with the
  38.      * max depth setting of the object normalizer.
  39.      */
  40.     public const CIRCULAR_REFERENCE_LIMIT 'circular_reference_limit';
  41.     /**
  42.      * Instead of creating a new instance of an object, update the specified object.
  43.      *
  44.      * If you have a nested structure, child objects will be overwritten with
  45.      * new instances unless you set DEEP_OBJECT_TO_POPULATE to true.
  46.      */
  47.     public const OBJECT_TO_POPULATE 'object_to_populate';
  48.     /**
  49.      * Only (de)normalize attributes that are in the specified groups.
  50.      */
  51.     public const GROUPS 'groups';
  52.     /**
  53.      * Limit (de)normalize to the specified names.
  54.      *
  55.      * For nested structures, this list needs to reflect the object tree.
  56.      */
  57.     public const ATTRIBUTES 'attributes';
  58.     /**
  59.      * If ATTRIBUTES are specified, and the source has fields that are not part of that list,
  60.      * either ignore those attributes (true) or throw an ExtraAttributesException (false).
  61.      */
  62.     public const ALLOW_EXTRA_ATTRIBUTES 'allow_extra_attributes';
  63.     /**
  64.      * Hashmap of default values for constructor arguments.
  65.      *
  66.      * The names need to match the parameter names in the constructor arguments.
  67.      */
  68.     public const DEFAULT_CONSTRUCTOR_ARGUMENTS 'default_constructor_arguments';
  69.     /**
  70.      * Hashmap of field name => callable to normalize this field.
  71.      *
  72.      * The callable is called if the field is encountered with the arguments:
  73.      *
  74.      * - mixed  $attributeValue value of this field
  75.      * - object $object         the whole object being normalized
  76.      * - string $attributeName  name of the attribute being normalized
  77.      * - string $format         the requested format
  78.      * - array  $context        the serialization context
  79.      */
  80.     public const CALLBACKS 'callbacks';
  81.     /**
  82.      * Handler to call when a circular reference has been detected.
  83.      *
  84.      * If you specify no handler, a CircularReferenceException is thrown.
  85.      *
  86.      * The method will be called with ($object, $format, $context) and its
  87.      * return value is returned as the result of the normalize call.
  88.      */
  89.     public const CIRCULAR_REFERENCE_HANDLER 'circular_reference_handler';
  90.     /**
  91.      * Skip the specified attributes when normalizing an object tree.
  92.      *
  93.      * This list is applied to each element of nested structures.
  94.      *
  95.      * Note: The behaviour for nested structures is different from ATTRIBUTES
  96.      * for historical reason. Aligning the behaviour would be a BC break.
  97.      */
  98.     public const IGNORED_ATTRIBUTES 'ignored_attributes';
  99.     /**
  100.      * @internal
  101.      */
  102.     protected const CIRCULAR_REFERENCE_LIMIT_COUNTERS 'circular_reference_limit_counters';
  103.     protected $defaultContext = [
  104.         self::ALLOW_EXTRA_ATTRIBUTES => true,
  105.         self::CIRCULAR_REFERENCE_LIMIT => 1,
  106.         self::IGNORED_ATTRIBUTES => [],
  107.     ];
  108.     /**
  109.      * @deprecated since Symfony 4.2
  110.      */
  111.     protected $circularReferenceLimit 1;
  112.     /**
  113.      * @deprecated since Symfony 4.2
  114.      *
  115.      * @var callable|null
  116.      */
  117.     protected $circularReferenceHandler;
  118.     /**
  119.      * @var ClassMetadataFactoryInterface|null
  120.      */
  121.     protected $classMetadataFactory;
  122.     /**
  123.      * @var NameConverterInterface|null
  124.      */
  125.     protected $nameConverter;
  126.     /**
  127.      * @deprecated since Symfony 4.2
  128.      */
  129.     protected $callbacks = [];
  130.     /**
  131.      * @deprecated since Symfony 4.2
  132.      */
  133.     protected $ignoredAttributes = [];
  134.     /**
  135.      * @deprecated since Symfony 4.2
  136.      */
  137.     protected $camelizedAttributes = [];
  138.     /**
  139.      * Sets the {@link ClassMetadataFactoryInterface} to use.
  140.      */
  141.     public function __construct(ClassMetadataFactoryInterface $classMetadataFactory nullNameConverterInterface $nameConverter null, array $defaultContext = [])
  142.     {
  143.         $this->classMetadataFactory $classMetadataFactory;
  144.         $this->nameConverter $nameConverter;
  145.         $this->defaultContext array_merge($this->defaultContext$defaultContext);
  146.         if (isset($this->defaultContext[self::CALLBACKS])) {
  147.             if (!\is_array($this->defaultContext[self::CALLBACKS])) {
  148.                 throw new InvalidArgumentException(sprintf('The "%s" default context option must be an array of callables.'self::CALLBACKS));
  149.             }
  150.             foreach ($this->defaultContext[self::CALLBACKS] as $attribute => $callback) {
  151.                 if (!\is_callable($callback)) {
  152.                     throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s" default context option.'$attributeself::CALLBACKS));
  153.                 }
  154.             }
  155.         }
  156.         if (isset($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER]) && !\is_callable($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER])) {
  157.             throw new InvalidArgumentException(sprintf('Invalid callback found in the "%s" default context option.'self::CIRCULAR_REFERENCE_HANDLER));
  158.         }
  159.     }
  160.     /**
  161.      * Sets circular reference limit.
  162.      *
  163.      * @deprecated since Symfony 4.2
  164.      *
  165.      * @param int $circularReferenceLimit Limit of iterations for the same object
  166.      *
  167.      * @return self
  168.      */
  169.     public function setCircularReferenceLimit($circularReferenceLimit)
  170.     {
  171.         @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "circular_reference_limit" key of the context instead.'__METHOD__), \E_USER_DEPRECATED);
  172.         $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT] = $this->circularReferenceLimit $circularReferenceLimit;
  173.         return $this;
  174.     }
  175.     /**
  176.      * Sets circular reference handler.
  177.      *
  178.      * @deprecated since Symfony 4.2
  179.      *
  180.      * @return self
  181.      */
  182.     public function setCircularReferenceHandler(callable $circularReferenceHandler)
  183.     {
  184.         @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "circular_reference_handler" key of the context instead.'__METHOD__), \E_USER_DEPRECATED);
  185.         $this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER] = $this->circularReferenceHandler $circularReferenceHandler;
  186.         return $this;
  187.     }
  188.     /**
  189.      * Sets normalization callbacks.
  190.      *
  191.      * @deprecated since Symfony 4.2
  192.      *
  193.      * @param callable[] $callbacks Help normalize the result
  194.      *
  195.      * @return self
  196.      *
  197.      * @throws InvalidArgumentException if a non-callable callback is set
  198.      */
  199.     public function setCallbacks(array $callbacks)
  200.     {
  201.         @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "callbacks" key of the context instead.'__METHOD__), \E_USER_DEPRECATED);
  202.         foreach ($callbacks as $attribute => $callback) {
  203.             if (!\is_callable($callback)) {
  204.                 throw new InvalidArgumentException(sprintf('The given callback for attribute "%s" is not callable.'$attribute));
  205.             }
  206.         }
  207.         $this->defaultContext[self::CALLBACKS] = $this->callbacks $callbacks;
  208.         return $this;
  209.     }
  210.     /**
  211.      * Sets ignored attributes for normalization and denormalization.
  212.      *
  213.      * @deprecated since Symfony 4.2
  214.      *
  215.      * @return self
  216.      */
  217.     public function setIgnoredAttributes(array $ignoredAttributes)
  218.     {
  219.         @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "ignored_attributes" key of the context instead.'__METHOD__), \E_USER_DEPRECATED);
  220.         $this->defaultContext[self::IGNORED_ATTRIBUTES] = $this->ignoredAttributes $ignoredAttributes;
  221.         return $this;
  222.     }
  223.     /**
  224.      * {@inheritdoc}
  225.      */
  226.     public function hasCacheableSupportsMethod(): bool
  227.     {
  228.         return false;
  229.     }
  230.     /**
  231.      * Detects if the configured circular reference limit is reached.
  232.      *
  233.      * @param object $object
  234.      * @param array  $context
  235.      *
  236.      * @return bool
  237.      *
  238.      * @throws CircularReferenceException
  239.      */
  240.     protected function isCircularReference($object, &$context)
  241.     {
  242.         $objectHash spl_object_hash($object);
  243.         $circularReferenceLimit $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->circularReferenceLimit;
  244.         if (isset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash])) {
  245.             if ($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] >= $circularReferenceLimit) {
  246.                 unset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]);
  247.                 return true;
  248.             }
  249.             ++$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash];
  250.         } else {
  251.             $context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] = 1;
  252.         }
  253.         return false;
  254.     }
  255.     /**
  256.      * Handles a circular reference.
  257.      *
  258.      * If a circular reference handler is set, it will be called. Otherwise, a
  259.      * {@class CircularReferenceException} will be thrown.
  260.      *
  261.      * @final since Symfony 4.2
  262.      *
  263.      * @param object      $object
  264.      * @param string|null $format
  265.      * @param array       $context
  266.      *
  267.      * @return mixed
  268.      *
  269.      * @throws CircularReferenceException
  270.      */
  271.     protected function handleCircularReference($object/*, string $format = null, array $context = []*/)
  272.     {
  273.         if (\func_num_args() < && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this__FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) {
  274.             @trigger_error(sprintf('The "%s()" method will have two new "string $format = null" and "array $context = []" arguments in version 5.0, not defining it is deprecated since Symfony 4.2.'__METHOD__), \E_USER_DEPRECATED);
  275.         }
  276.         $format = \func_num_args() > func_get_arg(1) : null;
  277.         $context = \func_num_args() > func_get_arg(2) : [];
  278.         $circularReferenceHandler $context[self::CIRCULAR_REFERENCE_HANDLER] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER] ?? $this->circularReferenceHandler;
  279.         if ($circularReferenceHandler) {
  280.             return $circularReferenceHandler($object$format$context);
  281.         }
  282.         throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', \get_class($object), $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->circularReferenceLimit));
  283.     }
  284.     /**
  285.      * Gets attributes to normalize using groups.
  286.      *
  287.      * @param string|object $classOrObject
  288.      * @param bool          $attributesAsString If false, return an array of {@link AttributeMetadataInterface}
  289.      *
  290.      * @throws LogicException if the 'allow_extra_attributes' context variable is false and no class metadata factory is provided
  291.      *
  292.      * @return string[]|AttributeMetadataInterface[]|bool
  293.      */
  294.     protected function getAllowedAttributes($classOrObject, array $context$attributesAsString false)
  295.     {
  296.         $allowExtraAttributes $context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES];
  297.         if (!$this->classMetadataFactory) {
  298.             if (!$allowExtraAttributes) {
  299.                 throw new LogicException(sprintf('A class metadata factory must be provided in the constructor when setting "%s" to false.'self::ALLOW_EXTRA_ATTRIBUTES));
  300.             }
  301.             return false;
  302.         }
  303.         $tmpGroups $context[self::GROUPS] ?? $this->defaultContext[self::GROUPS] ?? null;
  304.         $groups = (\is_array($tmpGroups) || is_scalar($tmpGroups)) ? (array) $tmpGroups false;
  305.         if (false === $groups && $allowExtraAttributes) {
  306.             return false;
  307.         }
  308.         $allowedAttributes = [];
  309.         foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) {
  310.             $name $attributeMetadata->getName();
  311.             if (
  312.                 (false === $groups || array_intersect($attributeMetadata->getGroups(), $groups)) &&
  313.                 $this->isAllowedAttribute($classOrObject$namenull$context)
  314.             ) {
  315.                 $allowedAttributes[] = $attributesAsString $name $attributeMetadata;
  316.             }
  317.         }
  318.         return $allowedAttributes;
  319.     }
  320.     /**
  321.      * Is this attribute allowed?
  322.      *
  323.      * @param object|string $classOrObject
  324.      * @param string        $attribute
  325.      * @param string|null   $format
  326.      *
  327.      * @return bool
  328.      */
  329.     protected function isAllowedAttribute($classOrObject$attribute$format null, array $context = [])
  330.     {
  331.         $ignoredAttributes $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES] ?? $this->ignoredAttributes;
  332.         if (\in_array($attribute$ignoredAttributes)) {
  333.             return false;
  334.         }
  335.         $attributes $context[self::ATTRIBUTES] ?? $this->defaultContext[self::ATTRIBUTES] ?? null;
  336.         if (isset($attributes[$attribute])) {
  337.             // Nested attributes
  338.             return true;
  339.         }
  340.         if (\is_array($attributes)) {
  341.             return \in_array($attribute$attributestrue);
  342.         }
  343.         return true;
  344.     }
  345.     /**
  346.      * Normalizes the given data to an array. It's particularly useful during
  347.      * the denormalization process.
  348.      *
  349.      * @param object|array $data
  350.      *
  351.      * @return array
  352.      */
  353.     protected function prepareForDenormalization($data)
  354.     {
  355.         return (array) $data;
  356.     }
  357.     /**
  358.      * Returns the method to use to construct an object. This method must be either
  359.      * the object constructor or static.
  360.      *
  361.      * @param string     $class
  362.      * @param array|bool $allowedAttributes
  363.      *
  364.      * @return \ReflectionMethod|null
  365.      */
  366.     protected function getConstructor(array &$data$class, array &$context, \ReflectionClass $reflectionClass$allowedAttributes)
  367.     {
  368.         return $reflectionClass->getConstructor();
  369.     }
  370.     /**
  371.      * Instantiates an object using constructor parameters when needed.
  372.      *
  373.      * This method also allows to denormalize data into an existing object if
  374.      * it is present in the context with the object_to_populate. This object
  375.      * is removed from the context before being returned to avoid side effects
  376.      * when recursively normalizing an object graph.
  377.      *
  378.      * @param string     $class
  379.      * @param array|bool $allowedAttributes
  380.      *
  381.      * @return object
  382.      *
  383.      * @throws RuntimeException
  384.      * @throws MissingConstructorArgumentsException
  385.      */
  386.     protected function instantiateObject(array &$data$class, array &$context, \ReflectionClass $reflectionClass$allowedAttributesstring $format null)
  387.     {
  388.         if (null !== $object $this->extractObjectToPopulate($class$contextself::OBJECT_TO_POPULATE)) {
  389.             unset($context[self::OBJECT_TO_POPULATE]);
  390.             return $object;
  391.         }
  392.         // clean up even if no match
  393.         unset($context[static::OBJECT_TO_POPULATE]);
  394.         $constructor $this->getConstructor($data$class$context$reflectionClass$allowedAttributes);
  395.         if ($constructor) {
  396.             if (true !== $constructor->isPublic()) {
  397.                 return $reflectionClass->newInstanceWithoutConstructor();
  398.             }
  399.             $constructorParameters $constructor->getParameters();
  400.             $params = [];
  401.             foreach ($constructorParameters as $constructorParameter) {
  402.                 $paramName $constructorParameter->name;
  403.                 $key $this->nameConverter $this->nameConverter->normalize($paramName$class$format$context) : $paramName;
  404.                 $allowed false === $allowedAttributes || \in_array($paramName$allowedAttributes);
  405.                 $ignored = !$this->isAllowedAttribute($class$paramName$format$context);
  406.                 if ($constructorParameter->isVariadic()) {
  407.                     if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key$data))) {
  408.                         if (!\is_array($data[$paramName])) {
  409.                             throw new RuntimeException(sprintf('Cannot create an instance of "%s" from serialized data because the variadic parameter "%s" can only accept an array.'$class$constructorParameter->name));
  410.                         }
  411.                         $variadicParameters = [];
  412.                         foreach ($data[$paramName] as $parameterData) {
  413.                             $variadicParameters[] = $this->denormalizeParameter($reflectionClass$constructorParameter$paramName$parameterData$context$format);
  414.                         }
  415.                         $params array_merge($params$variadicParameters);
  416.                         unset($data[$key]);
  417.                     }
  418.                 } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key$data))) {
  419.                     $parameterData $data[$key];
  420.                     if (null === $parameterData && $constructorParameter->allowsNull()) {
  421.                         $params[] = null;
  422.                         // Don't run set for a parameter passed to the constructor
  423.                         unset($data[$key]);
  424.                         continue;
  425.                     }
  426.                     // Don't run set for a parameter passed to the constructor
  427.                     $params[] = $this->denormalizeParameter($reflectionClass$constructorParameter$paramName$parameterData$context$format);
  428.                     unset($data[$key]);
  429.                 } elseif (\array_key_exists($key$context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
  430.                     $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
  431.                 } elseif (\array_key_exists($key$this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
  432.                     $params[] = $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
  433.                 } elseif ($constructorParameter->isDefaultValueAvailable()) {
  434.                     $params[] = $constructorParameter->getDefaultValue();
  435.                 } elseif ($constructorParameter->hasType() && $constructorParameter->getType()->allowsNull()) {
  436.                     $params[] = null;
  437.                 } else {
  438.                     throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.'$class$constructorParameter->name));
  439.                 }
  440.             }
  441.             if ($constructor->isConstructor()) {
  442.                 return $reflectionClass->newInstanceArgs($params);
  443.             } else {
  444.                 return $constructor->invokeArgs(null$params);
  445.             }
  446.         }
  447.         return new $class();
  448.     }
  449.     /**
  450.      * @internal
  451.      */
  452.     protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter$parameterName$parameterData, array $context$format null)
  453.     {
  454.         try {
  455.             if (($parameterType $parameter->getType()) instanceof \ReflectionNamedType && !$parameterType->isBuiltin()) {
  456.                 $parameterClass $parameterType->getName();
  457.                 new \ReflectionClass($parameterClass); // throws a \ReflectionException if the class doesn't exist
  458.                 if (!$this->serializer instanceof DenormalizerInterface) {
  459.                     throw new LogicException(sprintf('Cannot create an instance of "%s" from serialized data because the serializer inject in "%s" is not a denormalizer.'$parameterClass, static::class));
  460.                 }
  461.                 return $this->serializer->denormalize($parameterData$parameterClass$format$this->createChildContext($context$parameterName$format));
  462.             }
  463.         } catch (\ReflectionException $e) {
  464.             throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".'$parameterName), 0$e);
  465.         } catch (MissingConstructorArgumentsException $e) {
  466.             if (!$parameter->getType()->allowsNull()) {
  467.                 throw $e;
  468.             }
  469.             return null;
  470.         }
  471.         return $parameterData;
  472.     }
  473.     /**
  474.      * @param string $attribute Attribute name
  475.      *
  476.      * @internal
  477.      */
  478.     protected function createChildContext(array $parentContext$attribute/*, ?string $format */): array
  479.     {
  480.         if (\func_num_args() < 3) {
  481.             @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);
  482.         }
  483.         if (isset($parentContext[self::ATTRIBUTES][$attribute])) {
  484.             $parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute];
  485.         } else {
  486.             unset($parentContext[self::ATTRIBUTES]);
  487.         }
  488.         return $parentContext;
  489.     }
  490. }