diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php index e4868c054aea1..dfb2589cba315 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -32,6 +32,7 @@ use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer; use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; @@ -209,4 +210,11 @@ ->args([service('request_stack'), param('kernel.debug')]), ]) ; + + if (interface_exists(\BackedEnum::class)) { + $container->services() + ->set('serializer.normalizer.backed_enum', BackedEnumNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -915]) + ; + } }; diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index d5e5857ecc00a..3f816ce8f2dc5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -54,7 +54,7 @@ "symfony/process": "^4.4|^5.0|^6.0", "symfony/rate-limiter": "^5.2|^6.0", "symfony/security-bundle": "^5.3|^6.0", - "symfony/serializer": "^5.2|^6.0", + "symfony/serializer": "^5.4|^6.0", "symfony/stopwatch": "^4.4|^5.0|^6.0", "symfony/string": "^5.0|^6.0", "symfony/translation": "^5.3|^6.0", diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index b0bd0e711f5ac..23480900a9242 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.4 +--- + + * Add support of PHP backed enumerations + 5.3 --- diff --git a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php new file mode 100644 index 0000000000000..808e046283356 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Normalizer; + +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; + +/** + * Normalizes a {@see \BackedEnum} enumeration to a string or an integer. + * + * @author Alexandre Daubois + */ +final class BackedEnumNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface +{ + /** + * {@inheritdoc} + * + * @throws InvalidArgumentException + * + * @return int|string + */ + public function normalize($object, $format = null, array $context = []) + { + if (!$object instanceof \BackedEnum) { + throw new InvalidArgumentException('The data must belong to a backed enumeration.'); + } + + return $object->value; + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null) + { + return $data instanceof \BackedEnum; + } + + /** + * {@inheritdoc} + * + * @throws NotNormalizableValueException + */ + public function denormalize($data, $type, $format = null, array $context = []) + { + if (!is_subclass_of($type, \BackedEnum::class)) { + throw new InvalidArgumentException('The data must belong to a backed enumeration.'); + } + + if (!\is_int($data) && !\is_string($data)) { + throw new NotNormalizableValueException('The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as an enumeration case of type '.$type.'.'); + } + + try { + return $type::from($data); + } catch (\ValueError $e) { + throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + */ + public function supportsDenormalization($data, $type, $format = null) + { + return is_subclass_of($type, \BackedEnum::class); + } + + /** + * {@inheritdoc} + */ + public function hasCacheableSupportsMethod(): bool + { + return true; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/IntegerBackedEnumDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/IntegerBackedEnumDummy.php new file mode 100644 index 0000000000000..087da0f880a2c --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/IntegerBackedEnumDummy.php @@ -0,0 +1,8 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Normalizer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; +use Symfony\Component\Serializer\Tests\Fixtures\IntegerBackedEnumDummy; +use Symfony\Component\Serializer\Tests\Fixtures\StringBackedEnumDummy; +use Symfony\Component\Serializer\Tests\Fixtures\UnitEnumDummy; + +/** + * @author Alexandre Daubois + */ +class BackedEnumNormalizerTest extends TestCase +{ + /** + * @var BackedEnumNormalizer + */ + private $normalizer; + + protected function setUp(): void + { + $this->normalizer = new BackedEnumNormalizer(); + } + + /** + * @requires PHP 8.1 + */ + public function testSupportsNormalization() + { + $this->assertTrue($this->normalizer->supportsNormalization(StringBackedEnumDummy::GET)); + $this->assertTrue($this->normalizer->supportsNormalization(IntegerBackedEnumDummy::SUCCESS)); + $this->assertFalse($this->normalizer->supportsNormalization(UnitEnumDummy::GET)); + $this->assertFalse($this->normalizer->supportsNormalization(new \stdClass())); + } + + /** + * @requires PHP 8.1 + */ + public function testNormalize() + { + $this->assertSame('GET', $this->normalizer->normalize(StringBackedEnumDummy::GET)); + $this->assertSame(200, $this->normalizer->normalize(IntegerBackedEnumDummy::SUCCESS)); + } + + /** + * @requires PHP 8.1 + */ + public function testNormalizeBadObjectTypeThrowsException() + { + $this->expectException(InvalidArgumentException::class); + $this->normalizer->normalize(new \stdClass()); + } + + /** + * @requires PHP 8.1 + */ + public function testSupportsDenormalization() + { + $this->assertTrue($this->normalizer->supportsDenormalization(null, StringBackedEnumDummy::class)); + $this->assertTrue($this->normalizer->supportsDenormalization(null, IntegerBackedEnumDummy::class)); + $this->assertFalse($this->normalizer->supportsDenormalization(null, UnitEnumDummy::class)); + $this->assertFalse($this->normalizer->supportsDenormalization(null, \stdClass::class)); + } + + /** + * @requires PHP 8.1 + */ + public function testDenormalize() + { + $this->assertSame(StringBackedEnumDummy::GET, $this->normalizer->denormalize('GET', StringBackedEnumDummy::class)); + $this->assertSame(IntegerBackedEnumDummy::SUCCESS, $this->normalizer->denormalize(200, IntegerBackedEnumDummy::class)); + } + + /** + * @requires PHP 8.1 + */ + public function testDenormalizeNullValueThrowsException() + { + $this->expectException(NotNormalizableValueException::class); + $this->normalizer->denormalize(null, StringBackedEnumDummy::class); + } + + /** + * @requires PHP 8.1 + */ + public function testDenormalizeBooleanValueThrowsException() + { + $this->expectException(NotNormalizableValueException::class); + $this->normalizer->denormalize(true, StringBackedEnumDummy::class); + } + + /** + * @requires PHP 8.1 + */ + public function testDenormalizeObjectThrowsException() + { + $this->expectException(NotNormalizableValueException::class); + $this->normalizer->denormalize(new \stdClass(), StringBackedEnumDummy::class); + } + + /** + * @requires PHP 8.1 + */ + public function testDenormalizeBadBackingValueThrowsException() + { + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('"POST" is not a valid backing value for enum "'.StringBackedEnumDummy::class.'"'); + $this->normalizer->denormalize('POST', StringBackedEnumDummy::class); + } + + public function testNormalizeShouldThrowExceptionForNonEnumObjects() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The data must belong to a backed enumeration.'); + + $this->normalizer->normalize(\stdClass::class); + } + + public function testDenormalizeShouldThrowExceptionForNonEnumObjects() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The data must belong to a backed enumeration.'); + + $this->normalizer->denormalize('GET', \stdClass::class); + } + + public function testSupportsNormalizationShouldFailOnAnyPHPVersionForNonEnumObjects() + { + $this->assertFalse($this->normalizer->supportsNormalization(new \stdClass())); + } +}