123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- <?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);
- 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;
- }
- }
- }
|