Mercurial > hg > isophonics-drupal-site
diff vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 7a779792577d |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php Wed Nov 29 16:09:58 2017 +0000 @@ -0,0 +1,355 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Normalizer; + +use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Exception\CircularReferenceException; +use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +/** + * Base class for a normalizer dealing with objects. + * + * @author Kévin Dunglas <dunglas@gmail.com> + */ +abstract class AbstractObjectNormalizer extends AbstractNormalizer +{ + const ENABLE_MAX_DEPTH = 'enable_max_depth'; + const DEPTH_KEY_PATTERN = 'depth_%s::%s'; + + private $propertyTypeExtractor; + private $attributesCache = array(); + + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null) + { + parent::__construct($classMetadataFactory, $nameConverter); + + $this->propertyTypeExtractor = $propertyTypeExtractor; + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null) + { + return is_object($data) && !$data instanceof \Traversable; + } + + /** + * {@inheritdoc} + * + * @throws CircularReferenceException + */ + public function normalize($object, $format = null, array $context = array()) + { + if (!isset($context['cache_key'])) { + $context['cache_key'] = $this->getCacheKey($format, $context); + } + + if ($this->isCircularReference($object, $context)) { + return $this->handleCircularReference($object); + } + + $data = array(); + $stack = array(); + $attributes = $this->getAttributes($object, $format, $context); + $class = get_class($object); + $attributesMetadata = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null; + + foreach ($attributes as $attribute) { + if (null !== $attributesMetadata && $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) { + continue; + } + + $attributeValue = $this->getAttributeValue($object, $attribute, $format, $context); + + if (isset($this->callbacks[$attribute])) { + $attributeValue = call_user_func($this->callbacks[$attribute], $attributeValue); + } + + if (null !== $attributeValue && !is_scalar($attributeValue)) { + $stack[$attribute] = $attributeValue; + } + + $data = $this->updateData($data, $attribute, $attributeValue); + } + + foreach ($stack as $attribute => $attributeValue) { + if (!$this->serializer instanceof NormalizerInterface) { + throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer', $attribute)); + } + + $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $context)); + } + + return $data; + } + + /** + * Gets and caches attributes for the given object, format and context. + * + * @param object $object + * @param string|null $format + * @param array $context + * + * @return string[] + */ + protected function getAttributes($object, $format = null, array $context) + { + $class = get_class($object); + $key = $class.'-'.$context['cache_key']; + + if (isset($this->attributesCache[$key])) { + return $this->attributesCache[$key]; + } + + $allowedAttributes = $this->getAllowedAttributes($object, $context, true); + + if (false !== $allowedAttributes) { + if ($context['cache_key']) { + $this->attributesCache[$key] = $allowedAttributes; + } + + return $allowedAttributes; + } + + if (isset($this->attributesCache[$class])) { + return $this->attributesCache[$class]; + } + + return $this->attributesCache[$class] = $this->extractAttributes($object, $format, $context); + } + + /** + * Extracts attributes to normalize from the class of the given object, format and context. + * + * @param object $object + * @param string|null $format + * @param array $context + * + * @return string[] + */ + abstract protected function extractAttributes($object, $format = null, array $context = array()); + + /** + * Gets the attribute value. + * + * @param object $object + * @param string $attribute + * @param string|null $format + * @param array $context + * + * @return mixed + */ + abstract protected function getAttributeValue($object, $attribute, $format = null, array $context = array()); + + /** + * {@inheritdoc} + */ + public function supportsDenormalization($data, $type, $format = null) + { + return class_exists($type); + } + + /** + * {@inheritdoc} + */ + public function denormalize($data, $class, $format = null, array $context = array()) + { + if (!isset($context['cache_key'])) { + $context['cache_key'] = $this->getCacheKey($format, $context); + } + $allowedAttributes = $this->getAllowedAttributes($class, $context, true); + $normalizedData = $this->prepareForDenormalization($data); + + $reflectionClass = new \ReflectionClass($class); + $object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes, $format); + + foreach ($normalizedData as $attribute => $value) { + if ($this->nameConverter) { + $attribute = $this->nameConverter->denormalize($attribute); + } + + if (($allowedAttributes !== false && !in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($class, $attribute, $format, $context)) { + continue; + } + + $value = $this->validateAndDenormalize($class, $attribute, $value, $format, $context); + try { + $this->setAttributeValue($object, $attribute, $value, $format, $context); + } catch (InvalidArgumentException $e) { + throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); + } + } + + return $object; + } + + /** + * Sets attribute value. + * + * @param object $object + * @param string $attribute + * @param mixed $value + * @param string|null $format + * @param array $context + */ + abstract protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = array()); + + /** + * Validates the submitted data and denormalizes it. + * + * @param string $currentClass + * @param string $attribute + * @param mixed $data + * @param string|null $format + * @param array $context + * + * @return mixed + * + * @throws UnexpectedValueException + * @throws LogicException + */ + private function validateAndDenormalize($currentClass, $attribute, $data, $format, array $context) + { + if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) { + return $data; + } + + $expectedTypes = array(); + foreach ($types as $type) { + if (null === $data && $type->isNullable()) { + return; + } + + if ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) { + $builtinType = Type::BUILTIN_TYPE_OBJECT; + $class = $collectionValueType->getClassName().'[]'; + + if (null !== $collectionKeyType = $type->getCollectionKeyType()) { + $context['key_type'] = $collectionKeyType; + } + } else { + $builtinType = $type->getBuiltinType(); + $class = $type->getClassName(); + } + + $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true; + + if (Type::BUILTIN_TYPE_OBJECT === $builtinType) { + if (!$this->serializer instanceof DenormalizerInterface) { + throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer', $attribute, $class)); + } + + if ($this->serializer->supportsDenormalization($data, $class, $format)) { + return $this->serializer->denormalize($data, $class, $format, $context); + } + } + + // JSON only has a Number type corresponding to both int and float PHP types. + // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert + // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible). + // PHP's json_decode automatically converts Numbers without a decimal part to integers. + // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when + // a float is expected. + if (Type::BUILTIN_TYPE_FLOAT === $builtinType && is_int($data) && false !== strpos($format, JsonEncoder::FORMAT)) { + return (float) $data; + } + + if (call_user_func('is_'.$builtinType, $data)) { + return $data; + } + } + + throw new UnexpectedValueException(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))); + } + + /** + * Sets an attribute and apply the name converter if necessary. + * + * @param array $data + * @param string $attribute + * @param mixed $attributeValue + * + * @return array + */ + private function updateData(array $data, $attribute, $attributeValue) + { + if ($this->nameConverter) { + $attribute = $this->nameConverter->normalize($attribute); + } + + $data[$attribute] = $attributeValue; + + return $data; + } + + /** + * Is the max depth reached for the given attribute? + * + * @param AttributeMetadataInterface[] $attributesMetadata + * @param string $class + * @param string $attribute + * @param array $context + * + * @return bool + */ + private function isMaxDepthReached(array $attributesMetadata, $class, $attribute, array &$context) + { + if ( + !isset($context[static::ENABLE_MAX_DEPTH]) || + !isset($attributesMetadata[$attribute]) || + null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth() + ) { + return false; + } + + $key = sprintf(static::DEPTH_KEY_PATTERN, $class, $attribute); + if (!isset($context[$key])) { + $context[$key] = 1; + + return false; + } + + if ($context[$key] === $maxDepth) { + return true; + } + + ++$context[$key]; + + return false; + } + + /** + * Gets the cache key to use. + * + * @param string|null $format + * @param array $context + * + * @return bool|string + */ + private function getCacheKey($format, array $context) + { + try { + return md5($format.serialize($context)); + } catch (\Exception $exception) { + // The context cannot be serialized, skip the cache + return false; + } + } +}