diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php index efa78dfb05878..7ffd2e5f577a5 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php @@ -24,6 +24,7 @@ use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Serializer\Exception\NotEncodableValueException; use Symfony\Component\Serializer\Exception\PartialDenormalizationException; +use Symfony\Component\Serializer\Exception\UnexpectedPropertyException; use Symfony\Component\Serializer\Exception\UnsupportedFormatException; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -35,6 +36,7 @@ /** * @author Konstantin Myakshin + * @author Aurélien Pillevesse * * @final */ @@ -197,6 +199,8 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay throw new UnsupportedMediaTypeHttpException(sprintf('Unsupported format: "%s".', $format), $e); } catch (NotEncodableValueException $e) { throw new BadRequestHttpException(sprintf('Request payload contains invalid "%s" data.', $format), $e); + } catch (UnexpectedPropertyException $e) { + throw new BadRequestHttpException(sprintf('Request payload contains invalid "%s" property.', $e->property), $e); } } } diff --git a/src/Symfony/Component/Serializer/Exception/UnexpectedPropertyException.php b/src/Symfony/Component/Serializer/Exception/UnexpectedPropertyException.php new file mode 100644 index 0000000000000..4f9ead9a64c30 --- /dev/null +++ b/src/Symfony/Component/Serializer/Exception/UnexpectedPropertyException.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Exception; + +/** + * UnexpectedPropertyException. + * + * @author Aurélien Pillevesse + */ +class UnexpectedPropertyException extends \UnexpectedValueException implements ExceptionInterface +{ + public function __construct( + public readonly string $property, + ?\Throwable $previous = null, + ) { + $msg = sprintf('Property is not allowed ("%s" is unknown).', $this->property); + + parent::__construct($msg, 0, $previous); + } +} diff --git a/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php b/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php index a7b450fd27a34..8c0b53157f4cd 100644 --- a/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php +++ b/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php @@ -11,13 +11,21 @@ namespace Symfony\Component\Serializer\NameConverter; +use Symfony\Component\Serializer\Exception\UnexpectedPropertyException; + /** * CamelCase to Underscore name converter. * * @author Kévin Dunglas + * @author Aurélien Pillevesse */ -class CamelCaseToSnakeCaseNameConverter implements NameConverterInterface +class CamelCaseToSnakeCaseNameConverter implements AdvancedNameConverterInterface { + /** + * Require all properties to be written in snake_case. + */ + public const REQUIRE_SNAKE_CASE_PROPERTIES = 'require_snake_case_properties'; + /** * @param array|null $attributes The list of attributes to rename or null for all attributes * @param bool $lowerCamelCase Use lowerCamelCase style @@ -28,7 +36,7 @@ public function __construct( ) { } - public function normalize(string $propertyName): string + public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { if (null === $this->attributes || \in_array($propertyName, $this->attributes, true)) { return strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($propertyName))); @@ -37,8 +45,12 @@ public function normalize(string $propertyName): string return $propertyName; } - public function denormalize(string $propertyName): string + public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { + if (($context[self::REQUIRE_SNAKE_CASE_PROPERTIES] ?? false) && $propertyName !== $this->normalize($propertyName, $class, $format, $context)) { + throw new UnexpectedPropertyException($propertyName); + } + $camelCasedName = preg_replace_callback('/(^|_|\.)+(.)/', fn ($match) => ('.' === $match[1] ? '_' : '').strtoupper($match[2]), $propertyName); if ($this->lowerCamelCase) { diff --git a/src/Symfony/Component/Serializer/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php b/src/Symfony/Component/Serializer/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php index e4d419e454f16..f9d941890f1c8 100644 --- a/src/Symfony/Component/Serializer/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php +++ b/src/Symfony/Component/Serializer/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php @@ -12,11 +12,13 @@ namespace Symfony\Component\Serializer\Tests\NameConverter; use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Exception\UnexpectedPropertyException; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * @author Kévin Dunglas + * @author Aurélien Pillevesse */ class CamelCaseToSnakeCaseNameConverterTest extends TestCase { @@ -55,4 +57,20 @@ public static function attributeProvider() ['this_is_a_test', 'ThisIsATest', false], ]; } + + public function testDenormalizeWithContext() + { + $nameConverter = new CamelCaseToSnakeCaseNameConverter(null, true); + $denormalizedValue = $nameConverter->denormalize('last_name', context: [CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES]); + + $this->assertSame($denormalizedValue, 'lastName'); + } + + public function testErrorDenormalizeWithContext() + { + $nameConverter = new CamelCaseToSnakeCaseNameConverter(null, true); + + $this->expectException(UnexpectedPropertyException::class); + $nameConverter->denormalize('lastName', context: [CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES => true]); + } }