From 6b464b01aadb7ada6569f840b28be1d0adc9d6c5 Mon Sep 17 00:00:00 2001 From: Mihai Stancu Date: Tue, 6 Oct 2015 14:13:03 +0300 Subject: [PATCH 1/2] Recursive denormalize using PropertyInfo - Refactored PR 14844 "Denormalize with typehinting" - Now using PropertyInfo to extract type information - Updated tests - Updated composer.json --- .../Normalizer/AbstractNormalizer.php | 31 +++++++- .../Normalizer/GetSetMethodNormalizer.php | 60 +++++++++++++++ .../Normalizer/GetSetMethodNormalizerTest.php | 73 +++++++++++++++++++ .../Component/Serializer/composer.json | 2 + 4 files changed, 165 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 5eedac7253b8c..0e9973c3a89cc 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Serializer\Normalizer; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\RuntimeException; @@ -68,16 +69,22 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn */ protected $camelizedAttributes = array(); + /** + * @var PropertyInfoExtractorInterface + */ + protected $propertyInfoExtractor; + /** * Sets the {@link ClassMetadataFactoryInterface} to use. * * @param ClassMetadataFactoryInterface|null $classMetadataFactory * @param NameConverterInterface|null $nameConverter */ - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null) + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyInfoExtractorInterface $propertyInfoExtractor = null) { $this->classMetadataFactory = $classMetadataFactory; $this->nameConverter = $nameConverter; + $this->propertyInfoExtractor = $propertyInfoExtractor; } /** @@ -285,6 +292,11 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref return $object; } + $format = null; + if (isset($context['format'])) { + $format = $context['format']; + } + $constructor = $reflectionClass->getConstructor(); if ($constructor) { $constructorParameters = $constructor->getParameters(); @@ -305,6 +317,23 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref $params = array_merge($params, $data[$paramName]); } } elseif ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) { + if ($this->propertyInfoExtractor) { + $types = $this->propertyInfoExtractor->getTypes($class, $key); + + foreach ($types as $type) { + if ($type && $type->getClassName() && (!empty($data[$key]) || !$type->isNullable())) { + if (!$this->serializer instanceof DenormalizerInterface) { + throw new RuntimeException(sprintf('Cannot denormalize attribute "%s" because injected serializer is not a denormalizer', $key)); + } + + $value = $data[$paramName]; + $data[$paramName] = $this->serializer->denormalize($value, $type->getClassName(), $format, $context); + + break; + } + } + } + $params[] = $data[$key]; // don't run set for a parameter passed to the constructor unset($data[$key]); diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index c24444686232d..78eafa29a6893 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -36,6 +36,66 @@ class GetSetMethodNormalizer extends AbstractObjectNormalizer { private static $setterAccessibleCache = array(); + /** + * {@inheritdoc} + * + * @throws RuntimeException + */ + public function denormalize($data, $class, $format = null, array $context = array()) + { + $allowedAttributes = $this->getAllowedAttributes($class, $context, true); + $normalizedData = $this->prepareForDenormalization($data); + + $reflectionClass = new \ReflectionClass($class); + $subcontext = array_merge($context, array('format' => $format)); + $object = $this->instantiateObject($normalizedData, $class, $subcontext, $reflectionClass, $allowedAttributes); + + $classMethods = get_class_methods($object); + foreach ($normalizedData as $attribute => $value) { + if ($this->nameConverter) { + $attribute = $this->nameConverter->denormalize($attribute); + } + + $allowed = $allowedAttributes === false || in_array($attribute, $allowedAttributes); + $ignored = in_array($attribute, $this->ignoredAttributes); + + if ($allowed && !$ignored) { + $setter = 'set'.ucfirst($attribute); + if (in_array($setter, $classMethods) && !$reflectionClass->getMethod($setter)->isStatic()) { + if ($this->propertyInfoExtractor) { + $types = (array) $this->propertyInfoExtractor->getTypes($class, $attribute); + + foreach ($types as $type) { + if ($type && (!empty($value) || !$type->isNullable())) { + if (!$this->serializer instanceof DenormalizerInterface) { + throw new RuntimeException( + sprintf( + 'Cannot denormalize attribute "%s" because injected serializer is not a denormalizer', + $attribute + ) + ); + } + + $value = $this->serializer->denormalize( + $value, + $type->getClassName(), + $format, + $context + ); + + break; + } + } + } + + $object->$setter($value); + } + } + } + + return $object; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index 6034dab4b79cd..a0c4250f02033 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Serializer\Tests\Normalizer; use Doctrine\Common\Annotations\AnnotationReader; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; use Symfony\Component\Serializer\Serializer; @@ -490,6 +492,24 @@ public function testNoStaticGetSetSupport() $this->assertFalse($this->normalizer->supportsNormalization(new ObjectWithJustStaticSetterDummy())); } + public function testDenormalizeWithTypehint() + { + /* need a serializer that can recurse denormalization $normalizer */ + $normalizer = new GetSetMethodNormalizer(null, null, new PropertyInfoExtractor(array(), array(new ReflectionExtractor()))); + $serializer = new Serializer(array($normalizer)); + $normalizer->setSerializer($serializer); + + $obj = $normalizer->denormalize( + array( + 'object' => array('foo' => 'foo', 'bar' => 'bar'), + ), + __NAMESPACE__.'\GetTypehintedDummy', + 'any' + ); + $this->assertEquals('foo', $obj->getObject()->getFoo()); + $this->assertEquals('bar', $obj->getObject()->getBar()); + } + public function testPrivateSetter() { $obj = $this->normalizer->denormalize(array('foo' => 'foobar'), __NAMESPACE__.'\ObjectWithPrivateSetterDummy'); @@ -758,6 +778,59 @@ public function getBar_foo() } } +class GetTypehintedDummy +{ + protected $object; + + public function getObject() + { + return $this->object; + } + + public function setObject(GetTypehintDummy $object) + { + $this->object = $object; + } +} + +class GetTypehintDummy +{ + protected $foo; + protected $bar; + + /** + * @return mixed + */ + public function getFoo() + { + return $this->foo; + } + + /** + * @param mixed $foo + */ + public function setFoo($foo) + { + $this->foo = $foo; + } + + /** + * @return mixed + */ + public function getBar() + { + return $this->bar; + } + + /** + * @param mixed $bar + */ + public function setBar($bar) + { + $this->bar = $bar; + } +} + class ObjectConstructorArgsWithPrivateMutatorDummy { private $foo; diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 572561639db5b..b1a6aa433acc5 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -24,6 +24,7 @@ "symfony/property-access": "~2.8|~3.0", "symfony/http-foundation": "~2.8|~3.0", "symfony/cache": "~3.1", + "symfony/property-info": "~2.8|~3.0", "doctrine/annotations": "~1.0", "doctrine/cache": "~1.0" }, @@ -32,6 +33,7 @@ }, "suggest": { "psr/cache-implementation": "For using the metadata cache.", + "symfony/property-info": "To harden the component and deserialize relations.", "symfony/yaml": "For using the default YAML mapping loader.", "symfony/config": "For using the XML mapping loader.", "symfony/property-access": "For using the ObjectNormalizer.", From 5194482ed5ea0103c9cffb4ef1848649d244cd3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 3 Feb 2016 00:02:19 +0100 Subject: [PATCH 2/2] [Serializer] Integrate the PropertyInfo Component Recursive denormalization handling and hardening. --- src/Symfony/Component/Serializer/CHANGELOG.md | 1 + .../Normalizer/AbstractNormalizer.php | 31 +------- .../Normalizer/AbstractObjectNormalizer.php | 77 +++++++++++++++++-- .../Normalizer/GetSetMethodNormalizer.php | 30 +------- .../Normalizer/ObjectNormalizer.php | 5 +- .../Normalizer/GetSetMethodNormalizerTest.php | 75 +----------------- .../Tests/Normalizer/ObjectNormalizerTest.php | 60 ++++++++++++++- .../Normalizer/PropertyNormalizerTest.php | 2 +- .../Component/Serializer/composer.json | 2 +- 9 files changed, 139 insertions(+), 144 deletions(-) diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 8cbd61aa2281b..de24d9ed74b7e 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -16,6 +16,7 @@ CHANGELOG * added support for serializing objects that implement `DateTimeInterface` * added `AbstractObjectNormalizer` as a base class for normalizers that deal with objects + * added support to relation deserialization 2.7.0 ----- diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 0e9973c3a89cc..5eedac7253b8c 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\RuntimeException; @@ -69,22 +68,16 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn */ protected $camelizedAttributes = array(); - /** - * @var PropertyInfoExtractorInterface - */ - protected $propertyInfoExtractor; - /** * Sets the {@link ClassMetadataFactoryInterface} to use. * * @param ClassMetadataFactoryInterface|null $classMetadataFactory * @param NameConverterInterface|null $nameConverter */ - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyInfoExtractorInterface $propertyInfoExtractor = null) + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null) { $this->classMetadataFactory = $classMetadataFactory; $this->nameConverter = $nameConverter; - $this->propertyInfoExtractor = $propertyInfoExtractor; } /** @@ -292,11 +285,6 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref return $object; } - $format = null; - if (isset($context['format'])) { - $format = $context['format']; - } - $constructor = $reflectionClass->getConstructor(); if ($constructor) { $constructorParameters = $constructor->getParameters(); @@ -317,23 +305,6 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref $params = array_merge($params, $data[$paramName]); } } elseif ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) { - if ($this->propertyInfoExtractor) { - $types = $this->propertyInfoExtractor->getTypes($class, $key); - - foreach ($types as $type) { - if ($type && $type->getClassName() && (!empty($data[$key]) || !$type->isNullable())) { - if (!$this->serializer instanceof DenormalizerInterface) { - throw new RuntimeException(sprintf('Cannot denormalize attribute "%s" because injected serializer is not a denormalizer', $key)); - } - - $value = $data[$paramName]; - $data[$paramName] = $this->serializer->denormalize($value, $type->getClassName(), $format, $context); - - break; - } - } - } - $params[] = $data[$key]; // don't run set for a parameter passed to the constructor unset($data[$key]); diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 28755eca57315..d39023a4fd0f2 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -15,6 +15,10 @@ 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\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * Base class for a normalizer dealing with objects. @@ -26,8 +30,16 @@ 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} */ @@ -76,7 +88,7 @@ public function normalize($object, $format = null, array $context = array()) foreach ($stack as $attribute => $attributeValue) { if (!$this->serializer instanceof NormalizerInterface) { - throw new LogicException(sprintf('Cannot normalize attribute "%s" because injected serializer is not a normalizer', $attribute)); + 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)); @@ -173,12 +185,15 @@ public function denormalize($data, $class, $format = null, array $context = arra $allowed = $allowedAttributes === false || in_array($attribute, $allowedAttributes); $ignored = in_array($attribute, $this->ignoredAttributes); - if ($allowed && !$ignored) { - try { - $this->setAttributeValue($object, $attribute, $value, $format, $context); - } catch (InvalidArgumentException $e) { - throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); - } + if (!$allowed || $ignored) { + 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); } } @@ -210,6 +225,54 @@ protected function isAttributeToNormalize($object, $attributeName, &$context) return !in_array($attributeName, $this->ignoredAttributes) && !$this->isMaxDepthReached(get_class($object), $attributeName, $context); } + /** + * 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; + } + + $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); + } + } + + 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. * diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index 78eafa29a6893..504a5ccdad04d 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -47,8 +47,7 @@ public function denormalize($data, $class, $format = null, array $context = arra $normalizedData = $this->prepareForDenormalization($data); $reflectionClass = new \ReflectionClass($class); - $subcontext = array_merge($context, array('format' => $format)); - $object = $this->instantiateObject($normalizedData, $class, $subcontext, $reflectionClass, $allowedAttributes); + $object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes); $classMethods = get_class_methods($object); foreach ($normalizedData as $attribute => $value) { @@ -61,33 +60,8 @@ public function denormalize($data, $class, $format = null, array $context = arra if ($allowed && !$ignored) { $setter = 'set'.ucfirst($attribute); - if (in_array($setter, $classMethods) && !$reflectionClass->getMethod($setter)->isStatic()) { - if ($this->propertyInfoExtractor) { - $types = (array) $this->propertyInfoExtractor->getTypes($class, $attribute); - - foreach ($types as $type) { - if ($type && (!empty($value) || !$type->isNullable())) { - if (!$this->serializer instanceof DenormalizerInterface) { - throw new RuntimeException( - sprintf( - 'Cannot denormalize attribute "%s" because injected serializer is not a denormalizer', - $attribute - ) - ); - } - - $value = $this->serializer->denormalize( - $value, - $type->getClassName(), - $format, - $context - ); - - break; - } - } - } + if (in_array($setter, $classMethods) && !$reflectionClass->getMethod($setter)->isStatic()) { $object->$setter($value); } } diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index 1447091a4c603..fc774e6563686 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -14,6 +14,7 @@ use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -29,9 +30,9 @@ class ObjectNormalizer extends AbstractObjectNormalizer */ protected $propertyAccessor; - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null) + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null) { - parent::__construct($classMetadataFactory, $nameConverter); + parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor); $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index a0c4250f02033..959e2dbf57dd7 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -12,8 +12,6 @@ namespace Symfony\Component\Serializer\Tests\Normalizer; use Doctrine\Common\Annotations\AnnotationReader; -use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; -use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; use Symfony\Component\Serializer\Serializer; @@ -392,7 +390,7 @@ public function provideCallbacks() /** * @expectedException \Symfony\Component\Serializer\Exception\LogicException - * @expectedExceptionMessage Cannot normalize attribute "object" because injected serializer is not a normalizer + * @expectedExceptionMessage Cannot normalize attribute "object" because the injected serializer is not a normalizer */ public function testUnableToNormalizeObjectAttribute() { @@ -492,24 +490,6 @@ public function testNoStaticGetSetSupport() $this->assertFalse($this->normalizer->supportsNormalization(new ObjectWithJustStaticSetterDummy())); } - public function testDenormalizeWithTypehint() - { - /* need a serializer that can recurse denormalization $normalizer */ - $normalizer = new GetSetMethodNormalizer(null, null, new PropertyInfoExtractor(array(), array(new ReflectionExtractor()))); - $serializer = new Serializer(array($normalizer)); - $normalizer->setSerializer($serializer); - - $obj = $normalizer->denormalize( - array( - 'object' => array('foo' => 'foo', 'bar' => 'bar'), - ), - __NAMESPACE__.'\GetTypehintedDummy', - 'any' - ); - $this->assertEquals('foo', $obj->getObject()->getFoo()); - $this->assertEquals('bar', $obj->getObject()->getBar()); - } - public function testPrivateSetter() { $obj = $this->normalizer->denormalize(array('foo' => 'foobar'), __NAMESPACE__.'\ObjectWithPrivateSetterDummy'); @@ -778,59 +758,6 @@ public function getBar_foo() } } -class GetTypehintedDummy -{ - protected $object; - - public function getObject() - { - return $this->object; - } - - public function setObject(GetTypehintDummy $object) - { - $this->object = $object; - } -} - -class GetTypehintDummy -{ - protected $foo; - protected $bar; - - /** - * @return mixed - */ - public function getFoo() - { - return $this->foo; - } - - /** - * @param mixed $foo - */ - public function setFoo($foo) - { - $this->foo = $foo; - } - - /** - * @return mixed - */ - public function getBar() - { - return $this->bar; - } - - /** - * @param mixed $bar - */ - public function setBar($bar) - { - $this->bar = $bar; - } -} - class ObjectConstructorArgsWithPrivateMutatorDummy { private $foo; diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index 88ccdee0081f3..b04d09bf1fc08 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -12,7 +12,10 @@ namespace Symfony\Component\Serializer\Tests\Normalizer; use Doctrine\Common\Annotations\AnnotationReader; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerInterface; @@ -372,7 +375,7 @@ public function provideCallbacks() /** * @expectedException \Symfony\Component\Serializer\Exception\LogicException - * @expectedExceptionMessage Cannot normalize attribute "object" because injected serializer is not a normalizer + * @expectedExceptionMessage Cannot normalize attribute "object" because the injected serializer is not a normalizer */ public function testUnableToNormalizeObjectAttribute() { @@ -506,6 +509,29 @@ public function testThrowUnexpectedValueException() { $this->normalizer->denormalize(array('foo' => 'bar'), ObjectTypeHinted::class); } + + public function testDenomalizeRecursive() + { + $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); + $serializer = new Serializer(array(new DateTimeNormalizer(), $normalizer)); + + $obj = $serializer->denormalize(array('inner' => array('foo' => 'foo', 'bar' => 'bar'), 'date' => '1988/01/21'), ObjectOuter::class); + $this->assertEquals('foo', $obj->getInner()->foo); + $this->assertEquals('bar', $obj->getInner()->bar); + $this->assertEquals('1988-01-21', $obj->getDate()->format('Y-m-d')); + } + + /** + * @expectedException UnexpectedValueException + * @expectedExceptionMessage The type of the "date" attribute for class "Symfony\Component\Serializer\Tests\Normalizer\ObjectOuter" must be one of "DateTimeInterface" ("string" given). + */ + public function testRejectInvalidType() + { + $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); + $serializer = new Serializer(array($normalizer)); + + $serializer->denormalize(array('date' => 'foo'), ObjectOuter::class); + } } class ObjectDummy @@ -673,3 +699,35 @@ public function setFoo(array $f) { } } + +class ObjectOuter +{ + private $inner; + private $date; + + public function getInner() + { + return $this->inner; + } + + public function setInner(ObjectInner $inner) + { + $this->inner = $inner; + } + + public function setDate(\DateTimeInterface $date) + { + $this->date = $date; + } + + public function getDate() + { + return $this->date; + } +} + +class ObjectInner +{ + public $foo; + public $bar; +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index 35d726d2c3f05..9da80e36936ed 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -349,7 +349,7 @@ public function testDenormalizeShouldIgnoreStaticProperty() /** * @expectedException \Symfony\Component\Serializer\Exception\LogicException - * @expectedExceptionMessage Cannot normalize attribute "bar" because injected serializer is not a normalizer + * @expectedExceptionMessage Cannot normalize attribute "bar" because the injected serializer is not a normalizer */ public function testUnableToNormalizeObjectAttribute() { diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index b1a6aa433acc5..29d40aab0293a 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -33,7 +33,7 @@ }, "suggest": { "psr/cache-implementation": "For using the metadata cache.", - "symfony/property-info": "To harden the component and deserialize relations.", + "symfony/property-info": "To deserialize relations.", "symfony/yaml": "For using the default YAML mapping loader.", "symfony/config": "For using the XML mapping loader.", "symfony/property-access": "For using the ObjectNormalizer.",