From 17aa47bd2f0ddce2971004f9a90a77af59e2ca38 Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Fri, 24 Apr 2020 08:44:36 +0200 Subject: [PATCH 1/2] infer types from php 7.4 --- src/Builder/DefaultDriverFactory.php | 11 +- src/Metadata/Driver/TypedPropertiesDriver.php | 120 ++++++++++++++++++ tests/Fixtures/TypedProperties/Role.php | 10 ++ tests/Fixtures/TypedProperties/User.php | 23 ++++ .../Driver/DefaultDriverFactoryTest.php | 39 ++++++ .../Driver/TypedPropertiesDriverTest.php | 43 +++++++ tests/Serializer/BaseSerializationTest.php | 29 +++++ tests/Serializer/JsonSerializationTest.php | 1 + tests/Serializer/xml/typed_props.xml | 13 ++ 9 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 src/Metadata/Driver/TypedPropertiesDriver.php create mode 100644 tests/Fixtures/TypedProperties/Role.php create mode 100644 tests/Fixtures/TypedProperties/User.php create mode 100644 tests/Metadata/Driver/DefaultDriverFactoryTest.php create mode 100644 tests/Metadata/Driver/TypedPropertiesDriverTest.php create mode 100644 tests/Serializer/xml/typed_props.xml diff --git a/src/Builder/DefaultDriverFactory.php b/src/Builder/DefaultDriverFactory.php index 2dd75d1db..2e87f3b33 100644 --- a/src/Builder/DefaultDriverFactory.php +++ b/src/Builder/DefaultDriverFactory.php @@ -7,6 +7,7 @@ use Doctrine\Common\Annotations\Reader; use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface; use JMS\Serializer\Metadata\Driver\AnnotationDriver; +use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver; use JMS\Serializer\Metadata\Driver\XmlDriver; use JMS\Serializer\Metadata\Driver\YamlDriver; use JMS\Serializer\Naming\PropertyNamingStrategyInterface; @@ -45,13 +46,19 @@ public function createDriver(array $metadataDirs, Reader $annotationReader): Dri if (!empty($metadataDirs)) { $fileLocator = new FileLocator($metadataDirs); - return new DriverChain([ + $driver = new DriverChain([ new YamlDriver($fileLocator, $this->propertyNamingStrategy, $this->typeParser, $this->expressionEvaluator), new XmlDriver($fileLocator, $this->propertyNamingStrategy, $this->typeParser, $this->expressionEvaluator), new AnnotationDriver($annotationReader, $this->propertyNamingStrategy, $this->typeParser, $this->expressionEvaluator), ]); + } else { + $driver = new AnnotationDriver($annotationReader, $this->propertyNamingStrategy, $this->typeParser); } - return new AnnotationDriver($annotationReader, $this->propertyNamingStrategy, $this->typeParser); + if (PHP_VERSION_ID >= 70400) { + $driver = new TypedPropertiesDriver($driver, $this->typeParser); + } + + return $driver; } } diff --git a/src/Metadata/Driver/TypedPropertiesDriver.php b/src/Metadata/Driver/TypedPropertiesDriver.php new file mode 100644 index 000000000..7d036ec54 --- /dev/null +++ b/src/Metadata/Driver/TypedPropertiesDriver.php @@ -0,0 +1,120 @@ +delegate = $delegate; + $this->typeParser = $typeParser ?: new Parser(); + $this->whiteList = array_merge($whiteList, $this->getDefaultWhiteList()); + } + + private function getDefaultWhiteList(): array + { + return [ + 'int', + 'float', + 'bool', + 'boolean', + 'string', + 'double', + 'iterable', + 'resource', + ]; + } + + public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata + { + /** @var SerializerClassMetadata $classMetadata */ + $classMetadata = $this->delegate->loadMetadataForClass($class); + + if (null === $classMetadata) { + return null; + } + // We base our scan on the internal driver's property list so that we + // respect any internal white/blacklisting like in the AnnotationDriver + foreach ($classMetadata->propertyMetadata as $key => $propertyMetadata) { + /** @var $propertyMetadata PropertyMetadata */ + + // If the inner driver provides a type, don't guess anymore. + if ($propertyMetadata->type || $this->isVirtualProperty($propertyMetadata)) { + continue; + } + + try { + $propertyReflection = $this->getReflection($propertyMetadata); + if ($this->shouldTypeHint($propertyReflection)) { + $propertyMetadata->setType($this->typeParser->parse($propertyReflection->getType()->getName())); + } + } catch (ReflectionException $e) { + continue; + } + } + + return $classMetadata; + } + + private function shouldTypeHint(ReflectionProperty $propertyReflection): bool + { + if (null === $propertyReflection->getType()) { + return false; + } + + if (in_array($propertyReflection->getType()->getName(), $this->whiteList, true)) { + return true; + } + + if (class_exists($propertyReflection->getType()->getName())) { + return true; + } + + return false; + } + + private function getReflection(PropertyMetadata $propertyMetadata): ReflectionProperty + { + return new ReflectionProperty($propertyMetadata->class, $propertyMetadata->name); + } + + private function isVirtualProperty(PropertyMetadata $propertyMetadata): bool + { + return $propertyMetadata instanceof VirtualPropertyMetadata + || $propertyMetadata instanceof StaticPropertyMetadata + || $propertyMetadata instanceof ExpressionPropertyMetadata; + } +} diff --git a/tests/Fixtures/TypedProperties/Role.php b/tests/Fixtures/TypedProperties/Role.php new file mode 100644 index 000000000..3a66b945d --- /dev/null +++ b/tests/Fixtures/TypedProperties/Role.php @@ -0,0 +1,10 @@ +markTestSkipped(sprintf('%s requires PHP 7.4', __METHOD__)); + } + + $factory = new DefaultDriverFactory(new IdenticalPropertyNamingStrategy()); + + $driver = $factory->createDriver([], new AnnotationReader()); + + $m = $driver->loadMetadataForClass(new \ReflectionClass(User::class)); + self::assertNotNull($m); + + $expectedPropertyTypes = [ + 'id' => 'int', + 'role' => 'JMS\Serializer\Tests\Fixtures\TypedProperties\Role', + 'created' => 'DateTime', + 'tags' => 'iterable', + ]; + + foreach ($expectedPropertyTypes as $property => $type) { + self::assertEquals(['name' => $type, 'params' => []], $m->propertyMetadata[$property]->type); + } + } +} diff --git a/tests/Metadata/Driver/TypedPropertiesDriverTest.php b/tests/Metadata/Driver/TypedPropertiesDriverTest.php new file mode 100644 index 000000000..41317ba78 --- /dev/null +++ b/tests/Metadata/Driver/TypedPropertiesDriverTest.php @@ -0,0 +1,43 @@ +markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + } + + public function testInferPropertiesFromTypes() + { + $baseDriver = new AnnotationDriver(new AnnotationReader(), new IdenticalPropertyNamingStrategy()); + $driver = new TypedPropertiesDriver($baseDriver); + + $m = $driver->loadMetadataForClass(new \ReflectionClass(User::class)); + + self::assertNotNull($m); + + $expectedPropertyTypes = [ + 'id' => 'int', + 'role' => 'JMS\Serializer\Tests\Fixtures\TypedProperties\Role', + 'created' => 'DateTime', + 'tags' => 'iterable', + ]; + + foreach ($expectedPropertyTypes as $property => $type) { + self::assertEquals(['name' => $type, 'params' => []], $m->propertyMetadata[$property]->type); + } + } +} diff --git a/tests/Serializer/BaseSerializationTest.php b/tests/Serializer/BaseSerializationTest.php index 6cbd08743..c63c88aeb 100644 --- a/tests/Serializer/BaseSerializationTest.php +++ b/tests/Serializer/BaseSerializationTest.php @@ -108,6 +108,7 @@ use JMS\Serializer\Tests\Fixtures\Tag; use JMS\Serializer\Tests\Fixtures\Timestamp; use JMS\Serializer\Tests\Fixtures\Tree; +use JMS\Serializer\Tests\Fixtures\TypedProperties; use JMS\Serializer\Tests\Fixtures\VehicleInterfaceGarage; use JMS\Serializer\Visitor\DeserializationVisitorInterface; use JMS\Serializer\Visitor\SerializationVisitorInterface; @@ -1289,6 +1290,34 @@ public function testCustomHandler() self::assertEquals('customly_unserialized_value', $object->someProperty); } + public function testTypedProperties() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', __METHOD__)); + } + + $user = new TypedProperties\User(); + $user->id = 1; + $user->created = new \DateTime('2010-10-01 00:00:00'); + $user->updated = new \DateTime('2011-10-01 00:00:00'); + $user->tags = ['a', 'b']; + $role = new TypedProperties\Role(); + $role->id = 5; + $user->role = $role; + + $result = $this->serialize($user); + + self::assertEquals($this->getContent('typed_props'), $result); + + if ($this->hasDeserializer()) { + // updated is read only + $user->updated = null; + $user->tags = []; + + self::assertEquals($user, $this->deserialize($this->getContent('typed_props'), get_class($user))); + } + } + /** * @doesNotPerformAssertions */ diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index 8ffbd832d..bae8d36f8 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -128,6 +128,7 @@ protected function getContent($key) $outputs['user_discriminator_array'] = '[{"entityName":"User"},{"entityName":"ExtendedUser"}]'; $outputs['user_discriminator'] = '{"entityName":"User"}'; $outputs['user_discriminator_extended'] = '{"entityName":"ExtendedUser"}'; + $outputs['typed_props'] = '{"id":1,"role":{"id":5},"created":"2010-10-01T00:00:00+00:00","updated":"2011-10-01T00:00:00+00:00","tags":["a","b"]}'; } if (!isset($outputs[$key])) { diff --git a/tests/Serializer/xml/typed_props.xml b/tests/Serializer/xml/typed_props.xml new file mode 100644 index 000000000..886fd7537 --- /dev/null +++ b/tests/Serializer/xml/typed_props.xml @@ -0,0 +1,13 @@ + + + 1 + + 5 + + + + + + + + From 02431ce3c05dfedcedc828742090a50b51932f5f Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Mon, 4 May 2020 09:35:05 +0200 Subject: [PATCH 2/2] the deserializer should convert the iterable --- src/GraphNavigator/DeserializationGraphNavigator.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/GraphNavigator/DeserializationGraphNavigator.php b/src/GraphNavigator/DeserializationGraphNavigator.php index 4f4021d13..80c128f38 100644 --- a/src/GraphNavigator/DeserializationGraphNavigator.php +++ b/src/GraphNavigator/DeserializationGraphNavigator.php @@ -17,7 +17,6 @@ use JMS\Serializer\Exception\RuntimeException; use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy; use JMS\Serializer\Expression\ExpressionEvaluatorInterface; -use JMS\Serializer\Functions; use JMS\Serializer\GraphNavigator; use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\Handler\HandlerRegistryInterface; @@ -134,7 +133,7 @@ public function accept($data, ?array $type = null) return $this->visitor->visitDouble($data, $type); case 'iterable': - return $this->visitor->visitArray(Functions::iterableToArray($data), $type); + return $this->visitor->visitArray($data, $type); case 'array': return $this->visitor->visitArray($data, $type);