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/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 c24444686232d..504a5ccdad04d 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -36,6 +36,40 @@ 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); + $object = $this->instantiateObject($normalizedData, $class, $context, $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()) { + $object->$setter($value); + } + } + } + + return $object; + } + /** * {@inheritdoc} */ 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 6034dab4b79cd..959e2dbf57dd7 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -390,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() { 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 572561639db5b..29d40aab0293a 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 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.",