Skip to content

Commit

Permalink
Recursive denormalize using PropertyInfo
Browse files Browse the repository at this point in the history
- Refactored PR 14844 "Denormalize with typehinting"
- Now using PropertyInfo to extract type information
- Updated tests
- Updated composer.json
  • Loading branch information
Mihai Stancu authored and dunglas committed Apr 18, 2016
1 parent ce9520e commit 63a077c
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 1 deletion.
31 changes: 30 additions & 1 deletion Normalizer/AbstractNormalizer.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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();
Expand All @@ -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]);
Expand Down
60 changes: 60 additions & 0 deletions Normalizer/GetSetMethodNormalizer.php
Expand Up @@ -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}
*/
Expand Down
73 changes: 73 additions & 0 deletions Tests/Normalizer/GetSetMethodNormalizerTest.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions composer.json
Expand Up @@ -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"
},
Expand All @@ -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.",
Expand Down

0 comments on commit 63a077c

Please sign in to comment.