diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index 562f9c3d9..e5b89b313 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -16,6 +16,7 @@ jobs: php-version: - "7.2" - "8.0" + - "8.1" steps: - name: Checkout code diff --git a/.github/workflows/static-analysis.yaml b/.github/workflows/static-analysis.yaml index 3df7a81dc..921b360ba 100644 --- a/.github/workflows/static-analysis.yaml +++ b/.github/workflows/static-analysis.yaml @@ -33,7 +33,8 @@ jobs: - name: "Run a static analysis with phpstan/phpstan" run: "vendor/bin/phpstan analyse -c phpstan-7-4.neon.dist --error-format=checkstyle | cs2pr" - static-analysis-phpstan: + + static-analysis-phpstan-8-0: name: "Static Analysis with PHPStan" runs-on: "ubuntu-20.04" @@ -41,6 +42,32 @@ jobs: matrix: php-version: - "8.0" + + steps: + - name: "Checkout code" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + tools: "cs2pr" + extensions: pdo_sqlite + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v1" + + - name: "Run a static analysis with phpstan/phpstan" + run: "vendor/bin/phpstan analyse -c phpstan-8-0.neon.dist --error-format=checkstyle | cs2pr" + + static-analysis-phpstan-8-1: + name: "Static Analysis with PHPStan" + runs-on: "ubuntu-20.04" + + strategy: + matrix: + php-version: - "8.1" include: - { php-version: '8.2', composer-options: '--ignore-platform-req=php' } diff --git a/phpstan-7-4.neon.dist b/phpstan-7-4.neon.dist index ec226c59d..8c4c3f59d 100644 --- a/phpstan-7-4.neon.dist +++ b/phpstan-7-4.neon.dist @@ -13,6 +13,7 @@ parameters: - %currentWorkingDirectory%/tests excludePaths: - %currentWorkingDirectory%/tests/Fixtures/TypedProperties/UnionTypedProperties.php + - %currentWorkingDirectory%/tests/Fixtures/TypedProperties/ConstructorPromotion/Vase.php - %currentWorkingDirectory%/tests/Fixtures/TypedProperties/UnionTypedProperties.php - %currentWorkingDirectory%/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php - %currentWorkingDirectory%/tests/Serializer/BaseSerializationTest.php diff --git a/phpstan-8-0.neon.dist b/phpstan-8-0.neon.dist new file mode 100644 index 000000000..2d08cd164 --- /dev/null +++ b/phpstan-8-0.neon.dist @@ -0,0 +1,14 @@ +parameters: + level: 1 + + ignoreErrors: + - '~Class Doctrine\\Common\\Persistence\\Proxy not found~' + - '~Class Doctrine\\ODM\\MongoDB\\PersistentCollection not found~' + - '~Class Symfony\\(Contracts|Component)\\Translation\\TranslatorInterface not found~' + - '#Constructor of class JMS\\Serializer\\Annotation\\.*? has an unused parameter#' + - '#Class JMS\\Serializer\\Annotation\\DeprecatedReadOnly extends @final class JMS\\Serializer\\Annotation\\ReadOnlyProperty.#' + + paths: + - %currentWorkingDirectory%/src + - %currentWorkingDirectory%/tests + - %currentWorkingDirectory%/tests/Fixtures/TypedProperties/ConstructorPromotion/Vase.php diff --git a/src/Builder/DefaultDriverFactory.php b/src/Builder/DefaultDriverFactory.php index e0240b6ba..49081dd24 100644 --- a/src/Builder/DefaultDriverFactory.php +++ b/src/Builder/DefaultDriverFactory.php @@ -8,6 +8,7 @@ use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface; use JMS\Serializer\Metadata\Driver\AnnotationDriver; use JMS\Serializer\Metadata\Driver\AttributeDriver; +use JMS\Serializer\Metadata\Driver\DefaultValuePropertyDriver; use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver; use JMS\Serializer\Metadata\Driver\XmlDriver; use JMS\Serializer\Metadata\Driver\YamlDriver; @@ -63,6 +64,10 @@ public function createDriver(array $metadataDirs, Reader $annotationReader): Dri $driver = new TypedPropertiesDriver($driver, $this->typeParser); } + if (PHP_VERSION_ID >= 80000) { + $driver = new DefaultValuePropertyDriver($driver); + } + return $driver; } } diff --git a/src/GraphNavigator/DeserializationGraphNavigator.php b/src/GraphNavigator/DeserializationGraphNavigator.php index ab8c9bace..eaf236240 100644 --- a/src/GraphNavigator/DeserializationGraphNavigator.php +++ b/src/GraphNavigator/DeserializationGraphNavigator.php @@ -214,6 +214,9 @@ public function accept($data, ?array $type = null) $v = $this->visitor->visitProperty($propertyMetadata, $data); $this->accessor->setValue($object, $v, $propertyMetadata, $this->context); } catch (NotAcceptableException $e) { + if (true === $propertyMetadata->hasDefault) { + $this->accessor->setValue($object, $propertyMetadata->defaultValue, $propertyMetadata, $this->context); + } } $this->context->popPropertyMetadata(); diff --git a/src/Metadata/Driver/DefaultValuePropertyDriver.php b/src/Metadata/Driver/DefaultValuePropertyDriver.php new file mode 100644 index 000000000..4dedec2e9 --- /dev/null +++ b/src/Metadata/Driver/DefaultValuePropertyDriver.php @@ -0,0 +1,80 @@ +delegate = $delegate; + } + + public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata + { + $classMetadata = $this->delegate->loadMetadataForClass($class); + \assert($classMetadata instanceof SerializerClassMetadata); + + if (null === $classMetadata) { + return null; + } + + foreach ($classMetadata->propertyMetadata as $key => $propertyMetadata) { + \assert($propertyMetadata instanceof PropertyMetadata); + if (null !== $propertyMetadata->hasDefault) { + continue; + } + + try { + $propertyReflection = $this->getPropertyReflection($propertyMetadata); + $propertyMetadata->hasDefault = false; + if ($propertyReflection->hasDefaultValue() && $propertyReflection->hasType()) { + $propertyMetadata->hasDefault = true; + $propertyMetadata->defaultValue = $propertyReflection->getDefaultValue(); + } elseif ($propertyReflection->isPromoted()) { + // need to get the parameter in the constructor to check for default values + $classReflection = $this->getClassReflection($propertyMetadata); + $params = $classReflection->getConstructor()->getParameters(); + foreach ($params as $parameter) { + if ($parameter->getName() === $propertyMetadata->name) { + if ($parameter->isDefaultValueAvailable()) { + $propertyMetadata->hasDefault = true; + $propertyMetadata->defaultValue = $parameter->getDefaultValue(); + } + + break; + } + } + } + } catch (ReflectionException $e) { + continue; + } + } + + return $classMetadata; + } + + private function getPropertyReflection(PropertyMetadata $propertyMetadata): ReflectionProperty + { + return new ReflectionProperty($propertyMetadata->class, $propertyMetadata->name); + } + + private function getClassReflection(PropertyMetadata $propertyMetadata): ReflectionClass + { + return new ReflectionClass($propertyMetadata->class); + } +} diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index b945f8269..ff41c949f 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -133,6 +133,16 @@ class PropertyMetadata extends BasePropertyMetadata */ public $excludeIf = null; + /** + * @var bool|null + */ + public $hasDefault; + + /** + * @var mixed|null + */ + public $defaultValue; + /** * @internal * @@ -235,6 +245,8 @@ protected function serializeToArray(): array $this->excludeIf, $this->skipWhenEmpty, $this->forceReflectionAccess, + $this->hasDefault, + $this->defaultValue, parent::serializeToArray(), ]; } @@ -267,6 +279,8 @@ protected function unserializeFromArray(array $data): void $this->excludeIf, $this->skipWhenEmpty, $this->forceReflectionAccess, + $this->hasDefault, + $this->defaultValue, $parentData, ] = $data; diff --git a/tests/Fixtures/TypedProperties/ConstructorPromotion/Vase.php b/tests/Fixtures/TypedProperties/ConstructorPromotion/Vase.php new file mode 100644 index 000000000..9d8e5c96d --- /dev/null +++ b/tests/Fixtures/TypedProperties/ConstructorPromotion/Vase.php @@ -0,0 +1,16 @@ +markTestSkipped(sprintf('%s requires PHP 8.0', __METHOD__)); + } + + $builder = SerializerBuilder::create($this->handlerRegistry, $this->dispatcher); + $builder->includeInterfaceMetadata(true); + $this->serializer = $builder->build(); + + $vase = new TypedProperties\ConstructorPromotion\Vase('blue'); + $result = $this->serialize($vase); + self::assertEquals($this->getContent('typed_props_constructor_promotion_with_default_values'), $result); + if ($this->hasDeserializer()) { + $deserialized = $this->deserialize($this->getContent('typed_props_constructor_promotion_with_default_values'), get_class($vase)); + self::assertEquals($vase->color, $deserialized->color); + self::assertEquals($vase->plant, $deserialized->plant); + self::assertEquals($vase->typeOfSoil, $deserialized->typeOfSoil); + self::assertEquals($vase->daysSincePotting, $deserialized->daysSincePotting); + } + } + public function testUninitializedTypedProperties() { if (PHP_VERSION_ID < 70400) { diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index c4e684027..a04292b05 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -136,6 +136,7 @@ protected function getContent($key) $outputs['user_discriminator'] = '{"entityName":"User"}'; $outputs['user_discriminator_extended'] = '{"entityName":"ExtendedUser"}'; $outputs['typed_props'] = '{"id":1,"role":{"id":5},"vehicle":{"type":"car"},"created":"2010-10-01T00:00:00+00:00","updated":"2011-10-01T00:00:00+00:00","tags":["a","b"]}'; + $outputs['typed_props_constructor_promotion_with_default_values'] = '{"color":"blue","type_of_soil":"potting mix","days_since_potting":-1}'; $outputs['uninitialized_typed_props'] = '{"id":1,"role":{},"tags":[]}'; $outputs['custom_datetimeinterface'] = '{"custom":"2021-09-07"}'; $outputs['data_integer'] = '{"data":10000}'; diff --git a/tests/Serializer/xml/typed_props_constructor_promotion_with_default_values.xml b/tests/Serializer/xml/typed_props_constructor_promotion_with_default_values.xml new file mode 100644 index 000000000..5a7e90320 --- /dev/null +++ b/tests/Serializer/xml/typed_props_constructor_promotion_with_default_values.xml @@ -0,0 +1,6 @@ + + + + + -1 +