diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index 596977094..e2857c318 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -9,7 +9,7 @@ on: jobs: phpbench: name: "PHPBench" - runs-on: "ubuntu-20.04" + runs-on: ubuntu-latest strategy: matrix: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f2ced2dc1..aacbc654f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ on: jobs: phpunit: name: "PHPUnit" - runs-on: "ubuntu-20.04" + runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/coding-standards.yaml b/.github/workflows/coding-standards.yaml index 70b31f510..204ec4293 100644 --- a/.github/workflows/coding-standards.yaml +++ b/.github/workflows/coding-standards.yaml @@ -9,7 +9,7 @@ on: jobs: coding-standards: name: "Coding Standards" - runs-on: "ubuntu-20.04" + runs-on: ubuntu-latest strategy: matrix: diff --git a/.github/workflows/static-analysis.yaml b/.github/workflows/static-analysis.yaml index 44016b465..74eb5393d 100644 --- a/.github/workflows/static-analysis.yaml +++ b/.github/workflows/static-analysis.yaml @@ -9,7 +9,7 @@ on: jobs: static-analysis-phpstan: name: "Static Analysis with PHPStan" - runs-on: "ubuntu-20.04" + runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index f5f9ffc2e..9d6c24992 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -4,9 +4,9 @@ namespace JMS\Serializer\Handler; -use JMS\Serializer\Context; use JMS\Serializer\DeserializationContext; use JMS\Serializer\Exception\NonVisitableTypeException; +use JMS\Serializer\Exception\NotAcceptableException; use JMS\Serializer\Exception\RuntimeException; use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\SerializationContext; @@ -50,15 +50,18 @@ public function serializeUnion( SerializationContext $context ): mixed { if ($this->isPrimitiveType(gettype($data))) { - return $this->matchSimpleType($data, $type, $context); + $resolvedType = [ + 'name' => gettype($data), + 'params' => [], + ]; } else { $resolvedType = [ 'name' => get_class($data), 'params' => [], ]; - - return $context->getNavigator()->accept($data, $resolvedType); } + + return $context->getNavigator()->accept($data, $resolvedType); } public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed $data, array $type, DeserializationContext $context): mixed @@ -87,30 +90,27 @@ public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed return $context->getNavigator()->accept($data, $finalType); } - foreach ($type['params'][0] as $possibleType) { - if ($this->isPrimitiveType($possibleType['name']) && $this->testPrimitive($data, $possibleType['name'], $context->getFormat())) { - return $context->getNavigator()->accept($data, $possibleType); - } - } + $dataType = gettype($data); - return null; - } + if ( + array_filter( + $type['params'][0], + static fn (array $type): bool => $type['name'] === $dataType || (isset(self::$aliases[$dataType]) && $type['name'] === self::$aliases[$dataType]), + ) + ) { + return $context->getNavigator()->accept($data, [ + 'name' => $dataType, + 'params' => [], + ]); + } - private function matchSimpleType(mixed $data, array $type, Context $context): mixed - { foreach ($type['params'][0] as $possibleType) { - if ($this->isPrimitiveType($possibleType['name']) && !$this->testPrimitive($data, $possibleType['name'], $context->getFormat())) { - continue; - } - - try { + if ($this->isPrimitiveType($possibleType['name']) && $this->testPrimitive($data, $possibleType['name'])) { return $context->getNavigator()->accept($data, $possibleType); - } catch (NonVisitableTypeException $e) { - continue; } } - return null; + throw new NotAcceptableException(); } private function isPrimitiveType(string $type): bool @@ -118,7 +118,7 @@ private function isPrimitiveType(string $type): bool return in_array($type, ['int', 'integer', 'float', 'double', 'bool', 'boolean', 'true', 'false', 'string', 'array'], true); } - private function testPrimitive(mixed $data, string $type, string $format): bool + private function testPrimitive(mixed $data, string $type): bool { switch ($type) { case 'array': @@ -143,7 +143,7 @@ private function testPrimitive(mixed $data, string $type, string $format): bool return false === $data; case 'string': - return is_string($data); + return !is_array($data) && !is_object($data); } return false; diff --git a/tests/Fixtures/TypedProperties/BoolOrString.php b/tests/Fixtures/TypedProperties/BoolOrString.php new file mode 100644 index 000000000..c692f4994 --- /dev/null +++ b/tests/Fixtures/TypedProperties/BoolOrString.php @@ -0,0 +1,15 @@ +data = $data; + } +} diff --git a/tests/Fixtures/TypedProperties/FalseOrString.php b/tests/Fixtures/TypedProperties/FalseOrString.php new file mode 100644 index 000000000..b9b25af40 --- /dev/null +++ b/tests/Fixtures/TypedProperties/FalseOrString.php @@ -0,0 +1,15 @@ +data = $data; + } +} diff --git a/tests/Fixtures/TypedProperties/UnionTypedProperties.php b/tests/Fixtures/TypedProperties/UnionTypedProperties.php index c3f26c8cb..9ec0ff480 100644 --- a/tests/Fixtures/TypedProperties/UnionTypedProperties.php +++ b/tests/Fixtures/TypedProperties/UnionTypedProperties.php @@ -6,7 +6,7 @@ class UnionTypedProperties { - private int|bool|float|string|array $data; + public bool|float|string|array|int $data; private int|bool|float|string|null $nullableData = null; diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index dce450cc7..353959467 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -24,7 +24,9 @@ use JMS\Serializer\Tests\Fixtures\ObjectWithInlineArray; use JMS\Serializer\Tests\Fixtures\ObjectWithObjectProperty; use JMS\Serializer\Tests\Fixtures\Tag; +use JMS\Serializer\Tests\Fixtures\TypedProperties\BoolOrString; use JMS\Serializer\Tests\Fixtures\TypedProperties\ComplexDiscriminatedUnion; +use JMS\Serializer\Tests\Fixtures\TypedProperties\FalseOrString; use JMS\Serializer\Tests\Fixtures\TypedProperties\UnionTypedProperties; use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory; use JMS\Serializer\Visitor\SerializationVisitorInterface; @@ -151,9 +153,12 @@ protected static function getContent($key) $outputs['uninitialized_typed_props'] = '{"virtual_role":{},"id":1,"role":{},"tags":[]}'; $outputs['custom_datetimeinterface'] = '{"custom":"2021-09-07"}'; $outputs['data_integer'] = '{"data":10000}'; + $outputs['data_integer_one'] = '{"data":1}'; $outputs['data_float'] = '{"data":1.236}'; $outputs['data_bool'] = '{"data":false}'; $outputs['data_string'] = '{"data":"foo"}'; + $outputs['data_string_empty'] = '{"data":""}'; + $outputs['data_string_zero'] = '{"data":"0"}'; $outputs['data_array'] = '{"data":[1,2,3]}'; $outputs['data_true'] = '{"data":true}'; $outputs['data_false'] = '{"data":false}'; @@ -452,12 +457,16 @@ public static function getTypeHintedArraysAndStdClass() public static function getSimpleUnionProperties(): iterable { - yield 'int' => [[10000, null, false], 'union_typed_properties_integer']; - yield 'float' => [[1.236, null, false], 'union_typed_properties_float']; - yield 'bool' => [[false, null, false], 'union_typed_properties_bool']; - yield 'string' => [['foo', null, false], 'union_typed_properties_string']; - yield 'array' => [[[1, 2, 3], null, false], 'union_typed_properties_array']; - yield 'false_array' => [[false, null, 'foo'], 'union_typed_properties_false_string']; + yield [10000, 'data_integer']; + yield [1.236, 'data_float']; + yield [false, 'data_bool']; + yield ['foo', 'data_string']; + yield [[1, 2, 3], 'data_array']; + yield [1, 'data_integer_one']; + yield ['0', 'data_string_zero']; + yield ['', 'data_string_empty']; + yield [true, 'data_true']; + yield [false, 'data_false']; } /** @@ -472,9 +481,57 @@ public function testUnionProperties($data, string $expected): void return; } - $object = new UnionTypedProperties(...$data); - self::assertEquals($object, $this->deserialize(static::getContent($expected), UnionTypedProperties::class)); - self::assertEquals($this->serialize($object), static::getContent($expected)); + $deserialized = $this->deserialize(static::getContent($expected), UnionTypedProperties::class); + + self::assertSame($data, $deserialized->data); + self::assertSame($this->serialize($deserialized), static::getContent($expected)); + } + + public static function getUnionCastableTypes(): iterable + { + yield ['10000', 'data_integer']; + yield ['1.236', 'data_float']; + yield [true, 'data_integer_one']; + } + + /** + * @dataProvider getUnionCastableTypes + */ + #[DataProvider('getUnionCastableTypes')] + public function testUnionPropertiesWithCastableType($data, string $expected) + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + $deserialized = $this->deserialize(static::getContent($expected), BoolOrString::class); + + self::assertSame($data, $deserialized->data); + } + + public static function getUnionNotCastableTypes(): iterable + { + yield ['data_array']; + } + + /** + * @dataProvider getUnionNotCastableTypes + */ + #[DataProvider('getUnionNotCastableTypes')] + public function testUnionPropertiesWithNotCastableType(string $expected) + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + $deserialized = $this->deserialize(static::getContent($expected), BoolOrString::class); + + $this->expectException(\Error::class); + $deserialized->data; } public function testTrueDataType() @@ -489,7 +546,6 @@ public function testTrueDataType() static::getContent('data_true'), $this->serialize(new DataTrue(true)), ); - self::assertEquals( new DataTrue(true), $this->deserialize(static::getContent('data_true'), DataTrue::class), @@ -517,6 +573,16 @@ public function testFalseDataType() $this->deserialize(static::getContent('data_false'), DataFalse::class), ); + self::assertEquals( + static::getContent('data_false'), + $this->serialize(new FalseOrString(false)), + ); + + self::assertEquals( + new FalseOrString(false), + $this->deserialize(static::getContent('data_false'), FalseOrString::class), + ); + $this->expectException(TypeError::class); $this->deserialize(static::getContent('data_true'), DataFalse::class); } diff --git a/tests/Serializer/JsonStrictSerializationTest.php b/tests/Serializer/JsonStrictSerializationTest.php index c0227fd28..6bdb7f712 100644 --- a/tests/Serializer/JsonStrictSerializationTest.php +++ b/tests/Serializer/JsonStrictSerializationTest.php @@ -4,6 +4,7 @@ namespace JMS\Serializer\Tests\Serializer; +use JMS\Serializer\Exception\NonVisitableTypeException; use JMS\Serializer\SerializerBuilder; use JMS\Serializer\Visitor\Factory\JsonDeserializationVisitorFactory; use PHPUnit\Framework\Attributes\DataProvider; @@ -25,4 +26,15 @@ public function testFirstClassMapCollections(array $items, string $expected): vo { self::markTestIncomplete('Fixtures are broken'); } + + /** + * @dataProvider getUnionCastableTypes + */ + #[DataProvider('getUnionCastableTypes')] + public function testUnionPropertiesWithCastableType($data, string $expected): void + { + $this->expectException(NonVisitableTypeException::class); + + parent::testUnionPropertiesWithCastableType($data, $expected); + } } diff --git a/tests/Serializer/XmlSerializationTest.php b/tests/Serializer/XmlSerializationTest.php index 11a3da373..464790aba 100644 --- a/tests/Serializer/XmlSerializationTest.php +++ b/tests/Serializer/XmlSerializationTest.php @@ -50,7 +50,6 @@ use JMS\Serializer\XmlSerializationVisitor; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; -use PHPUnit\Framework\Attributes\Group; class XmlSerializationTest extends BaseSerializationTestCase {