From 1a3ec1bf2bebe7e5613c48f18c7f0ca5d313bc8c Mon Sep 17 00:00:00 2001 From: jewome62 Date: Sat, 6 Apr 2019 16:35:52 +0200 Subject: [PATCH 1/3] [Serializer] Instantiator - Add an interface and default implementation to instantiate objects --- .../Serializer/Instantiator/Instantiator.php | 151 ++++++++++++++++++ .../Instantiator/InstantiatorInterface.php | 23 +++ .../Normalizer/NewObjectNormalizer.php | 111 +++++++++++++ 3 files changed, 285 insertions(+) create mode 100644 src/Symfony/Component/Serializer/Instantiator/Instantiator.php create mode 100644 src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php create mode 100644 src/Symfony/Component/Serializer/Normalizer/NewObjectNormalizer.php diff --git a/src/Symfony/Component/Serializer/Instantiator/Instantiator.php b/src/Symfony/Component/Serializer/Instantiator/Instantiator.php new file mode 100644 index 0000000000000..32767c686c43e --- /dev/null +++ b/src/Symfony/Component/Serializer/Instantiator/Instantiator.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Instantiator; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; +use Symfony\Component\Serializer\Exception\RuntimeException; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait; +use Symfony\Component\Serializer\SerializerAwareInterface; +use Symfony\Component\Serializer\SerializerAwareTrait; + +/** + * @author Jérôme Desjardins + */ +class Instantiator implements InstantiatorInterface, SerializerAwareInterface +{ + use ObjectToPopulateTrait; + use SerializerAwareTrait; + + private $classDiscriminatorResolver; + private $propertyListExtractor; + private $nameConverter; + + public function __construct(ClassDiscriminatorResolverInterface $classDiscriminatorResolver, PropertyListExtractorInterface $propertyListExtractor, NameConverterInterface $nameConverter) + { + $this->classDiscriminatorResolver = $classDiscriminatorResolver; + $this->propertyListExtractor = $propertyListExtractor; + $this->nameConverter = $nameConverter; + } + + public function instantiate(string $class, $data, $format = null, array $context = []) + { + if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { + if (!isset($data[$mapping->getTypeProperty()])) { + throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping->getTypeProperty(), $class)); + } + + $type = $data[$mapping->getTypeProperty()]; + if (null === ($mappedClass = $mapping->getClassForType($type))) { + throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"', $type, $class)); + } + + $class = $mappedClass; + } + + $reflectionClass = new \ReflectionClass($class); + + if (null !== $object = $this->extractObjectToPopulate($class, $context, AbstractNormalizer::OBJECT_TO_POPULATE)) { + unset($context[AbstractNormalizer::OBJECT_TO_POPULATE]); + + return $object; + } + + $defaultConstructionArgumentKey = $context['defaultConstructionArgumentKey'] ?? AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS + $allowedAttributes = $this->propertyListExtractor->getProperties($class, $context); + $constructor = $reflectionClass->getConstructor(); + if ($constructor) { + $constructorParameters = $constructor->getParameters(); + + $params = []; + foreach ($constructorParameters as $constructorParameter) { + $paramName = $constructorParameter->name; + $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName; + + $allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes); + if ($constructorParameter->isVariadic()) { + if ($allowed && (isset($data[$key]) || \array_key_exists($key, $data))) { + if (!\is_array($data[$paramName])) { + throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because the variadic parameter %s can only accept an array.', $class, $constructorParameter->name)); + } + + $params = array_merge($params, $data[$paramName]); + } + } elseif ($allowed && (isset($data[$key]) || \array_key_exists($key, $data))) { + $parameterData = $data[$key]; + if (null === $parameterData && $constructorParameter->allowsNull()) { + $params[] = null; + // Don't run set for a parameter passed to the constructor + unset($data[$key]); + continue; + } + + // Don't run set for a parameter passed to the constructor + $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format); + unset($data[$key]); + } elseif (\array_key_exists($key, $context[$defaultConstructionArgumentKey][$class] ?? [])) { + $params[] = $context[$defaultConstructionArgumentKey][$class][$key]; + } elseif ($constructorParameter->isDefaultValueAvailable()) { + $params[] = $constructorParameter->getDefaultValue(); + } else { + throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name)); + } + } + + if ($constructor->isConstructor()) { + return $reflectionClass->newInstanceArgs($params); + } + + return $constructor->invokeArgs(null, $params); + } + + return new $class(); + } + + public function createChildContext(string $class, string $attribute, $parentData, array $parentContext = []) + { + if (isset($parentContext[AbstractNormalizer::ATTRIBUTES][$attribute])) { + $parentContext[AbstractNormalizer::ATTRIBUTES] = $parentContext[AbstractNormalizer::ATTRIBUTES][$attribute]; + } else { + unset($parentContext[AbstractNormalizer::ATTRIBUTES]); + } + + return $parentContext; + } + + private function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null) + { + try { + if (null !== $parameter->getClass()) { + if (!$this->serializer instanceof DenormalizerInterface) { + throw new LogicException(sprintf('Cannot create an instance of %s from serialized data because the serializer inject in "%s" is not a denormalizer', $parameter->getClass(), self::class)); + } + $parameterClass = $parameter->getClass()->getName(); + $parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createContext($context, $parameterName)); + } + } catch (\ReflectionException $e) { + throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e); + } catch (MissingConstructorArgumentsException $e) { + if (!$parameter->getType()->allowsNull()) { + throw $e; + } + $parameterData = null; + } + + return $parameterData; + } + +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php b/src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php new file mode 100644 index 0000000000000..bb6204c560d64 --- /dev/null +++ b/src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Instantiator; + +/** + * @author Jérôme Desjardins + */ +interface InstantiatorInterface +{ + public function instantiate(string $class, $data, $format = null, array $context = []); + + public function createChildContext(string $class, $data, array $context = [], $attribute); + +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Normalizer/NewObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/NewObjectNormalizer.php new file mode 100644 index 0000000000000..1ed509445035f --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/NewObjectNormalizer.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Normalizer; + +use Symfony\Component\Serializer\Instantiator\InstantiatorInterface; +use Symfony\Component\Serializer\SerializerAwareInterface; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * @author Jérôme Desjardins + */ +class NewObjectNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface +{ + private $instantiator; + + public function __construct(InstantiatorInterface $instantiator) + { + $this->instantiator = $instantiator; + } + + public function denormalize($data, $class, $format = null, array $context = []) + { + if (!isset($context['cache_key'])) { + $context['cache_key'] = $this->getCacheKey($format, $context); + } + + $allowedAttributes = $this->getAllowedAttributes($class, $context, true); + $normalizedData = $this->prepareForDenormalization($data); + $extraAttributes = []; + + $this->instantiator->instantiate($class, $normalizedData, $format, $context); + + foreach ($normalizedData as $attribute => $value) { + if ($this->nameConverter) { + $attribute = $this->nameConverter->denormalize($attribute, $class, $format, $context); + } + + if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($class, $attribute, $format, $context)) { + if (!($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES])) { + $extraAttributes[] = $attribute; + } + + continue; + } + + if ($context[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) { + try { + $context[self::OBJECT_TO_POPULATE] = $this->getAttributeValue($object, $attribute, $format, $context); + } catch (NoSuchPropertyException $e) { + } + } + + $value = $this->validateAndDenormalize($class, $attribute, $value, $format, $context); + try { + $this->setAttributeValue($object, $attribute, $value, $format, $context); + } catch (InvalidArgumentException $e) { + throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e); + } + } + + if (!empty($extraAttributes)) { + throw new ExtraAttributesException($extraAttributes); + } + + return $object; + } + + /** + * {@inheritdoc} + */ + public function supportsDenormalization($data, $type, $format = null) + { + return \class_exists($type) || (\interface_exists($type, false) && $this->classDiscriminatorResolver && null !== $this->classDiscriminatorResolver->getMappingForClass($type)); + } + + public function normalize($object, $format = null, array $context = []) + { + // TODO: Implement normalize() method. + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null) + { + return \is_object($data) && !$data instanceof \Traversable; + } + + + /** + * {@inheritdoc} + */ + public function hasCacheableSupportsMethod(): bool + { + return __CLASS__ === \get_class($this); + } + + public function setSerializer(SerializerInterface $serializer) + { + // TODO: Implement setSerializer() method. + } +} \ No newline at end of file From ebb86e09506406960a9f34973ce4fa35725dc1ec Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Sun, 7 Apr 2019 10:10:39 +0200 Subject: [PATCH 2/3] [Serializer] Instantiator - Add an interface and default implementation to instantiate objects #30956 --- .../Serializer/Instantiator/Instantiator.php | 136 +++++++---------- .../Instantiator/InstantiatorInterface.php | 14 +- .../Normalizer/NewObjectNormalizer.php | 111 -------------- .../Tests/Instantiator/InstantiatorTest.php | 140 ++++++++++++++++++ 4 files changed, 202 insertions(+), 199 deletions(-) delete mode 100644 src/Symfony/Component/Serializer/Normalizer/NewObjectNormalizer.php create mode 100644 src/Symfony/Component/Serializer/Tests/Instantiator/InstantiatorTest.php diff --git a/src/Symfony/Component/Serializer/Instantiator/Instantiator.php b/src/Symfony/Component/Serializer/Instantiator/Instantiator.php index 32767c686c43e..e868211921e62 100644 --- a/src/Symfony/Component/Serializer/Instantiator/Instantiator.php +++ b/src/Symfony/Component/Serializer/Instantiator/Instantiator.php @@ -10,131 +10,100 @@ */ namespace Symfony\Component\Serializer\Instantiator; + use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; -use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; use Symfony\Component\Serializer\Exception\RuntimeException; -use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait; -use Symfony\Component\Serializer\SerializerAwareInterface; -use Symfony\Component\Serializer\SerializerAwareTrait; /** * @author Jérôme Desjardins */ -class Instantiator implements InstantiatorInterface, SerializerAwareInterface +class Instantiator implements InstantiatorInterface, DenormalizerAwareInterface { + public const DEFAULT_CONSTRUCTOR_ARGUMENTS = AbstractObjectNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS; + use ObjectToPopulateTrait; - use SerializerAwareTrait; + use DenormalizerAwareTrait; private $classDiscriminatorResolver; private $propertyListExtractor; private $nameConverter; - public function __construct(ClassDiscriminatorResolverInterface $classDiscriminatorResolver, PropertyListExtractorInterface $propertyListExtractor, NameConverterInterface $nameConverter) + public function __construct(PropertyListExtractorInterface $propertyListExtractor = null, NameConverterInterface $nameConverter = null) { - $this->classDiscriminatorResolver = $classDiscriminatorResolver; $this->propertyListExtractor = $propertyListExtractor; $this->nameConverter = $nameConverter; } + /** + * {@inheritdoc} + */ public function instantiate(string $class, $data, $format = null, array $context = []) { - if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { - if (!isset($data[$mapping->getTypeProperty()])) { - throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping->getTypeProperty(), $class)); - } - - $type = $data[$mapping->getTypeProperty()]; - if (null === ($mappedClass = $mapping->getClassForType($type))) { - throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"', $type, $class)); - } + $allowedAttributes = $this->propertyListExtractor ? $this->propertyListExtractor->getProperties($class, $context) : null; + $reflectionClass = new \ReflectionClass($class); + $constructor = $reflectionClass->getConstructor(); - $class = $mappedClass; + if (null === $constructor) { + return new $class(); } - $reflectionClass = new \ReflectionClass($class); + $constructorParameters = $constructor->getParameters(); - if (null !== $object = $this->extractObjectToPopulate($class, $context, AbstractNormalizer::OBJECT_TO_POPULATE)) { - unset($context[AbstractNormalizer::OBJECT_TO_POPULATE]); + $params = []; + foreach ($constructorParameters as $constructorParameter) { + $paramName = $constructorParameter->name; + $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName; + $allowed = null === $allowedAttributes || \in_array($paramName, $allowedAttributes, true); - return $object; - } + if ($allowed && $constructorParameter->isVariadic() && \array_key_exists($key, $data)) { + if (!\is_array($data[$paramName])) { + throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because the variadic parameter %s can only accept an array.', $class, $constructorParameter->name)); + } - $defaultConstructionArgumentKey = $context['defaultConstructionArgumentKey'] ?? AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS - $allowedAttributes = $this->propertyListExtractor->getProperties($class, $context); - $constructor = $reflectionClass->getConstructor(); - if ($constructor) { - $constructorParameters = $constructor->getParameters(); - - $params = []; - foreach ($constructorParameters as $constructorParameter) { - $paramName = $constructorParameter->name; - $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName; - - $allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes); - if ($constructorParameter->isVariadic()) { - if ($allowed && (isset($data[$key]) || \array_key_exists($key, $data))) { - if (!\is_array($data[$paramName])) { - throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because the variadic parameter %s can only accept an array.', $class, $constructorParameter->name)); - } - - $params = array_merge($params, $data[$paramName]); - } - } elseif ($allowed && (isset($data[$key]) || \array_key_exists($key, $data))) { - $parameterData = $data[$key]; - if (null === $parameterData && $constructorParameter->allowsNull()) { - $params[] = null; - // Don't run set for a parameter passed to the constructor - unset($data[$key]); - continue; - } - - // Don't run set for a parameter passed to the constructor - $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format); - unset($data[$key]); - } elseif (\array_key_exists($key, $context[$defaultConstructionArgumentKey][$class] ?? [])) { - $params[] = $context[$defaultConstructionArgumentKey][$class][$key]; - } elseif ($constructorParameter->isDefaultValueAvailable()) { - $params[] = $constructorParameter->getDefaultValue(); - } else { - throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name)); + $params = array_merge($params, $data[$paramName]); + } elseif ($allowed && \array_key_exists($key, $data)) { + $parameterData = $data[$key]; + + if (null === $parameterData && $constructorParameter->allowsNull()) { + $params[] = null; + + continue; } - } - if ($constructor->isConstructor()) { - return $reflectionClass->newInstanceArgs($params); + $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format); + } elseif (\array_key_exists($key, $context[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) { + $params[] = $context[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key]; + } elseif ($constructorParameter->isDefaultValueAvailable()) { + $params[] = $constructorParameter->getDefaultValue(); + } else { + throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name)); } - - return $constructor->invokeArgs(null, $params); } - return new $class(); - } - - public function createChildContext(string $class, string $attribute, $parentData, array $parentContext = []) - { - if (isset($parentContext[AbstractNormalizer::ATTRIBUTES][$attribute])) { - $parentContext[AbstractNormalizer::ATTRIBUTES] = $parentContext[AbstractNormalizer::ATTRIBUTES][$attribute]; - } else { - unset($parentContext[AbstractNormalizer::ATTRIBUTES]); + if ($constructor->isConstructor()) { + return $reflectionClass->newInstanceArgs($params); } - return $parentContext; + return $constructor->invokeArgs(null, $params); } private function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null) { try { if (null !== $parameter->getClass()) { - if (!$this->serializer instanceof DenormalizerInterface) { - throw new LogicException(sprintf('Cannot create an instance of %s from serialized data because the serializer inject in "%s" is not a denormalizer', $parameter->getClass(), self::class)); - } $parameterClass = $parameter->getClass()->getName(); - $parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createContext($context, $parameterName)); + + if (null === $this->denormalizer) { + throw new MissingConstructorArgumentsException(sprintf('Could not create object of class "%s" of the parameter "%s".', $parameterClass, $parameterName)); + } + + $parameterData = $this->denormalizer->denormalize($parameterData, $parameterClass, $format, $context); } } catch (\ReflectionException $e) { throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e); @@ -147,5 +116,4 @@ private function denormalizeParameter(\ReflectionClass $class, \ReflectionParame return $parameterData; } - -} \ No newline at end of file +} diff --git a/src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php b/src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php index bb6204c560d64..276074b3651ba 100644 --- a/src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php +++ b/src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php @@ -11,13 +11,19 @@ namespace Symfony\Component\Serializer\Instantiator; +use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; + /** * @author Jérôme Desjardins */ interface InstantiatorInterface { + /** + * Instantiate a new object. + * + * @throws MissingConstructorArgumentsException When some arguments are missing to use the constructor + * + * @return mixed + */ public function instantiate(string $class, $data, $format = null, array $context = []); - - public function createChildContext(string $class, $data, array $context = [], $attribute); - -} \ No newline at end of file +} diff --git a/src/Symfony/Component/Serializer/Normalizer/NewObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/NewObjectNormalizer.php deleted file mode 100644 index 1ed509445035f..0000000000000 --- a/src/Symfony/Component/Serializer/Normalizer/NewObjectNormalizer.php +++ /dev/null @@ -1,111 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Serializer\Normalizer; - -use Symfony\Component\Serializer\Instantiator\InstantiatorInterface; -use Symfony\Component\Serializer\SerializerAwareInterface; -use Symfony\Component\Serializer\SerializerInterface; - -/** - * @author Jérôme Desjardins - */ -class NewObjectNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface -{ - private $instantiator; - - public function __construct(InstantiatorInterface $instantiator) - { - $this->instantiator = $instantiator; - } - - public function denormalize($data, $class, $format = null, array $context = []) - { - if (!isset($context['cache_key'])) { - $context['cache_key'] = $this->getCacheKey($format, $context); - } - - $allowedAttributes = $this->getAllowedAttributes($class, $context, true); - $normalizedData = $this->prepareForDenormalization($data); - $extraAttributes = []; - - $this->instantiator->instantiate($class, $normalizedData, $format, $context); - - foreach ($normalizedData as $attribute => $value) { - if ($this->nameConverter) { - $attribute = $this->nameConverter->denormalize($attribute, $class, $format, $context); - } - - if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($class, $attribute, $format, $context)) { - if (!($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES])) { - $extraAttributes[] = $attribute; - } - - continue; - } - - if ($context[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) { - try { - $context[self::OBJECT_TO_POPULATE] = $this->getAttributeValue($object, $attribute, $format, $context); - } catch (NoSuchPropertyException $e) { - } - } - - $value = $this->validateAndDenormalize($class, $attribute, $value, $format, $context); - try { - $this->setAttributeValue($object, $attribute, $value, $format, $context); - } catch (InvalidArgumentException $e) { - throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e); - } - } - - if (!empty($extraAttributes)) { - throw new ExtraAttributesException($extraAttributes); - } - - return $object; - } - - /** - * {@inheritdoc} - */ - public function supportsDenormalization($data, $type, $format = null) - { - return \class_exists($type) || (\interface_exists($type, false) && $this->classDiscriminatorResolver && null !== $this->classDiscriminatorResolver->getMappingForClass($type)); - } - - public function normalize($object, $format = null, array $context = []) - { - // TODO: Implement normalize() method. - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null) - { - return \is_object($data) && !$data instanceof \Traversable; - } - - - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - return __CLASS__ === \get_class($this); - } - - public function setSerializer(SerializerInterface $serializer) - { - // TODO: Implement setSerializer() method. - } -} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Instantiator/InstantiatorTest.php b/src/Symfony/Component/Serializer/Tests/Instantiator/InstantiatorTest.php new file mode 100644 index 0000000000000..0353d9f0dfd2f --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Instantiator/InstantiatorTest.php @@ -0,0 +1,140 @@ + 'foo', 'bar' => 'bar', 'baz' => 'baz']; + + $dummy = $instantiator->instantiate(DummyWithoutConstructor::class, $data); + + $this->assertInstanceOf(DummyWithoutConstructor::class, $dummy); + } + + public function testInstantiateWithConstructor() + { + $instantiator = new Instantiator(); + $data = ['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz']; + + $dummy = $instantiator->instantiate(DummyWithConstructor::class, $data); + + $this->assertInstanceOf(DummyWithConstructor::class, $dummy); + $this->assertSame('foo', $dummy->foo); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException + * @expectedExceptionMessage Cannot create an instance of Symfony\Component\Serializer\Tests\Instantiator\DummyWithExtraConstructor from serialized data because its constructor requires parameter "extra" to be present. + */ + public function testCannotInstantiate() + { + $instantiator = new Instantiator(); + $data = ['foo' => 'foo']; + + $instantiator->instantiate(DummyWithExtraConstructor::class, $data); + } + + public function testInstantiateWithDefaultArguments() + { + $instantiator = new Instantiator(); + $data = ['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz']; + + $dummy = $instantiator->instantiate(DummyWithExtraConstructor::class, $data, null, [ + AbstractObjectNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS => [ + DummyWithExtraConstructor::class => ['extra' => 'extraData'], + ], + ]); + + $this->assertInstanceOf(DummyWithExtraConstructor::class, $dummy); + $this->assertSame('foo', $dummy->foo); + $this->assertSame('extraData', $dummy->extra); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException + * @expectedExceptionMessage Could not create object of class "Symfony\Component\Serializer\Tests\Instantiator\DummyBar" of the parameter "bar". + */ + public function testInstantiateWithDenormalizationAndDenormalizer() + { + $instantiator = new Instantiator(); + $data = ['foo' => 'foo', 'bar' => ['baz' => 'baz']]; + + $dummy = $instantiator->instantiate(DummyWithObjectArgument::class, $data); + + $this->assertInstanceOf(DummyWithExtraConstructor::class, $dummy); + $this->assertSame('foo', $dummy->foo); + $this->assertSame('extraData', $dummy->extra); + } + + public function testInstantiateWithDenormalization() + { + $instantiator = new Instantiator(); + $instantiator->setDenormalizer(new ObjectNormalizer()); + + $data = ['foo' => 'foo', 'bar' => ['baz' => 'baz']]; + + $dummy = $instantiator->instantiate(DummyWithObjectArgument::class, $data); + + $this->assertInstanceOf(DummyWithObjectArgument::class, $dummy); + $this->assertSame('foo', $dummy->foo); + $this->assertInstanceOf(DummyBar::class, $dummy->bar); + } +} + +class DummyWithoutConstructor +{ + public $foo; + public $bar; + public $baz; +} + +class DummyWithConstructor +{ + public $foo; + public $bar; + public $quz; + + public function __construct($foo) + { + $this->foo = $foo; + } +} + +class DummyWithExtraConstructor +{ + public $foo; + public $bar; + public $quz; + public $extra; + + public function __construct($foo, $extra) + { + $this->foo = $foo; + $this->extra = $extra; + } +} + +class DummyWithObjectArgument +{ + public $foo; + public $bar; + + public function __construct($foo, DummyBar $bar) + { + $this->foo = $foo; + $this->bar = $bar; + } +} + +class DummyBar +{ + public $baz; +} From cc3f13dc35faa90ebd46160ea1d094254ef8ee59 Mon Sep 17 00:00:00 2001 From: Baptiste Leduc Date: Fri, 31 Jan 2020 22:00:04 +0100 Subject: [PATCH 3/3] [Serializer] Instantiator - Add an interface and default implementation to instantiate objects #30956 --- .../Context/ChildContextFactory.php | 35 +++ .../Context/ChildContextFactoryInterface.php | 22 ++ .../Context/ObjectChildContextFactory.php | 59 +++++ .../Serializer/Instantiator/Instantiator.php | 209 ++++++++++++++++-- .../Instantiator/InstantiatorInterface.php | 9 +- .../Instantiator/InstantiatorResult.php | 58 +++++ .../Normalizer/AbstractNormalizer.php | 19 +- .../Normalizer/AbstractObjectNormalizer.php | 74 ++++--- .../Normalizer/ObjectNormalizer.php | 6 +- ....php => StaticConstructorInstantiator.php} | 12 +- .../Tests/Instantiator/InstantiatorTest.php | 43 ++-- .../Normalizer/AbstractNormalizerTest.php | 5 +- .../AbstractObjectNormalizerTest.php | 23 +- .../Tests/Normalizer/ObjectNormalizerTest.php | 6 +- 14 files changed, 466 insertions(+), 114 deletions(-) create mode 100644 src/Symfony/Component/Serializer/Context/ChildContextFactory.php create mode 100644 src/Symfony/Component/Serializer/Context/ChildContextFactoryInterface.php create mode 100644 src/Symfony/Component/Serializer/Context/ObjectChildContextFactory.php create mode 100644 src/Symfony/Component/Serializer/Instantiator/InstantiatorResult.php rename src/Symfony/Component/Serializer/Tests/Fixtures/{StaticConstructorNormalizer.php => StaticConstructorInstantiator.php} (53%) diff --git a/src/Symfony/Component/Serializer/Context/ChildContextFactory.php b/src/Symfony/Component/Serializer/Context/ChildContextFactory.php new file mode 100644 index 0000000000000..d370a2e10ddba --- /dev/null +++ b/src/Symfony/Component/Serializer/Context/ChildContextFactory.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Context; + +use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; + +/** + * Create a child context during serialization/deserialization process. + * + * @author Baptiste Leduc + */ +class ChildContextFactory implements ChildContextFactoryInterface +{ + public const ATTRIBUTES = AbstractObjectNormalizer::ATTRIBUTES; + + public function create(array $parentContext, string $attribute, ?string $format = null, array $defaultContext = []): array + { + if (isset($parentContext[self::ATTRIBUTES][$attribute])) { + $parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute]; + } else { + unset($parentContext[self::ATTRIBUTES]); + } + + return $parentContext; + } +} diff --git a/src/Symfony/Component/Serializer/Context/ChildContextFactoryInterface.php b/src/Symfony/Component/Serializer/Context/ChildContextFactoryInterface.php new file mode 100644 index 0000000000000..37bb01c53665d --- /dev/null +++ b/src/Symfony/Component/Serializer/Context/ChildContextFactoryInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Context; + +/** + * Defines the interface to create a child context during serialization/deserialization or instantiation process. + * + * @author Baptiste Leduc + */ +interface ChildContextFactoryInterface +{ + public function create(array $parentContext, string $attribute, ?string $format = null, array $defaultContext = []): array; +} diff --git a/src/Symfony/Component/Serializer/Context/ObjectChildContextFactory.php b/src/Symfony/Component/Serializer/Context/ObjectChildContextFactory.php new file mode 100644 index 0000000000000..31921aadc77f5 --- /dev/null +++ b/src/Symfony/Component/Serializer/Context/ObjectChildContextFactory.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Context; + +use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; + +/** + * Create a child context with cache_key during serialization/deserialization or instantiation process. + * + * @author Baptiste Leduc + */ +class ObjectChildContextFactory extends ChildContextFactory +{ + public const EXCLUDE_FROM_CACHE_KEY = AbstractObjectNormalizer::EXCLUDE_FROM_CACHE_KEY; + public const IGNORED_ATTRIBUTES = AbstractObjectNormalizer::IGNORED_ATTRIBUTES; + + public function create(array $parentContext, string $attribute, ?string $format = null, array $defaultContext = []): array + { + $parentContext = parent::create($parentContext, $attribute, $format, $defaultContext); + $parentContext['cache_key'] = $this->getAttributesCacheKey($parentContext, $format, $defaultContext); + + return $parentContext; + } + + /** + * Builds the cache key for the attributes cache. + * + * The key must be different for every option in the context that could change which attributes should be handled. + * + * @return bool|string + */ + private function getAttributesCacheKey(array $context, ?string $format = null, array $defaultContext = []) + { + foreach ($context[self::EXCLUDE_FROM_CACHE_KEY] ?? $defaultContext[self::EXCLUDE_FROM_CACHE_KEY] ?? [] as $key) { + unset($context[$key]); + } + unset($context[self::EXCLUDE_FROM_CACHE_KEY]); + unset($context['cache_key']); // avoid artificially different keys + + try { + return md5($format.serialize([ + 'context' => $context, + 'ignored' => $context[self::IGNORED_ATTRIBUTES] ?? $defaultContext[self::IGNORED_ATTRIBUTES] ?? [], + ])); + } catch (\Exception $exception) { + // The context cannot be serialized, skip the cache + return false; + } + } +} diff --git a/src/Symfony/Component/Serializer/Instantiator/Instantiator.php b/src/Symfony/Component/Serializer/Instantiator/Instantiator.php index e868211921e62..fd4b144522a87 100644 --- a/src/Symfony/Component/Serializer/Instantiator/Instantiator.php +++ b/src/Symfony/Component/Serializer/Instantiator/Instantiator.php @@ -11,9 +11,20 @@ namespace Symfony\Component\Serializer\Instantiator; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyInfo\Extractor\SerializerExtractor; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\Context\ChildContextFactoryInterface; +use Symfony\Component\Serializer\Context\ObjectChildContextFactory; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; use Symfony\Component\Serializer\Exception\RuntimeException; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; @@ -21,36 +32,75 @@ use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait; /** + * Instantiates an object using constructor parameters when needed. + * + * This class also allows to denormalize data into an existing object if + * it is present in the context with the object_to_populate. This object + * is removed from the context before being returned to avoid side effects + * when recursively normalizing an object graph. + * * @author Jérôme Desjardins + * @author Baptiste Leduc */ class Instantiator implements InstantiatorInterface, DenormalizerAwareInterface { + public const ATTRIBUTES = AbstractObjectNormalizer::ATTRIBUTES; + public const IGNORED_ATTRIBUTES = AbstractObjectNormalizer::IGNORED_ATTRIBUTES; + public const OBJECT_TO_POPULATE = AbstractObjectNormalizer::OBJECT_TO_POPULATE; public const DEFAULT_CONSTRUCTOR_ARGUMENTS = AbstractObjectNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS; use ObjectToPopulateTrait; use DenormalizerAwareTrait; private $classDiscriminatorResolver; + private $propertyTypeExtractor; private $propertyListExtractor; private $nameConverter; + private $propertyAccessor; + private $childContextFactory; - public function __construct(PropertyListExtractorInterface $propertyListExtractor = null, NameConverterInterface $nameConverter = null) + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, PropertyListExtractorInterface $propertyListExtractor = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, ChildContextFactoryInterface $childContextFactory = null) { + if (null === $classDiscriminatorResolver && null !== $classMetadataFactory) { + $classDiscriminatorResolver = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + } + $this->classDiscriminatorResolver = $classDiscriminatorResolver; + + $this->propertyTypeExtractor = $propertyTypeExtractor; + if (null === $propertyListExtractor && null !== $classMetadataFactory) { + $propertyListExtractor = new SerializerExtractor($classMetadataFactory); + } $this->propertyListExtractor = $propertyListExtractor; $this->nameConverter = $nameConverter; + $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); + + $this->childContextFactory = $childContextFactory ?? new ObjectChildContextFactory(); } /** * {@inheritdoc} */ - public function instantiate(string $class, $data, $format = null, array $context = []) + public function instantiate(string $class, array $data, array $context, string $format = null): InstantiatorResult { + if (null !== $this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { + $class = $this->handleDiscriminator($class, $data, $mapping); + } + + if (null !== $object = $this->extractObjectToPopulate($class, $context, self::OBJECT_TO_POPULATE)) { + unset($context[self::OBJECT_TO_POPULATE]); + + return new InstantiatorResult($object, $data, $context); + } + + // clean up even if no match + unset($context[self::OBJECT_TO_POPULATE]); + $allowedAttributes = $this->propertyListExtractor ? $this->propertyListExtractor->getProperties($class, $context) : null; $reflectionClass = new \ReflectionClass($class); - $constructor = $reflectionClass->getConstructor(); + $constructor = $this->getConstructor($reflectionClass); - if (null === $constructor) { - return new $class(); + if (null === $constructor || !$constructor->isPublic()) { + return new InstantiatorResult($reflectionClass->newInstanceWithoutConstructor(), $data, $context); } $constructorParameters = $constructor->getParameters(); @@ -59,61 +109,180 @@ public function instantiate(string $class, $data, $format = null, array $context foreach ($constructorParameters as $constructorParameter) { $paramName = $constructorParameter->name; $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName; - $allowed = null === $allowedAttributes || \in_array($paramName, $allowedAttributes, true); + $allowed = (null === $allowedAttributes || \in_array($paramName, $allowedAttributes, true)) && $this->isAllowedAttribute($object, $paramName, $format, $context); + $childContext = $this->childContextFactory->create($context, $paramName, $format); + + if ($allowed && $constructorParameter->isVariadic()) { + if (!\array_key_exists($paramName, $data)) { + $data[$paramName] = []; + } - if ($allowed && $constructorParameter->isVariadic() && \array_key_exists($key, $data)) { if (!\is_array($data[$paramName])) { - throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because the variadic parameter %s can only accept an array.', $class, $constructorParameter->name)); + throw new RuntimeException(sprintf('Cannot create an instance of "%s" from serialized data because the variadic parameter "%s" can only accept an array.', $class, $constructorParameter->name)); + } + + $variadicParameters = []; + foreach ($data[$paramName] as $parameterData) { + [$currentParameter, $error] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $childContext, $format); + + if (null !== $error) { + return new InstantiatorResult(null, $data, $context, $error); + } else { + $variadicParameters[] = $currentParameter; + } } - $params = array_merge($params, $data[$paramName]); + $params = array_merge($params, $variadicParameters); + unset($data[$key]); } elseif ($allowed && \array_key_exists($key, $data)) { $parameterData = $data[$key]; if (null === $parameterData && $constructorParameter->allowsNull()) { $params[] = null; + unset($data[$key]); continue; } - $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format); + [$currentParameter, $error] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $childContext, $format); + + if (null !== $error) { + return new InstantiatorResult(null, $data, $context, $error); + } + $params[] = $currentParameter; + unset($data[$key]); } elseif (\array_key_exists($key, $context[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) { $params[] = $context[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key]; } elseif ($constructorParameter->isDefaultValueAvailable()) { $params[] = $constructorParameter->getDefaultValue(); } else { - throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name)); + return new InstantiatorResult(null, $data, $context, sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name)); } } if ($constructor->isConstructor()) { - return $reflectionClass->newInstanceArgs($params); + return new InstantiatorResult($reflectionClass->newInstanceArgs($params), $data, $context); + } + + return new InstantiatorResult($constructor->invokeArgs(null, $params), $data, $context); + } + + private function handleDiscriminator(string $class, array $data, ClassDiscriminatorMapping $mapping): string + { + if (!isset($data[$mapping->getTypeProperty()])) { + throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class)); + } + + $type = $data[$mapping->getTypeProperty()]; + if (null === ($mappedClass = $mapping->getClassForType($type))) { + throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s".', $type, $class)); } - return $constructor->invokeArgs(null, $params); + return $mappedClass; } - private function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null) + private function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null): array { try { - if (null !== $parameter->getClass()) { - $parameterClass = $parameter->getClass()->getName(); + $parameterClass = $parameter->getClass(); + if (null === $parameterClass && null !== $this->propertyTypeExtractor) { + $types = $this->propertyTypeExtractor->getTypes($class->getName(), $parameterName, $context); + + if (null !== $types) { + foreach ($types as $type) { + $collectionValueType = $type->isCollection() ? $type->getCollectionValueType() : null; + + if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) { + $builtinType = Type::BUILTIN_TYPE_OBJECT; + $class = $collectionValueType->getClassName().'[]'; + + if (null !== $collectionKeyType = $type->getCollectionKeyType()) { + $context['key_type'] = $collectionKeyType; + } + } elseif ($type->isCollection() && null !== $collectionValueType && Type::BUILTIN_TYPE_ARRAY === $collectionValueType->getBuiltinType()) { + // get inner type for any nested array + $innerType = $collectionValueType; + + // note that it will break for any other builtinType + $dimensions = '[]'; + while (null !== $innerType->getCollectionValueType() && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) { + $dimensions .= '[]'; + $innerType = $innerType->getCollectionValueType(); + } + + if (null !== $innerType->getClassName()) { + // the builtinType is the inner one and the class is the class followed by []...[] + $builtinType = $innerType->getBuiltinType(); + $class = $innerType->getClassName().$dimensions; + } else { + // default fallback (keep it as array) + $builtinType = $type->getBuiltinType(); + $class = $type->getClassName(); + } + } else { + $builtinType = $type->getBuiltinType(); + $class = $type->getClassName(); + } + + if (Type::BUILTIN_TYPE_OBJECT === $builtinType) { + if (null === $this->denormalizer) { + throw new MissingConstructorArgumentsException(sprintf('Could not create object of class "%s" of the parameter "%s".', $class, $parameterName)); + } + + if ($this->denormalizer->supportsDenormalization($parameterData, $class, $format, $context)) { + return [$this->denormalizer->denormalize($parameterData, $class, $format, $context), null]; + } + } + } + } + } + + if (null !== $parameterClass) { + $parameterClassName = $parameter->getClass()->getName(); if (null === $this->denormalizer) { - throw new MissingConstructorArgumentsException(sprintf('Could not create object of class "%s" of the parameter "%s".', $parameterClass, $parameterName)); + throw new MissingConstructorArgumentsException(sprintf('Could not create object of class "%s" of the parameter "%s".', $parameterClassName, $parameterName)); } - $parameterData = $this->denormalizer->denormalize($parameterData, $parameterClass, $format, $context); + $parameterData = $this->denormalizer->denormalize($parameterData, $parameterClassName, $format, $context); } } catch (\ReflectionException $e) { throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e); } catch (MissingConstructorArgumentsException $e) { if (!$parameter->getType()->allowsNull()) { - throw $e; + return [null, $e->getMessage()]; } $parameterData = null; } - return $parameterData; + return [$parameterData, null]; + } + + protected function getConstructor(\ReflectionClass $reflectionClass): ?\ReflectionMethod + { + return $reflectionClass->getConstructor(); + } + + /** + * @param object|string $classOrObject + */ + private function isAllowedAttribute($classOrObject, string $attribute, string $format = null, array $context = []): bool + { + $ignoredAttributes = $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES] ?? []; + if (\in_array($attribute, $ignoredAttributes)) { + return false; + } + + $attributes = $context[self::ATTRIBUTES] ?? $this->defaultContext[self::ATTRIBUTES] ?? null; + if (isset($attributes[$attribute])) { + // Nested attributes + return true; + } + + if (\is_array($attributes)) { + return \in_array($attribute, $attributes, true); + } + + return true; } } diff --git a/src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php b/src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php index 276074b3651ba..1dad91c70b069 100644 --- a/src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php +++ b/src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php @@ -14,16 +14,17 @@ use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; /** + * Describes the interface to instantiate an object using constructor parameters when needed. + * * @author Jérôme Desjardins + * @author Baptiste Leduc */ interface InstantiatorInterface { /** - * Instantiate a new object. + * Instantiates a new object. * * @throws MissingConstructorArgumentsException When some arguments are missing to use the constructor - * - * @return mixed */ - public function instantiate(string $class, $data, $format = null, array $context = []); + public function instantiate(string $class, array $data, array $context, string $format = null): InstantiatorResult; } diff --git a/src/Symfony/Component/Serializer/Instantiator/InstantiatorResult.php b/src/Symfony/Component/Serializer/Instantiator/InstantiatorResult.php new file mode 100644 index 0000000000000..98eb4310f706d --- /dev/null +++ b/src/Symfony/Component/Serializer/Instantiator/InstantiatorResult.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Instantiator; + +/** + * Contains the result of an instantiation process. + * + * @author Baptiste Leduc + */ +final class InstantiatorResult +{ + private $object; + private $data; + private $context; + private $error; + + public function __construct(?object $object, array $data, array $context, string $error = null) + { + $this->object = $object; + $this->data = $data; + $this->context = $context; + $this->error = $error; + } + + public function getObject(): ?object + { + return $this->object; + } + + public function getUnusedData(): array + { + return $this->data; + } + + public function getUnusedContext(): array + { + return $this->context; + } + + public function getError(): ?string + { + return $this->error; + } + + public function hasFailed(): bool + { + return null === $this->object; + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index e0b52da867831..6930a5a7549e1 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Serializer\Normalizer; +use Symfony\Component\Serializer\Context\ChildContextFactoryInterface; +use Symfony\Component\Serializer\Context\ObjectChildContextFactory; use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\LogicException; @@ -133,10 +135,12 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn */ protected $nameConverter; + protected $childContextFactory; + /** * Sets the {@link ClassMetadataFactoryInterface} to use. */ - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, array $defaultContext = []) + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, array $defaultContext = [], ChildContextFactoryInterface $childContextFactory = null) { $this->classMetadataFactory = $classMetadataFactory; $this->nameConverter = $nameConverter; @@ -157,6 +161,8 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory if (isset($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER]) && !\is_callable($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER])) { throw new InvalidArgumentException(sprintf('Invalid callback found in the "%s" default context option.', self::CIRCULAR_REFERENCE_HANDLER)); } + + $this->childContextFactory = $childContextFactory ?? new ObjectChildContextFactory(); } /** @@ -306,6 +312,7 @@ protected function prepareForDenormalization($data) } /** +<<<<<<< HEAD * Returns the method to use to construct an object. This method must be either * the object constructor or static. * @@ -436,15 +443,11 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara /** * @internal + * + * @deprecated the "createChildContext" method is deprecated, use Symfony\Component\Serializer\Context\ChildContextFactory::create() instead */ protected function createChildContext(array $parentContext, string $attribute, ?string $format): array { - if (isset($parentContext[self::ATTRIBUTES][$attribute])) { - $parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute]; - } else { - unset($parentContext[self::ATTRIBUTES]); - } - - return $parentContext; + return $this->childContextFactory->create($parentContext, $attribute, $format); } } diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index b9d1653e57186..bcf3f63a393f2 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -15,11 +15,15 @@ use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\Context\ChildContextFactoryInterface; +use Symfony\Component\Serializer\Context\ObjectChildContextFactory; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Exception\ExtraAttributesException; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; -use Symfony\Component\Serializer\Exception\RuntimeException; +use Symfony\Component\Serializer\Instantiator\Instantiator; +use Symfony\Component\Serializer\Instantiator\InstantiatorInterface; use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; @@ -31,8 +35,10 @@ * * @author Kévin Dunglas */ -abstract class AbstractObjectNormalizer extends AbstractNormalizer +abstract class AbstractObjectNormalizer extends AbstractNormalizer implements DenormalizerAwareInterface { + use DenormalizerAwareTrait; + /** * Set to true to respect the max depth metadata on fields. */ @@ -91,6 +97,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects'; private $propertyTypeExtractor; + private $instantiator; private $typesCache = []; private $attributesCache = []; @@ -101,7 +108,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer */ protected $classDiscriminatorResolver; - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = []) + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [], ChildContextFactoryInterface $childContextFactory = null, InstantiatorInterface $instantiator = null) { parent::__construct($classMetadataFactory, $nameConverter, $defaultContext); @@ -118,6 +125,26 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory } $this->classDiscriminatorResolver = $classDiscriminatorResolver; $this->objectClassResolver = $objectClassResolver; + + $this->childContextFactory = $childContextFactory ?? new ObjectChildContextFactory(); + + if (null === $instantiator) { + $instantiator = new Instantiator($classMetadataFactory, $this->classDiscriminatorResolver, $propertyTypeExtractor, null, $nameConverter, null, $this->childContextFactory); + + if ($this->denormalizer instanceof DenormalizerInterface) { + $instantiator->setDenormalizer($this->denormalizer); + } + } + $this->instantiator = $instantiator; + } + + public function setDenormalizer(DenormalizerInterface $denormalizer) + { + $this->denormalizer = $denormalizer; + + // because we need a denormalizer for the instantiator & when doing a new AbstractObjectNormalizer THEN new + // Serializer, the DenormalizerAwareInterface propagation will be done after Instantiator creation. + $this->instantiator->setDenormalizer($denormalizer); } /** @@ -208,28 +235,6 @@ public function normalize($object, string $format = null, array $context = []) return $data; } - /** - * {@inheritdoc} - */ - protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null) - { - if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { - if (!isset($data[$mapping->getTypeProperty()])) { - throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class)); - } - - $type = $data[$mapping->getTypeProperty()]; - if (null === ($mappedClass = $mapping->getClassForType($type))) { - throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s".', $type, $class)); - } - - $class = $mappedClass; - $reflectionClass = new \ReflectionClass($class); - } - - return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes, $format); - } - /** * Gets and caches attributes for the given object, format and context. * @@ -304,8 +309,14 @@ public function denormalize($data, string $type, string $format = null, array $c $normalizedData = $this->prepareForDenormalization($data); $extraAttributes = []; - $reflectionClass = new \ReflectionClass($type); - $object = $this->instantiateObject($normalizedData, $type, $context, $reflectionClass, $allowedAttributes, $format); + $instantiatorResult = $this->instantiator->instantiate($type, $normalizedData, $context, $format); + if ($instantiatorResult->hasFailed()) { + throw new MissingConstructorArgumentsException($instantiatorResult->getError()); + } + $object = $instantiatorResult->getObject(); + $normalizedData = $instantiatorResult->getUnusedData(); + $context = $instantiatorResult->getUnusedContext(); + $resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object); foreach ($normalizedData as $attribute => $value) { @@ -332,7 +343,7 @@ public function denormalize($data, string $type, string $format = null, array $c try { $this->setAttributeValue($object, $attribute, $value, $format, $context); } catch (InvalidArgumentException $e) { - throw new NotNormalizableValueException(sprintf('Failed to denormalize attribute "%s" value for class "%s": ', $attribute, $type).$e->getMessage(), $e->getCode(), $e); + throw new NotNormalizableValueException(sprintf('Failed to denormalize attribute "%s" value for class "%s": "%s".', $attribute, $type, $e->getMessage()), $e->getCode(), $e); } } @@ -554,13 +565,12 @@ private function isMaxDepthReached(array $attributesMetadata, string $class, str * {@inheritdoc} * * @internal + * + * @deprecated the "createChildContext" method is deprecated, use Symfony\Component\Serializer\Context\ObjectChildContextFactory::create() instead */ protected function createChildContext(array $parentContext, string $attribute, ?string $format): array { - $context = parent::createChildContext($parentContext, $attribute, $format); - $context['cache_key'] = $this->getCacheKey($format, $context); - - return $context; + return $this->childContextFactory->create($parentContext, $attribute, $format); } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index c3ab890951b3a..87d19030c8f31 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -15,7 +15,9 @@ use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\Serializer\Context\ChildContextFactoryInterface; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Instantiator\InstantiatorInterface; use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; @@ -34,13 +36,13 @@ class ObjectNormalizer extends AbstractObjectNormalizer private $objectClassResolver; - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = []) + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [], ChildContextFactoryInterface $childContextFactory = null, InstantiatorInterface $instantiator = null) { if (!class_exists(PropertyAccess::class)) { throw new LogicException('The ObjectNormalizer class requires the "PropertyAccess" component. Install "symfony/property-access" to use it.'); } - parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext); + parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext, $childContextFactory, $instantiator); $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorNormalizer.php b/src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorInstantiator.php similarity index 53% rename from src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorNormalizer.php rename to src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorInstantiator.php index 10398f4fc271f..9f78d3832a4d9 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorNormalizer.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorInstantiator.php @@ -11,22 +11,24 @@ namespace Symfony\Component\Serializer\Tests\Fixtures; -use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Instantiator\Instantiator; /** - * @author Guilhem N. + * @author Baptite Leduc */ -class StaticConstructorNormalizer extends ObjectNormalizer +class StaticConstructorInstantiator extends Instantiator { /** * {@inheritdoc} */ - protected function getConstructor(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes): ?\ReflectionMethod + protected function getConstructor(\ReflectionClass $reflectionClass): ?\ReflectionMethod { + $class = $reflectionClass->getName(); + if (is_a($class, StaticConstructorDummy::class, true)) { return new \ReflectionMethod($class, 'create'); } - return parent::getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes); + return parent::getConstructor($reflectionClass); } } diff --git a/src/Symfony/Component/Serializer/Tests/Instantiator/InstantiatorTest.php b/src/Symfony/Component/Serializer/Tests/Instantiator/InstantiatorTest.php index 0353d9f0dfd2f..46334f76217b6 100644 --- a/src/Symfony/Component/Serializer/Tests/Instantiator/InstantiatorTest.php +++ b/src/Symfony/Component/Serializer/Tests/Instantiator/InstantiatorTest.php @@ -13,65 +13,66 @@ public function testInstantiate() { $instantiator = new Instantiator(); $data = ['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz']; + $context = []; - $dummy = $instantiator->instantiate(DummyWithoutConstructor::class, $data); + $dummyResult = $instantiator->instantiate(DummyWithoutConstructor::class, $data, $context); - $this->assertInstanceOf(DummyWithoutConstructor::class, $dummy); + $this->assertInstanceOf(DummyWithoutConstructor::class, $dummyResult->getObject()); } public function testInstantiateWithConstructor() { $instantiator = new Instantiator(); $data = ['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz']; + $context = []; - $dummy = $instantiator->instantiate(DummyWithConstructor::class, $data); + $dummyResult = $instantiator->instantiate(DummyWithConstructor::class, $data, $context); + $dummy = $dummyResult->getObject(); $this->assertInstanceOf(DummyWithConstructor::class, $dummy); $this->assertSame('foo', $dummy->foo); } - /** - * @expectedException \Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException - * @expectedExceptionMessage Cannot create an instance of Symfony\Component\Serializer\Tests\Instantiator\DummyWithExtraConstructor from serialized data because its constructor requires parameter "extra" to be present. - */ public function testCannotInstantiate() { $instantiator = new Instantiator(); $data = ['foo' => 'foo']; + $context = []; - $instantiator->instantiate(DummyWithExtraConstructor::class, $data); + $dummyResult = $instantiator->instantiate(DummyWithExtraConstructor::class, $data, $context); + + $this->assertNull($dummyResult->getObject()); + $this->assertEquals('Cannot create an instance of "Symfony\\Component\\Serializer\\Tests\\Instantiator\\DummyWithExtraConstructor" from serialized data because its constructor requires parameter "extra" to be present.', $dummyResult->getError()); } public function testInstantiateWithDefaultArguments() { $instantiator = new Instantiator(); $data = ['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz']; - - $dummy = $instantiator->instantiate(DummyWithExtraConstructor::class, $data, null, [ + $context = [ AbstractObjectNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS => [ DummyWithExtraConstructor::class => ['extra' => 'extraData'], ], - ]); + ]; + + $dummyResult = $instantiator->instantiate(DummyWithExtraConstructor::class, $data, $context, null); + $dummy = $dummyResult->getObject(); $this->assertInstanceOf(DummyWithExtraConstructor::class, $dummy); $this->assertSame('foo', $dummy->foo); $this->assertSame('extraData', $dummy->extra); } - /** - * @expectedException \Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException - * @expectedExceptionMessage Could not create object of class "Symfony\Component\Serializer\Tests\Instantiator\DummyBar" of the parameter "bar". - */ public function testInstantiateWithDenormalizationAndDenormalizer() { $instantiator = new Instantiator(); $data = ['foo' => 'foo', 'bar' => ['baz' => 'baz']]; + $context = []; - $dummy = $instantiator->instantiate(DummyWithObjectArgument::class, $data); + $dummyResult = $instantiator->instantiate(DummyWithObjectArgument::class, $data, $context); - $this->assertInstanceOf(DummyWithExtraConstructor::class, $dummy); - $this->assertSame('foo', $dummy->foo); - $this->assertSame('extraData', $dummy->extra); + $this->assertNull($dummyResult->getObject()); + $this->assertEquals('Could not create object of class "Symfony\\Component\\Serializer\\Tests\\Instantiator\\DummyBar" of the parameter "bar".', $dummyResult->getError()); } public function testInstantiateWithDenormalization() @@ -80,8 +81,10 @@ public function testInstantiateWithDenormalization() $instantiator->setDenormalizer(new ObjectNormalizer()); $data = ['foo' => 'foo', 'bar' => ['baz' => 'baz']]; + $context = []; - $dummy = $instantiator->instantiate(DummyWithObjectArgument::class, $data); + $dummyResult = $instantiator->instantiate(DummyWithObjectArgument::class, $data, $context); + $dummy = $dummyResult->getObject(); $this->assertInstanceOf(DummyWithObjectArgument::class, $dummy); $this->assertSame('foo', $dummy->foo); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php index 2a029b6db5a6b..07d313dfa9279 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php @@ -16,7 +16,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\IgnoreDummy; use Symfony\Component\Serializer\Tests\Fixtures\NullableConstructorArgumentDummy; use Symfony\Component\Serializer\Tests\Fixtures\StaticConstructorDummy; -use Symfony\Component\Serializer\Tests\Fixtures\StaticConstructorNormalizer; +use Symfony\Component\Serializer\Tests\Fixtures\StaticConstructorInstantiator; use Symfony\Component\Serializer\Tests\Fixtures\VariadicConstructorTypedArgsDummy; /** @@ -106,7 +106,8 @@ public function testGetAllowedAttributesAsObjects() public function testObjectWithStaticConstructor() { - $normalizer = new StaticConstructorNormalizer(); + $instantiator = new StaticConstructorInstantiator(); + $normalizer = new ObjectNormalizer(null, null, null, null, null, null, [], null, $instantiator); $dummy = $normalizer->denormalize(['foo' => 'baz'], StaticConstructorDummy::class); $this->assertInstanceOf(StaticConstructorDummy::class, $dummy); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index fd400b9cbfe7f..f408974e1832b 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -47,17 +47,6 @@ public function testDenormalize() $this->assertSame('baz', $normalizedData->baz); } - public function testInstantiateObjectDenormalizer() - { - $data = ['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz']; - $class = Dummy::class; - $context = []; - - $normalizer = new AbstractObjectNormalizerDummy(); - - $this->assertInstanceOf(Dummy::class, $normalizer->instantiateObject($data, $class, $context, new \ReflectionClass($class), [])); - } - public function testDenormalizeWithExtraAttributes() { $this->expectException('Symfony\Component\Serializer\Exception\ExtraAttributesException'); @@ -275,18 +264,18 @@ protected function getAttributeValue(object $object, string $attribute, string $ protected function setAttributeValue(object $object, string $attribute, $value, string $format = null, array $context = []) { - $object->$attribute = $value; + $reflClass = new \ReflectionClass($object); + $reflProp = $reflClass->getProperty($attribute); + + if ($reflProp->isPublic()) { + $object->$attribute = $value; + } } protected function isAllowedAttribute($classOrObject, string $attribute, string $format = null, array $context = []): bool { return \in_array($attribute, ['foo', 'baz', 'quux', 'value']); } - - public function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null): object - { - return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes, $format); - } } class Dummy diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index 8bb4ed221aa1c..5893e155f520d 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -213,11 +213,9 @@ public function testConstructorWithObjectTypeHintDenormalize() ], ]; - $normalizer = new ObjectNormalizer(); - $serializer = new Serializer([$normalizer]); - $normalizer->setSerializer($serializer); + $serializer = new Serializer([new ObjectNormalizer()]); - $obj = $normalizer->denormalize($data, DummyWithConstructorObject::class); + $obj = $serializer->denormalize($data, DummyWithConstructorObject::class); $this->assertInstanceOf(DummyWithConstructorObject::class, $obj); $this->assertEquals(10, $obj->getId()); $this->assertInstanceOf(ObjectInner::class, $obj->getInner());