From 7cf6937d0a18ec7b3734055ec9e02c74a4ffbecc Mon Sep 17 00:00:00 2001 From: Pavel Batanov Date: Fri, 30 Apr 2021 16:24:38 +0300 Subject: [PATCH 1/2] Check data can be casted before actual casting --- src/AbstractVisitor.php | 47 +++++++++++++++++ src/Exception/NonCastableTypeException.php | 37 ++++++++++++++ .../NonFloatCastableTypeException.php | 16 ++++++ src/Exception/NonIntCastableTypeException.php | 16 ++++++ .../NonStringCastableTypeException.php | 16 ++++++ src/JsonDeserializationVisitor.php | 6 +++ src/XmlDeserializationVisitor.php | 8 +++ .../Deserializer/BaseDeserializationTest.php | 50 ++++++++++++++++--- 8 files changed, 188 insertions(+), 8 deletions(-) create mode 100644 src/Exception/NonCastableTypeException.php create mode 100644 src/Exception/NonFloatCastableTypeException.php create mode 100644 src/Exception/NonIntCastableTypeException.php create mode 100644 src/Exception/NonStringCastableTypeException.php diff --git a/src/AbstractVisitor.php b/src/AbstractVisitor.php index 76e00bc7e..e56906b77 100644 --- a/src/AbstractVisitor.php +++ b/src/AbstractVisitor.php @@ -4,6 +4,10 @@ namespace JMS\Serializer; +use JMS\Serializer\Exception\NonFloatCastableTypeException; +use JMS\Serializer\Exception\NonIntCastableTypeException; +use JMS\Serializer\Exception\NonStringCastableTypeException; + /** * @internal */ @@ -39,4 +43,47 @@ protected function getElementType(array $typeArray): ?array return $typeArray['params'][0]; } } + + /** + * logic according to strval https://www.php.net/manual/en/function.strval.php + * "You cannot use strval() on arrays or on objects that do not implement the __toString() method." + * + * @param mixed $value + */ + protected function assertValueCanBeCastToString($value): void + { + if (is_array($value)) { + throw new NonStringCastableTypeException($value); + } + + if (is_object($value) && !method_exists($value, '__toString')) { + throw new NonStringCastableTypeException($value); + } + } + + /** + * logic according to intval https://www.php.net/manual/en/function.intval.php + * "intval() should not be used on objects, as doing so will emit an E_NOTICE level error and return 1." + * + * @param mixed $value + */ + protected function assertValueCanBeCastToInt($value): void + { + if (is_object($value) && !$value instanceof \SimpleXMLElement) { + throw new NonIntCastableTypeException($value); + } + } + + /** + * logic according to floatval https://www.php.net/manual/en/function.floatval.php + * "floatval() should not be used on objects, as doing so will emit an E_NOTICE level error and return 1." + * + * @param mixed $value + */ + protected function assertValueCanCastToFloat($value): void + { + if (is_object($value) && !$value instanceof \SimpleXMLElement) { + throw new NonFloatCastableTypeException($value); + } + } } diff --git a/src/Exception/NonCastableTypeException.php b/src/Exception/NonCastableTypeException.php new file mode 100644 index 000000000..b1815898b --- /dev/null +++ b/src/Exception/NonCastableTypeException.php @@ -0,0 +1,37 @@ +value = $value; + + parent::__construct( + sprintf( + 'Cannot convert value of type "%s" to %s', + gettype($value), + $expectedType + ) + ); + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } +} diff --git a/src/Exception/NonFloatCastableTypeException.php b/src/Exception/NonFloatCastableTypeException.php new file mode 100644 index 000000000..4a749d875 --- /dev/null +++ b/src/Exception/NonFloatCastableTypeException.php @@ -0,0 +1,16 @@ +assertValueCanBeCastToString($data); + return (string) $data; } @@ -71,6 +73,8 @@ public function visitBoolean($data, array $type): bool */ public function visitInteger($data, array $type): int { + $this->assertValueCanBeCastToInt($data); + return (int) $data; } @@ -79,6 +83,8 @@ public function visitInteger($data, array $type): int */ public function visitDouble($data, array $type): float { + $this->assertValueCanCastToFloat($data); + return (float) $data; } diff --git a/src/XmlDeserializationVisitor.php b/src/XmlDeserializationVisitor.php index a14f0835e..b48ef2502 100644 --- a/src/XmlDeserializationVisitor.php +++ b/src/XmlDeserializationVisitor.php @@ -128,6 +128,8 @@ public function visitNull($data, array $type) */ public function visitString($data, array $type): string { + $this->assertValueCanBeCastToString($data); + return (string) $data; } @@ -136,6 +138,8 @@ public function visitString($data, array $type): string */ public function visitBoolean($data, array $type): bool { + $this->assertValueCanBeCastToString($data); + $data = (string) $data; if ('true' === $data || '1' === $data) { @@ -152,6 +156,8 @@ public function visitBoolean($data, array $type): bool */ public function visitInteger($data, array $type): int { + $this->assertValueCanBeCastToInt($data); + return (int) $data; } @@ -160,6 +166,8 @@ public function visitInteger($data, array $type): int */ public function visitDouble($data, array $type): float { + $this->assertValueCanCastToFloat($data); + return (float) $data; } diff --git a/tests/Deserializer/BaseDeserializationTest.php b/tests/Deserializer/BaseDeserializationTest.php index b059f57dc..8fb853a6b 100644 --- a/tests/Deserializer/BaseDeserializationTest.php +++ b/tests/Deserializer/BaseDeserializationTest.php @@ -5,12 +5,46 @@ namespace JMS\Serializer\Tests\Deserializer; use JMS\Serializer\DeserializationContext; +use JMS\Serializer\Exception\NonCastableTypeException; use JMS\Serializer\SerializerBuilder; +use JMS\Serializer\Tests\Fixtures\Discriminator\Car; use JMS\Serializer\Tests\Fixtures\GroupsObject; +use JMS\Serializer\Tests\Fixtures\Price; +use JMS\Serializer\Tests\Fixtures\Publisher; use PHPUnit\Framework\TestCase; class BaseDeserializationTest extends TestCase { + /** + * @dataProvider dataTypeCannotBeCasted + */ + public function testDeserializationInvalidDataCausesException($data, string $type): void + { + $serializer = SerializerBuilder::create()->build(); + + $this->expectException(NonCastableTypeException::class); + + $serializer->fromArray($data, $type); + } + + public function dataTypeCannotBeCasted(): iterable + { + yield 'array to string' => [ + ['pub_name' => ['bla', 'bla']], + Publisher::class, + ]; + + yield 'object to float' => [ + ['price' => (object) ['bla' => 'bla']], + Price::class, + ]; + + yield 'object to int' => [ + ['km' => (object) ['bla' => 'bla']], + Car::class, + ]; + } + /** * @dataProvider dataDeserializerGroupExclusion */ @@ -25,17 +59,17 @@ public function testDeserializerGroupExclusion(array $data, array $groups, array public function dataDeserializerGroupExclusion(): iterable { $data = [ - 'foo' => 'foo', + 'foo' => 'foo', 'foobar' => 'foobar', - 'bar' => 'bar', - 'none' => 'none', + 'bar' => 'bar', + 'none' => 'none', ]; yield [ $data, ['Default'], [ - 'bar' => 'bar', + 'bar' => 'bar', 'none' => 'none', ], ]; @@ -44,7 +78,7 @@ public function dataDeserializerGroupExclusion(): iterable $data, ['foo'], [ - 'foo' => 'foo', + 'foo' => 'foo', 'foobar' => 'foobar', ], ]; @@ -54,7 +88,7 @@ public function dataDeserializerGroupExclusion(): iterable ['bar'], [ 'foobar' => 'foobar', - 'bar' => 'bar', + 'bar' => 'bar', ], ]; @@ -62,9 +96,9 @@ public function dataDeserializerGroupExclusion(): iterable $data, ['foo', 'bar'], [ - 'foo' => 'foo', + 'foo' => 'foo', 'foobar' => 'foobar', - 'bar' => 'bar', + 'bar' => 'bar', ], ]; From 667f2c043fcd2f80d3d9c0063ff4638fe332c390 Mon Sep 17 00:00:00 2001 From: Pavel Batanov Date: Sun, 2 May 2021 23:51:49 +0300 Subject: [PATCH 2/2] Tune class hierarchy --- src/Exception/NonCastableTypeException.php | 2 +- src/Exception/NonFloatCastableTypeException.php | 2 +- src/Exception/NonIntCastableTypeException.php | 2 +- src/Exception/NonStringCastableTypeException.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Exception/NonCastableTypeException.php b/src/Exception/NonCastableTypeException.php index b1815898b..97d0be2a7 100644 --- a/src/Exception/NonCastableTypeException.php +++ b/src/Exception/NonCastableTypeException.php @@ -4,7 +4,7 @@ namespace JMS\Serializer\Exception; -class NonCastableTypeException extends RuntimeException +abstract class NonCastableTypeException extends RuntimeException { /** * @var mixed diff --git a/src/Exception/NonFloatCastableTypeException.php b/src/Exception/NonFloatCastableTypeException.php index 4a749d875..739f276a7 100644 --- a/src/Exception/NonFloatCastableTypeException.php +++ b/src/Exception/NonFloatCastableTypeException.php @@ -4,7 +4,7 @@ namespace JMS\Serializer\Exception; -class NonFloatCastableTypeException extends NonCastableTypeException +final class NonFloatCastableTypeException extends NonCastableTypeException { /** * @param mixed $value diff --git a/src/Exception/NonIntCastableTypeException.php b/src/Exception/NonIntCastableTypeException.php index 25b090f26..a7178a012 100644 --- a/src/Exception/NonIntCastableTypeException.php +++ b/src/Exception/NonIntCastableTypeException.php @@ -4,7 +4,7 @@ namespace JMS\Serializer\Exception; -class NonIntCastableTypeException extends NonCastableTypeException +final class NonIntCastableTypeException extends NonCastableTypeException { /** * @param mixed $value diff --git a/src/Exception/NonStringCastableTypeException.php b/src/Exception/NonStringCastableTypeException.php index 311f39cfa..5a04b887e 100644 --- a/src/Exception/NonStringCastableTypeException.php +++ b/src/Exception/NonStringCastableTypeException.php @@ -4,7 +4,7 @@ namespace JMS\Serializer\Exception; -class NonStringCastableTypeException extends NonCastableTypeException +final class NonStringCastableTypeException extends NonCastableTypeException { /** * @param mixed $value