diff --git a/api/src/Serializer/BackedEnumNormalizerDecorator.php b/api/src/Serializer/BackedEnumNormalizerDecorator.php new file mode 100644 index 000000000..522ab2ca9 --- /dev/null +++ b/api/src/Serializer/BackedEnumNormalizerDecorator.php @@ -0,0 +1,98 @@ +inner->normalize($data, $format, $context); + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $this->inner->supportsNormalization($data, $format, $context); + } + + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + if (!is_subclass_of($type, \BackedEnum::class)) { + return $this->inner->denormalize($data, $type, $format, $context); + } + + $backingType = (new \ReflectionEnum($type))->getBackingType()?->getName(); + + // Case 1: Type mismatch — data is not the expected backing type + if (null === $data || ('int' === $backingType && !\is_int($data)) || ('string' === $backingType && !\is_string($data))) { + throw NotNormalizableValueException::createForUnexpectedDataType( + \sprintf('The data must be of type %s.', $backingType), + $data, + [$backingType], + $context['deserialization_path'] ?? null, + true, + ); + } + + // Case 2: Invalid value — right type but not a valid enum case + try { + return $type::from($data); + } catch (\ValueError|\TypeError $e) { + $validValues = array_map( + static fn (\BackedEnum $case): string => \is_string($case->value) + ? \sprintf("'%s'", $case->value) + : (string) $case->value, + $type::cases(), + ); + + throw new NotNormalizableValueException( + message: \sprintf('The data must be one of the following values: %s', implode(', ', $validValues)), + previous: $e, + path: $context['deserialization_path'] ?? null, + useMessageForUser: true, + ); + } + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + return $this->inner->supportsDenormalization($data, $type, $format, $context); + } + + public function getSupportedTypes(?string $format): array + { + return $this->inner->getSupportedTypes($format); + } +} diff --git a/api/tests/Api/Admin/BookTest.php b/api/tests/Api/Admin/BookTest.php index 9eeaa889b..d134dd87e 100644 --- a/api/tests/Api/Admin/BookTest.php +++ b/api/tests/Api/Admin/BookTest.php @@ -308,6 +308,7 @@ public static function getInvalidDataOnCreate(): iterable public static function getInvalidData(): iterable { + $validValuesHint = "The data must be one of the following values: 'https://schema.org/NewCondition', 'https://schema.org/RefurbishedCondition', 'https://schema.org/DamagedCondition', 'https://schema.org/UsedCondition'"; yield 'empty data' => [ [ 'book' => '', @@ -317,11 +318,11 @@ public static function getInvalidData(): iterable [ '@type' => 'ConstraintViolation', 'title' => 'An error occurred', - 'description' => 'condition: This value should be of type int|string.', + 'description' => 'condition: ' . $validValuesHint, 'violations' => [ [ 'propertyPath' => 'condition', - 'hint' => 'The data must belong to a backed enumeration of type ' . BookCondition::class, + 'hint' => $validValuesHint, ], ], ], @@ -335,11 +336,11 @@ public static function getInvalidData(): iterable [ '@type' => 'ConstraintViolation', 'title' => 'An error occurred', - 'description' => 'condition: This value should be of type int|string.', + 'description' => 'condition: ' . $validValuesHint, 'violations' => [ [ 'propertyPath' => 'condition', - 'hint' => 'The data must belong to a backed enumeration of type ' . BookCondition::class, + 'hint' => $validValuesHint, ], ], ],