From 4a665acc39127aa054fd23490a52e7261030a022 Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Mon, 4 Dec 2023 02:41:30 +0300 Subject: [PATCH] [PropertyAccess][Serializer] Fix "type unknown" on denormalize --- .../RequestPayloadValueResolverTest.php | 4 +-- .../Component/HttpKernel/composer.json | 4 +-- .../Exception/InvalidTypeException.php | 32 +++++++++++++++++++ .../PropertyAccess/PropertyAccessor.php | 6 ++-- .../Normalizer/AbstractObjectNormalizer.php | 3 +- .../Tests/Normalizer/ObjectNormalizerTest.php | 20 ++++++++++++ 6 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 src/Symfony/Component/PropertyAccess/Exception/InvalidTypeException.php diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php index 326551d87b57e..d5939dbfa83bd 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php @@ -253,7 +253,7 @@ public function testValidationNotPassed() $validationFailedException = $e->getPrevious(); $this->assertSame(422, $e->getStatusCode()); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); - $this->assertSame('This value should be of type unknown.', $validationFailedException->getViolations()[0]->getMessage()); + $this->assertSame('This value should be of type string.', $validationFailedException->getViolations()[0]->getMessage()); $this->assertSame('Test', $validationFailedException->getViolations()[1]->getMessage()); } } @@ -665,7 +665,7 @@ public function testRequestPayloadValidationErrorCustomStatusCode() $validationFailedException = $e->getPrevious(); $this->assertSame(400, $e->getStatusCode()); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); - $this->assertSame('This value should be of type unknown.', $validationFailedException->getViolations()[0]->getMessage()); + $this->assertSame('This value should be of type string.', $validationFailedException->getViolations()[0]->getMessage()); $this->assertSame('Test', $validationFailedException->getViolations()[1]->getMessage()); } } diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index 09ac8fe4c162d..62d5f3eec7a56 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -35,9 +35,9 @@ "symfony/finder": "^6.4|^7.0", "symfony/http-client-contracts": "^2.5|^3", "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", + "symfony/property-access": "^7.1", "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/serializer": "^7.1", "symfony/stopwatch": "^6.4|^7.0", "symfony/translation": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", diff --git a/src/Symfony/Component/PropertyAccess/Exception/InvalidTypeException.php b/src/Symfony/Component/PropertyAccess/Exception/InvalidTypeException.php new file mode 100644 index 0000000000000..f659ffd07e6da --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Exception/InvalidTypeException.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when a type of given value does not match an expected type. + * + * @author Farhad Safarov + */ +class InvalidTypeException extends InvalidArgumentException +{ + public function __construct( + public readonly string $expectedType, + public readonly string $actualType, + public readonly string $propertyPath, + \Throwable $previous = null, + ) { + parent::__construct( + sprintf('Expected argument of type "%s", "%s" given at property path "%s".', $expectedType, 'NULL' === $actualType ? 'null' : $actualType, $propertyPath), + previous: $previous, + ); + } +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index a763f3ae08344..8393a332459a0 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -18,7 +18,7 @@ use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\PropertyAccess\Exception\AccessException; -use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException; +use Symfony\Component\PropertyAccess\Exception\InvalidTypeException; use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; @@ -192,12 +192,12 @@ private static function throwInvalidArgumentException(string $message, array $tr if (preg_match('/^\S+::\S+\(\): Argument #\d+ \(\$\S+\) must be of type (\S+), (\S+) given/', $message, $matches)) { [, $expectedType, $actualType] = $matches; - throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".', $expectedType, 'NULL' === $actualType ? 'null' : $actualType, $propertyPath), 0, $previous); + throw new InvalidTypeException($expectedType, $actualType, $propertyPath, $previous); } if (preg_match('/^Cannot assign (\S+) to property \S+::\$\S+ of type (\S+)$/', $message, $matches)) { [, $actualType, $expectedType] = $matches; - throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".', $expectedType, 'NULL' === $actualType ? 'null' : $actualType, $propertyPath), 0, $previous); + throw new InvalidTypeException($expectedType, $actualType, $propertyPath, $previous); } } diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 487cd4bda4fd6..1361115e24687 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Normalizer; use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException as PropertyAccessInvalidArgumentException; +use Symfony\Component\PropertyAccess\Exception\InvalidTypeException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -374,7 +375,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $exception = NotNormalizableValueException::createForUnexpectedDataType( sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), $data, - ['unknown'], + $e instanceof InvalidTypeException ? [$e->expectedType] : ['unknown'], $context['deserialization_path'] ?? null, false, $e->getCode(), diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index fa9f5c396067e..39ab6c4cf2ef5 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -14,11 +14,13 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\Exception\InvalidTypeException; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\RuntimeException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; @@ -835,6 +837,24 @@ public function testNormalizeStdClass() $this->assertSame(['baz' => 'baz'], $this->normalizer->normalize($o2)); } + + public function testNotNormalizableValueInvalidType() + { + if (!class_exists(InvalidTypeException::class)) { + $this->markTestSkipped('Skipping test as the improvements on PropertyAccess are required.'); + } + + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('Expected argument of type "string", "array" given at property path "initialized"'); + + try { + $this->normalizer->denormalize(['initialized' => ['not a string']], TypedPropertiesObject::class, 'array'); + } catch (NotNormalizableValueException $e) { + $this->assertSame(['string'], $e->getExpectedTypes()); + + throw $e; + } + } } class ProxyObjectDummy extends ObjectDummy