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\PropertyAccess\Exception\InvalidArgumentException; Chris@17: use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; Chris@17: use Symfony\Component\PropertyInfo\Type; Chris@0: use Symfony\Component\Serializer\Encoder\JsonEncoder; Chris@14: use Symfony\Component\Serializer\Exception\ExtraAttributesException; Chris@0: use Symfony\Component\Serializer\Exception\LogicException; Chris@14: use Symfony\Component\Serializer\Exception\NotNormalizableValueException; Chris@0: 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: Chris@0: /** Chris@0: * Base class for a normalizer dealing with objects. Chris@0: * Chris@0: * @author Kévin Dunglas Chris@0: */ Chris@0: abstract class AbstractObjectNormalizer extends AbstractNormalizer Chris@0: { Chris@0: const ENABLE_MAX_DEPTH = 'enable_max_depth'; Chris@0: const DEPTH_KEY_PATTERN = 'depth_%s::%s'; Chris@14: const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement'; Chris@0: Chris@0: private $propertyTypeExtractor; Chris@17: private $attributesCache = []; Chris@17: private $cache = []; Chris@0: Chris@0: public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null) Chris@0: { Chris@0: parent::__construct($classMetadataFactory, $nameConverter); Chris@0: Chris@0: $this->propertyTypeExtractor = $propertyTypeExtractor; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function supportsNormalization($data, $format = null) Chris@0: { Chris@14: return \is_object($data) && !$data instanceof \Traversable; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@17: public function normalize($object, $format = null, array $context = []) Chris@0: { Chris@0: if (!isset($context['cache_key'])) { Chris@0: $context['cache_key'] = $this->getCacheKey($format, $context); Chris@0: } Chris@0: Chris@0: if ($this->isCircularReference($object, $context)) { Chris@0: return $this->handleCircularReference($object); Chris@0: } Chris@0: Chris@17: $data = []; Chris@17: $stack = []; Chris@0: $attributes = $this->getAttributes($object, $format, $context); Chris@17: $class = \get_class($object); Chris@0: $attributesMetadata = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null; Chris@0: Chris@0: foreach ($attributes as $attribute) { Chris@0: if (null !== $attributesMetadata && $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: $attributeValue = $this->getAttributeValue($object, $attribute, $format, $context); Chris@0: Chris@0: if (isset($this->callbacks[$attribute])) { Chris@17: $attributeValue = \call_user_func($this->callbacks[$attribute], $attributeValue); Chris@0: } Chris@0: Chris@0: if (null !== $attributeValue && !is_scalar($attributeValue)) { Chris@0: $stack[$attribute] = $attributeValue; Chris@0: } Chris@0: Chris@0: $data = $this->updateData($data, $attribute, $attributeValue); Chris@0: } Chris@0: Chris@0: foreach ($stack as $attribute => $attributeValue) { Chris@0: if (!$this->serializer instanceof NormalizerInterface) { Chris@0: throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer', $attribute)); Chris@0: } Chris@0: Chris@18: $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format))); Chris@0: } Chris@0: Chris@0: return $data; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets and caches attributes for the given object, format and context. Chris@0: * Chris@0: * @param object $object Chris@0: * @param string|null $format Chris@0: * @param array $context Chris@0: * Chris@0: * @return string[] Chris@0: */ Chris@0: protected function getAttributes($object, $format = null, array $context) Chris@0: { Chris@17: $class = \get_class($object); Chris@0: $key = $class.'-'.$context['cache_key']; Chris@0: Chris@0: if (isset($this->attributesCache[$key])) { Chris@0: return $this->attributesCache[$key]; Chris@0: } Chris@0: Chris@0: $allowedAttributes = $this->getAllowedAttributes($object, $context, true); Chris@0: Chris@0: if (false !== $allowedAttributes) { Chris@0: if ($context['cache_key']) { Chris@0: $this->attributesCache[$key] = $allowedAttributes; Chris@0: } Chris@0: Chris@0: return $allowedAttributes; Chris@0: } Chris@0: Chris@18: $attributes = $this->extractAttributes($object, $format, $context); Chris@18: Chris@18: if ($context['cache_key']) { Chris@18: $this->attributesCache[$key] = $attributes; Chris@14: } Chris@14: Chris@18: return $attributes; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Extracts attributes to normalize from the class of the given object, format and context. Chris@0: * Chris@0: * @param object $object Chris@0: * @param string|null $format Chris@0: * @param array $context Chris@0: * Chris@0: * @return string[] Chris@0: */ Chris@17: abstract protected function extractAttributes($object, $format = null, array $context = []); Chris@0: Chris@0: /** Chris@0: * Gets the attribute value. Chris@0: * Chris@0: * @param object $object Chris@0: * @param string $attribute Chris@0: * @param string|null $format Chris@0: * @param array $context Chris@0: * Chris@0: * @return mixed Chris@0: */ Chris@17: abstract protected function getAttributeValue($object, $attribute, $format = null, array $context = []); Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function supportsDenormalization($data, $type, $format = null) Chris@0: { Chris@14: return isset($this->cache[$type]) ? $this->cache[$type] : $this->cache[$type] = class_exists($type); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@17: public function denormalize($data, $class, $format = null, array $context = []) Chris@0: { Chris@0: if (!isset($context['cache_key'])) { Chris@0: $context['cache_key'] = $this->getCacheKey($format, $context); Chris@0: } Chris@14: Chris@0: $allowedAttributes = $this->getAllowedAttributes($class, $context, true); Chris@0: $normalizedData = $this->prepareForDenormalization($data); Chris@17: $extraAttributes = []; Chris@0: Chris@0: $reflectionClass = new \ReflectionClass($class); Chris@0: $object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes, $format); Chris@0: Chris@0: foreach ($normalizedData as $attribute => $value) { Chris@0: if ($this->nameConverter) { Chris@0: $attribute = $this->nameConverter->denormalize($attribute); Chris@0: } Chris@0: Chris@17: if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($class, $attribute, $format, $context)) { Chris@14: if (isset($context[self::ALLOW_EXTRA_ATTRIBUTES]) && !$context[self::ALLOW_EXTRA_ATTRIBUTES]) { Chris@14: $extraAttributes[] = $attribute; Chris@14: } Chris@14: Chris@0: continue; Chris@0: } Chris@0: Chris@0: $value = $this->validateAndDenormalize($class, $attribute, $value, $format, $context); Chris@0: try { Chris@0: $this->setAttributeValue($object, $attribute, $value, $format, $context); Chris@0: } catch (InvalidArgumentException $e) { Chris@14: throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e); Chris@0: } Chris@0: } Chris@0: Chris@14: if (!empty($extraAttributes)) { Chris@14: throw new ExtraAttributesException($extraAttributes); Chris@14: } Chris@14: Chris@0: return $object; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets attribute value. Chris@0: * Chris@0: * @param object $object Chris@0: * @param string $attribute Chris@0: * @param mixed $value Chris@0: * @param string|null $format Chris@0: * @param array $context Chris@0: */ Chris@17: abstract protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []); Chris@0: Chris@0: /** Chris@0: * Validates the submitted data and denormalizes it. Chris@0: * Chris@0: * @param string $currentClass Chris@0: * @param string $attribute Chris@0: * @param mixed $data Chris@0: * @param string|null $format Chris@0: * @param array $context Chris@0: * Chris@0: * @return mixed Chris@0: * Chris@14: * @throws NotNormalizableValueException Chris@0: * @throws LogicException Chris@0: */ Chris@0: private function validateAndDenormalize($currentClass, $attribute, $data, $format, array $context) Chris@0: { Chris@0: if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) { Chris@0: return $data; Chris@0: } Chris@0: Chris@17: $expectedTypes = []; Chris@0: foreach ($types as $type) { Chris@0: if (null === $data && $type->isNullable()) { Chris@0: return; Chris@0: } Chris@0: Chris@0: if ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) { Chris@0: $builtinType = Type::BUILTIN_TYPE_OBJECT; Chris@0: $class = $collectionValueType->getClassName().'[]'; Chris@0: Chris@16: // Fix a collection that contains the only one element Chris@16: // This is special to xml format only Chris@17: if ('xml' === $format && !\is_int(key($data))) { Chris@17: $data = [$data]; Chris@16: } Chris@16: Chris@0: if (null !== $collectionKeyType = $type->getCollectionKeyType()) { Chris@0: $context['key_type'] = $collectionKeyType; Chris@0: } Chris@0: } else { Chris@0: $builtinType = $type->getBuiltinType(); Chris@0: $class = $type->getClassName(); Chris@0: } Chris@0: Chris@0: $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true; Chris@0: Chris@0: if (Type::BUILTIN_TYPE_OBJECT === $builtinType) { Chris@0: if (!$this->serializer instanceof DenormalizerInterface) { Chris@0: throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer', $attribute, $class)); Chris@0: } Chris@0: Chris@18: $childContext = $this->createChildContext($context, $attribute, $format); Chris@14: if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) { Chris@14: return $this->serializer->denormalize($data, $class, $format, $childContext); Chris@0: } Chris@0: } Chris@0: Chris@0: // JSON only has a Number type corresponding to both int and float PHP types. Chris@0: // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert Chris@0: // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible). Chris@0: // PHP's json_decode automatically converts Numbers without a decimal part to integers. Chris@0: // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when Chris@0: // a float is expected. Chris@17: if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && false !== strpos($format, JsonEncoder::FORMAT)) { Chris@0: return (float) $data; Chris@0: } Chris@0: Chris@17: if (\call_user_func('is_'.$builtinType, $data)) { Chris@0: return $data; Chris@0: } Chris@0: } Chris@0: Chris@14: if (!empty($context[self::DISABLE_TYPE_ENFORCEMENT])) { Chris@14: return $data; Chris@14: } Chris@14: Chris@17: throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), \gettype($data))); Chris@17: } Chris@17: Chris@17: /** Chris@17: * @internal Chris@17: */ Chris@17: protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null) Chris@17: { Chris@17: if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($class->getName(), $parameterName)) { Chris@17: return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format); Chris@17: } Chris@17: Chris@17: return $this->validateAndDenormalize($class->getName(), $parameterName, $parameterData, $format, $context); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets an attribute and apply the name converter if necessary. Chris@0: * Chris@0: * @param string $attribute Chris@0: * @param mixed $attributeValue Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function updateData(array $data, $attribute, $attributeValue) Chris@0: { Chris@0: if ($this->nameConverter) { Chris@0: $attribute = $this->nameConverter->normalize($attribute); Chris@0: } Chris@0: Chris@0: $data[$attribute] = $attributeValue; Chris@0: Chris@0: return $data; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Is the max depth reached for the given attribute? Chris@0: * Chris@0: * @param AttributeMetadataInterface[] $attributesMetadata Chris@0: * @param string $class Chris@0: * @param string $attribute Chris@0: * @param array $context Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: private function isMaxDepthReached(array $attributesMetadata, $class, $attribute, array &$context) Chris@0: { Chris@0: if ( Chris@0: !isset($context[static::ENABLE_MAX_DEPTH]) || Chris@16: !$context[static::ENABLE_MAX_DEPTH] || Chris@0: !isset($attributesMetadata[$attribute]) || Chris@0: null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth() Chris@0: ) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: $key = sprintf(static::DEPTH_KEY_PATTERN, $class, $attribute); Chris@0: if (!isset($context[$key])) { Chris@0: $context[$key] = 1; Chris@0: Chris@0: return false; Chris@0: } Chris@0: Chris@0: if ($context[$key] === $maxDepth) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: ++$context[$key]; Chris@0: Chris@0: return false; Chris@0: } Chris@0: Chris@0: /** Chris@18: * Overwritten to update the cache key for the child. Chris@18: * Chris@18: * We must not mix up the attribute cache between parent and children. Chris@18: * Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: protected function createChildContext(array $parentContext, $attribute/*, string $format = null */) Chris@18: { Chris@18: if (\func_num_args() >= 3) { Chris@18: $format = \func_get_arg(2); Chris@18: } else { Chris@18: // will be deprecated in version 4 Chris@18: $format = null; Chris@18: } Chris@18: Chris@18: $context = parent::createChildContext($parentContext, $attribute, $format); Chris@18: // format is already included in the cache_key of the parent. Chris@18: $context['cache_key'] = $this->getCacheKey($format, $context); Chris@18: Chris@18: return $context; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Builds the cache key for the attributes cache. Chris@18: * Chris@18: * The key must be different for every option in the context that could change which attributes should be handled. Chris@0: * Chris@0: * @param string|null $format Chris@0: * @param array $context Chris@0: * Chris@0: * @return bool|string Chris@0: */ Chris@0: private function getCacheKey($format, array $context) Chris@0: { Chris@18: unset($context['cache_key']); // avoid artificially different keys Chris@0: try { Chris@18: return md5($format.serialize([ Chris@18: 'context' => $context, Chris@18: 'ignored' => $this->ignoredAttributes, Chris@18: 'camelized' => $this->camelizedAttributes, Chris@18: ])); Chris@0: } catch (\Exception $exception) { Chris@0: // The context cannot be serialized, skip the cache Chris@0: return false; Chris@0: } Chris@0: } Chris@0: }