[Serializer] Integrate the PropertyInfo Component
Recursive denormalization handling and hardening.
This commit is contained in:
parent
6b464b01aa
commit
5194482ed5
@ -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
|
||||
-----
|
||||
|
@ -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 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
|
||||
return $object;
|
||||
}
|
||||
|
||||
$format = null;
|
||||
if (isset($context['format'])) {
|
||||
$format = $context['format'];
|
||||
}
|
||||
|
||||
$constructor = $reflectionClass->getConstructor();
|
||||
if ($constructor) {
|
||||
$constructorParameters = $constructor->getParameters();
|
||||
@ -317,23 +305,6 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
|
||||
$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]);
|
||||
|
@ -15,6 +15,10 @@ use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
|
||||
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 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
|
||||
|
||||
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 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
|
||||
$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 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
|
||||
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.
|
||||
*
|
||||
|
@ -47,8 +47,7 @@ class GetSetMethodNormalizer extends AbstractObjectNormalizer
|
||||
$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 @@ class GetSetMethodNormalizer extends AbstractObjectNormalizer
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ namespace Symfony\Component\Serializer\Normalizer;
|
||||
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();
|
||||
}
|
||||
|
@ -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 @@ class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
/**
|
||||
* @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 @@ class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase
|
||||
$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 @@ class GetCamelizedDummy
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -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 @@ class ObjectNormalizerTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
/**
|
||||
* @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 @@ class ObjectNormalizerTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
$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 @@ class ObjectTypeHinted
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -349,7 +349,7 @@ class PropertyNormalizerTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
/**
|
||||
* @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()
|
||||
{
|
||||
|
@ -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.",
|
||||
|
Reference in New Issue
Block a user