Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Serializer\Normalizer; Chris@0: Chris@0: use Symfony\Component\Serializer\Exception\CircularReferenceException; Chris@0: use Symfony\Component\Serializer\Exception\InvalidArgumentException; Chris@0: use Symfony\Component\Serializer\Exception\LogicException; Chris@0: use Symfony\Component\Serializer\Exception\RuntimeException; Chris@17: use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; Chris@0: use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; Chris@0: use Symfony\Component\Serializer\NameConverter\NameConverterInterface; Chris@0: use Symfony\Component\Serializer\SerializerAwareInterface; Chris@0: Chris@0: /** Chris@0: * Normalizer implementation. Chris@0: * Chris@0: * @author Kévin Dunglas Chris@0: */ Chris@0: abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface Chris@0: { Chris@14: use ObjectToPopulateTrait; Chris@14: Chris@0: const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit'; Chris@0: const OBJECT_TO_POPULATE = 'object_to_populate'; Chris@0: const GROUPS = 'groups'; Chris@14: const ATTRIBUTES = 'attributes'; Chris@14: const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes'; Chris@0: Chris@0: /** Chris@0: * @var int Chris@0: */ Chris@0: protected $circularReferenceLimit = 1; Chris@0: Chris@0: /** Chris@0: * @var callable Chris@0: */ Chris@0: protected $circularReferenceHandler; Chris@0: Chris@0: /** Chris@0: * @var ClassMetadataFactoryInterface|null Chris@0: */ Chris@0: protected $classMetadataFactory; Chris@0: Chris@0: /** Chris@0: * @var NameConverterInterface|null Chris@0: */ Chris@0: protected $nameConverter; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@17: protected $callbacks = []; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@17: protected $ignoredAttributes = []; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@17: protected $camelizedAttributes = []; Chris@0: Chris@0: /** Chris@0: * Sets the {@link ClassMetadataFactoryInterface} to use. Chris@0: */ Chris@0: public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null) Chris@0: { Chris@0: $this->classMetadataFactory = $classMetadataFactory; Chris@0: $this->nameConverter = $nameConverter; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set circular reference limit. Chris@0: * Chris@14: * @param int $circularReferenceLimit Limit of iterations for the same object Chris@0: * Chris@0: * @return self Chris@0: */ Chris@0: public function setCircularReferenceLimit($circularReferenceLimit) Chris@0: { Chris@0: $this->circularReferenceLimit = $circularReferenceLimit; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set circular reference handler. Chris@0: * Chris@0: * @param callable $circularReferenceHandler Chris@0: * Chris@0: * @return self Chris@0: */ Chris@0: public function setCircularReferenceHandler(callable $circularReferenceHandler) Chris@0: { Chris@0: $this->circularReferenceHandler = $circularReferenceHandler; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set normalization callbacks. Chris@0: * Chris@14: * @param callable[] $callbacks Help normalize the result Chris@0: * Chris@0: * @return self Chris@0: * Chris@0: * @throws InvalidArgumentException if a non-callable callback is set Chris@0: */ Chris@0: public function setCallbacks(array $callbacks) Chris@0: { Chris@0: foreach ($callbacks as $attribute => $callback) { Chris@14: if (!\is_callable($callback)) { Chris@17: throw new InvalidArgumentException(sprintf('The given callback for attribute "%s" is not callable.', $attribute)); Chris@0: } Chris@0: } Chris@0: $this->callbacks = $callbacks; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set ignored attributes for normalization and denormalization. Chris@0: * Chris@0: * @return self Chris@0: */ Chris@0: public function setIgnoredAttributes(array $ignoredAttributes) Chris@0: { Chris@0: $this->ignoredAttributes = $ignoredAttributes; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Detects if the configured circular reference limit is reached. Chris@0: * Chris@0: * @param object $object Chris@0: * @param array $context Chris@0: * Chris@0: * @return bool Chris@0: * Chris@0: * @throws CircularReferenceException Chris@0: */ Chris@0: protected function isCircularReference($object, &$context) Chris@0: { Chris@0: $objectHash = spl_object_hash($object); Chris@0: Chris@0: if (isset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash])) { Chris@0: if ($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] >= $this->circularReferenceLimit) { Chris@0: unset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]); Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: ++$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]; Chris@0: } else { Chris@0: $context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] = 1; Chris@0: } Chris@0: Chris@0: return false; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Handles a circular reference. Chris@0: * Chris@0: * If a circular reference handler is set, it will be called. Otherwise, a Chris@0: * {@class CircularReferenceException} will be thrown. Chris@0: * Chris@0: * @param object $object Chris@0: * Chris@0: * @return mixed Chris@0: * Chris@0: * @throws CircularReferenceException Chris@0: */ Chris@0: protected function handleCircularReference($object) Chris@0: { Chris@0: if ($this->circularReferenceHandler) { Chris@14: return \call_user_func($this->circularReferenceHandler, $object); Chris@0: } Chris@0: Chris@14: throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d)', \get_class($object), $this->circularReferenceLimit)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets attributes to normalize using groups. Chris@0: * Chris@0: * @param string|object $classOrObject Chris@0: * @param array $context Chris@0: * @param bool $attributesAsString If false, return an array of {@link AttributeMetadataInterface} Chris@0: * Chris@16: * @throws LogicException if the 'allow_extra_attributes' context variable is false and no class metadata factory is provided Chris@16: * Chris@0: * @return string[]|AttributeMetadataInterface[]|bool Chris@0: */ Chris@0: protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false) Chris@0: { Chris@14: if (!$this->classMetadataFactory) { Chris@16: if (isset($context[static::ALLOW_EXTRA_ATTRIBUTES]) && !$context[static::ALLOW_EXTRA_ATTRIBUTES]) { Chris@16: throw new LogicException(sprintf('A class metadata factory must be provided in the constructor when setting "%s" to false.', static::ALLOW_EXTRA_ATTRIBUTES)); Chris@16: } Chris@16: Chris@14: return false; Chris@14: } Chris@14: Chris@14: $groups = false; Chris@14: if (isset($context[static::GROUPS]) && \is_array($context[static::GROUPS])) { Chris@14: $groups = $context[static::GROUPS]; Chris@14: } elseif (!isset($context[static::ALLOW_EXTRA_ATTRIBUTES]) || $context[static::ALLOW_EXTRA_ATTRIBUTES]) { Chris@0: return false; Chris@0: } Chris@0: Chris@17: $allowedAttributes = []; Chris@0: foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) { Chris@0: $name = $attributeMetadata->getName(); Chris@0: Chris@0: if ( Chris@14: (false === $groups || array_intersect($attributeMetadata->getGroups(), $groups)) && Chris@0: $this->isAllowedAttribute($classOrObject, $name, null, $context) Chris@0: ) { Chris@0: $allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata; Chris@0: } Chris@0: } Chris@0: Chris@0: return $allowedAttributes; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Is this attribute allowed? Chris@0: * Chris@0: * @param object|string $classOrObject Chris@0: * @param string $attribute Chris@0: * @param string|null $format Chris@0: * @param array $context Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@17: protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = []) Chris@0: { Chris@17: if (\in_array($attribute, $this->ignoredAttributes)) { Chris@14: return false; Chris@14: } Chris@14: Chris@14: if (isset($context[self::ATTRIBUTES][$attribute])) { Chris@14: // Nested attributes Chris@14: return true; Chris@14: } Chris@14: Chris@17: if (isset($context[self::ATTRIBUTES]) && \is_array($context[self::ATTRIBUTES])) { Chris@17: return \in_array($attribute, $context[self::ATTRIBUTES], true); Chris@14: } Chris@14: Chris@14: return true; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Normalizes the given data to an array. It's particularly useful during Chris@0: * the denormalization process. Chris@0: * Chris@0: * @param object|array $data Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: protected function prepareForDenormalization($data) Chris@0: { Chris@0: return (array) $data; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the method to use to construct an object. This method must be either Chris@0: * the object constructor or static. Chris@0: * Chris@0: * @param array $data Chris@0: * @param string $class Chris@0: * @param array $context Chris@0: * @param \ReflectionClass $reflectionClass Chris@0: * @param array|bool $allowedAttributes Chris@0: * Chris@0: * @return \ReflectionMethod|null Chris@0: */ Chris@0: protected function getConstructor(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes) Chris@0: { Chris@0: return $reflectionClass->getConstructor(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Instantiates an object using constructor parameters when needed. Chris@0: * Chris@0: * This method also allows to denormalize data into an existing object if Chris@0: * it is present in the context with the object_to_populate. This object Chris@0: * is removed from the context before being returned to avoid side effects Chris@0: * when recursively normalizing an object graph. Chris@0: * Chris@0: * @param array $data Chris@0: * @param string $class Chris@0: * @param array $context Chris@0: * @param \ReflectionClass $reflectionClass Chris@0: * @param array|bool $allowedAttributes Chris@0: * @param string|null $format Chris@0: * Chris@0: * @return object Chris@0: * Chris@0: * @throws RuntimeException Chris@0: */ Chris@14: protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes/*, string $format = null*/) Chris@0: { Chris@14: if (\func_num_args() >= 6) { Chris@14: $format = \func_get_arg(5); Chris@0: } else { Chris@14: if (__CLASS__ !== \get_class($this)) { Chris@0: $r = new \ReflectionMethod($this, __FUNCTION__); Chris@0: if (__CLASS__ !== $r->getDeclaringClass()->getName()) { Chris@17: @trigger_error(sprintf('Method %s::%s() will have a 6th `string $format = null` argument in version 4.0. Not defining it is deprecated since Symfony 3.2.', \get_class($this), __FUNCTION__), E_USER_DEPRECATED); Chris@0: } Chris@0: } Chris@0: Chris@0: $format = null; Chris@0: } Chris@0: Chris@14: if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) { Chris@0: unset($context[static::OBJECT_TO_POPULATE]); Chris@0: Chris@0: return $object; Chris@0: } Chris@18: // clean up even if no match Chris@18: unset($context[static::OBJECT_TO_POPULATE]); Chris@0: Chris@0: $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes); Chris@0: if ($constructor) { Chris@18: if (true !== $constructor->isPublic()) { Chris@18: return $reflectionClass->newInstanceWithoutConstructor(); Chris@18: } Chris@18: Chris@0: $constructorParameters = $constructor->getParameters(); Chris@0: Chris@17: $params = []; Chris@0: foreach ($constructorParameters as $constructorParameter) { Chris@0: $paramName = $constructorParameter->name; Chris@0: $key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName; Chris@0: Chris@14: $allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes); Chris@14: $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context); Chris@0: if (method_exists($constructorParameter, 'isVariadic') && $constructorParameter->isVariadic()) { Chris@18: if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) { Chris@14: if (!\is_array($data[$paramName])) { Chris@0: 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)); Chris@0: } Chris@0: Chris@0: $params = array_merge($params, $data[$paramName]); Chris@0: } Chris@18: } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) { Chris@0: $parameterData = $data[$key]; Chris@16: if (null === $parameterData && $constructorParameter->allowsNull()) { Chris@16: $params[] = null; Chris@16: // Don't run set for a parameter passed to the constructor Chris@16: unset($data[$key]); Chris@16: continue; Chris@16: } Chris@0: Chris@0: // Don't run set for a parameter passed to the constructor Chris@17: $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format); Chris@0: unset($data[$key]); Chris@0: } elseif ($constructorParameter->isDefaultValueAvailable()) { Chris@0: $params[] = $constructorParameter->getDefaultValue(); Chris@0: } else { Chris@17: throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name)); Chris@0: } Chris@0: } Chris@0: Chris@0: if ($constructor->isConstructor()) { Chris@0: return $reflectionClass->newInstanceArgs($params); Chris@0: } else { Chris@0: return $constructor->invokeArgs(null, $params); Chris@0: } Chris@0: } Chris@0: Chris@0: return new $class(); Chris@0: } Chris@14: Chris@14: /** Chris@17: * @internal Chris@17: */ Chris@17: protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null) Chris@17: { Chris@17: try { Chris@17: if (null !== $parameter->getClass()) { Chris@17: if (!$this->serializer instanceof DenormalizerInterface) { Chris@17: throw new LogicException(sprintf('Cannot create an instance of %s from serialized data because the serializer inject in "%s" is not a denormalizer', $parameter->getClass(), static::class)); Chris@17: } Chris@17: $parameterClass = $parameter->getClass()->getName(); Chris@17: Chris@18: return $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format)); Chris@17: } Chris@17: Chris@17: return $parameterData; Chris@17: } catch (\ReflectionException $e) { Chris@17: throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e); Chris@17: } Chris@17: } Chris@17: Chris@17: /** Chris@18: * @param array $parentContext Chris@18: * @param string $attribute Attribute name Chris@18: * @param string|null $format Chris@14: * Chris@14: * @return array Chris@14: * Chris@14: * @internal Chris@14: */ Chris@18: protected function createChildContext(array $parentContext, $attribute/*, string $format = null */) Chris@14: { Chris@14: if (isset($parentContext[self::ATTRIBUTES][$attribute])) { Chris@14: $parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute]; Chris@14: } else { Chris@14: unset($parentContext[self::ATTRIBUTES]); Chris@14: } Chris@14: Chris@14: return $parentContext; Chris@14: } Chris@0: }