diff --git a/Normalizer/AbstractNormalizer.php b/Normalizer/AbstractNormalizer.php index 5eedac725..0e9973c3a 100644 --- a/Normalizer/AbstractNormalizer.php +++ b/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/Normalizer/GetSetMethodNormalizer.php b/Normalizer/GetSetMethodNormalizer.php index c24444686..78eafa29a 100644 --- a/Normalizer/GetSetMethodNormalizer.php +++ b/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/Tests/Normalizer/GetSetMethodNormalizerTest.php b/Tests/Normalizer/GetSetMethodNormalizerTest.php index 6034dab4b..a0c4250f0 100644 --- a/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/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/composer.json b/composer.json index 572561639..b1a6aa433 100644 --- a/composer.json +++ b/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.",