From bbd1ae4fbd9b1fac0044de26b364709b9ef4ed0d Mon Sep 17 00:00:00 2001 From: jewome62 Date: Sat, 6 Apr 2019 16:35:52 +0200 Subject: [PATCH] WIP --- .../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