diff --git a/Attribute/DiscriminatorMap.php b/Attribute/DiscriminatorMap.php index 48d0842aa..a61f32675 100644 --- a/Attribute/DiscriminatorMap.php +++ b/Attribute/DiscriminatorMap.php @@ -22,12 +22,14 @@ class DiscriminatorMap /** * @param string $typeProperty The property holding the type discriminator * @param array $mapping The mapping between types and classes (i.e. ['admin_user' => AdminUser::class]) + * @param ?string $defaultType The fallback value if nothing specified by $typeProperty * * @throws InvalidArgumentException */ public function __construct( private readonly string $typeProperty, private readonly array $mapping, + private readonly ?string $defaultType = null, ) { if (!$typeProperty) { throw new InvalidArgumentException(\sprintf('Parameter "typeProperty" given to "%s" cannot be empty.', static::class)); @@ -36,6 +38,10 @@ public function __construct( if (!$mapping) { throw new InvalidArgumentException(\sprintf('Parameter "mapping" given to "%s" cannot be empty.', static::class)); } + + if (null !== $this->defaultType && !\array_key_exists($this->defaultType, $this->mapping)) { + throw new InvalidArgumentException(\sprintf('Default type "%s" given to "%s" must be present in "mapping" types.', $this->defaultType, static::class)); + } } public function getTypeProperty(): string @@ -47,6 +53,11 @@ public function getMapping(): array { return $this->mapping; } + + public function getDefaultType(): ?string + { + return $this->defaultType; + } } if (!class_exists(\Symfony\Component\Serializer\Annotation\DiscriminatorMap::class, false)) { diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c36d5885..1b5c95cd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,19 @@ CHANGELOG ========= +7.3 +--- + + * Deprecate the `CompiledClassMetadataFactory` and `CompiledClassMetadataCacheWarmer` classes + * Register `NormalizerInterface` and `DenormalizerInterface` aliases for named serializers + * Add `NumberNormalizer` to normalize `BcMath\Number` and `GMP` as `string` + * Add `defaultType` to `DiscriminatorMap` + 7.2 --- - * Deprecate the `csv_escape_char` context option of `CsvEncoder`, the `CsvEncoder::ESCAPE_CHAR_KEY` constant - and the `CsvEncoderContextBuilder::withEscapeChar()` method, following its deprecation in PHP 8.4 + * Deprecate the `csv_escape_char` context option of `CsvEncoder` and the `CsvEncoder::ESCAPE_CHAR_KEY` constant + * Deprecate `CsvEncoderContextBuilder::withEscapeChar()` method * Add `SnakeCaseToCamelCaseNameConverter` * Support subclasses of `\DateTime` and `\DateTimeImmutable` for denormalization * Add the `UidNormalizer::NORMALIZATION_FORMAT_RFC9562` constant @@ -19,6 +27,7 @@ CHANGELOG * Add arguments `$class`, `$format` and `$context` to `NameConverterInterface::normalize()` and `NameConverterInterface::denormalize()` * Add `DateTimeNormalizer::CAST_KEY` context option + * Add `Default` and "class name" default groups * Add `AbstractNormalizer::FILTER_BOOL` context option * Add `CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES` context option * Deprecate `AbstractNormalizerContextBuilder::withDefaultContructorArguments(?array $defaultContructorArguments)`, use `withDefaultConstructorArguments(?array $defaultConstructorArguments)` instead (note the missing `s` character in Contructor word in deprecated method) diff --git a/CacheWarmer/CompiledClassMetadataCacheWarmer.php b/CacheWarmer/CompiledClassMetadataCacheWarmer.php index 379a2a380..1bd085024 100644 --- a/CacheWarmer/CompiledClassMetadataCacheWarmer.php +++ b/CacheWarmer/CompiledClassMetadataCacheWarmer.php @@ -16,8 +16,12 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryCompiler; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +trigger_deprecation('symfony/serializer', '7.3', 'The "%s" class is deprecated.', CompiledClassMetadataCacheWarmer::class); + /** * @author Fabien Bourigault + * + * @deprecated since Symfony 7.3 */ final class CompiledClassMetadataCacheWarmer implements CacheWarmerInterface { diff --git a/Command/DebugCommand.php b/Command/DebugCommand.php index 7df4d6bc8..c49b20f95 100644 --- a/Command/DebugCommand.php +++ b/Command/DebugCommand.php @@ -96,6 +96,9 @@ private function getAttributesData(ClassMetadataInterface $classMetadata): array { $data = []; + $mapping = $classMetadata->getClassDiscriminatorMapping(); + $typeProperty = $mapping?->getTypeProperty(); + foreach ($classMetadata->getAttributesMetadata() as $attributeMetadata) { $data[$attributeMetadata->getName()] = [ 'groups' => $attributeMetadata->getGroups(), @@ -106,6 +109,10 @@ private function getAttributesData(ClassMetadataInterface $classMetadata): array 'normalizationContexts' => $attributeMetadata->getNormalizationContexts(), 'denormalizationContexts' => $attributeMetadata->getDenormalizationContexts(), ]; + + if ($mapping && $typeProperty === $attributeMetadata->getName()) { + $data[$attributeMetadata->getName()]['discriminatorMap'] = $mapping->getTypesMapping(); + } } return $data; diff --git a/Context/Encoder/XmlEncoderContextBuilder.php b/Context/Encoder/XmlEncoderContextBuilder.php index 0fd1f2f44..7a5097e94 100644 --- a/Context/Encoder/XmlEncoderContextBuilder.php +++ b/Context/Encoder/XmlEncoderContextBuilder.php @@ -160,4 +160,12 @@ public function withCdataWrappingPattern(?string $cdataWrappingPattern): static { return $this->with(XmlEncoder::CDATA_WRAPPING_PATTERN, $cdataWrappingPattern); } + + /** + * Configures whether to ignore empty attributes. + */ + public function withIgnoreEmptyAttributes(?bool $ignoreEmptyAttributes): static + { + return $this->with(XmlEncoder::IGNORE_EMPTY_ATTRIBUTES, $ignoreEmptyAttributes); + } } diff --git a/Debug/TraceableNormalizer.php b/Debug/TraceableNormalizer.php index 1b143e295..d737f5b36 100644 --- a/Debug/TraceableNormalizer.php +++ b/Debug/TraceableNormalizer.php @@ -40,14 +40,14 @@ public function getSupportedTypes(?string $format): array return $this->normalizer->getSupportedTypes($format); } - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { if (!$this->normalizer instanceof NormalizerInterface) { throw new \BadMethodCallException(\sprintf('The "%s()" method cannot be called as nested normalizer doesn\'t implements "%s".', __METHOD__, NormalizerInterface::class)); } $startTime = microtime(true); - $normalized = $this->normalizer->normalize($object, $format, $context); + $normalized = $this->normalizer->normalize($data, $format, $context); $time = microtime(true) - $startTime; if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) { diff --git a/Debug/TraceableSerializer.php b/Debug/TraceableSerializer.php index a05bf4bf8..bd4f505f8 100644 --- a/Debug/TraceableSerializer.php +++ b/Debug/TraceableSerializer.php @@ -66,17 +66,17 @@ public function deserialize(mixed $data, string $type, string $format, array $co return $result; } - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { $context[self::DEBUG_TRACE_ID] = $traceId = bin2hex(random_bytes(4)); $startTime = microtime(true); - $result = $this->serializer->normalize($object, $format, $context); + $result = $this->serializer->normalize($data, $format, $context); $time = microtime(true) - $startTime; $caller = $this->getCaller(__FUNCTION__, NormalizerInterface::class); - $this->dataCollector->collectNormalize($traceId, $object, $format, $context, $time, $caller, $this->serializerName); + $this->dataCollector->collectNormalize($traceId, $data, $format, $context, $time, $caller, $this->serializerName); return $result; } diff --git a/DependencyInjection/SerializerPass.php b/DependencyInjection/SerializerPass.php index 179b7a3d9..994f61246 100644 --- a/DependencyInjection/SerializerPass.php +++ b/DependencyInjection/SerializerPass.php @@ -19,6 +19,8 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Serializer\Debug\TraceableEncoder; use Symfony\Component\Serializer\Debug\TraceableNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\SerializerInterface; @@ -175,6 +177,8 @@ private function configureNamedSerializers(ContainerBuilder $container, ?string $container->registerChild($serializerId, 'serializer')->setArgument('$defaultContext', $config['default_context']); $container->registerAliasForArgument($serializerId, SerializerInterface::class, $serializerName.'.serializer'); + $container->registerAliasForArgument($serializerId, NormalizerInterface::class, $serializerName.'.normalizer'); + $container->registerAliasForArgument($serializerId, DenormalizerInterface::class, $serializerName.'.denormalizer'); $this->configureSerializer($container, $serializerId, $normalizers, $encoders, $serializerName); diff --git a/Encoder/XmlEncoder.php b/Encoder/XmlEncoder.php index e1a816380..ed66fa308 100644 --- a/Encoder/XmlEncoder.php +++ b/Encoder/XmlEncoder.php @@ -60,6 +60,7 @@ class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwa public const VERSION = 'xml_version'; public const CDATA_WRAPPING = 'cdata_wrapping'; public const CDATA_WRAPPING_PATTERN = 'cdata_wrapping_pattern'; + public const IGNORE_EMPTY_ATTRIBUTES = 'ignore_empty_attributes'; private array $defaultContext = [ self::AS_COLLECTION => false, @@ -72,6 +73,7 @@ class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwa self::TYPE_CAST_ATTRIBUTES => true, self::CDATA_WRAPPING => true, self::CDATA_WRAPPING_PATTERN => '/[<>&]/', + self::IGNORE_EMPTY_ATTRIBUTES => false, ]; public function __construct(array $defaultContext = []) @@ -355,6 +357,13 @@ private function buildXml(\DOMNode $parentNode, mixed $data, string $format, arr if (\is_bool($data)) { $data = (int) $data; } + + if ($context[self::IGNORE_EMPTY_ATTRIBUTES] ?? $this->defaultContext[self::IGNORE_EMPTY_ATTRIBUTES]) { + if (null === $data || '' === $data) { + continue; + } + } + $parentNode->setAttribute($attributeName, $data); } elseif ('#' === $key) { $append = $this->selectNodeType($parentNode, $data, $format, $context); diff --git a/Mapping/ClassDiscriminatorMapping.php b/Mapping/ClassDiscriminatorMapping.php index 260575a41..985ea1ce5 100644 --- a/Mapping/ClassDiscriminatorMapping.php +++ b/Mapping/ClassDiscriminatorMapping.php @@ -22,6 +22,7 @@ class ClassDiscriminatorMapping public function __construct( private readonly string $typeProperty, private array $typesMapping = [], + private readonly ?string $defaultType = null, ) { uasort($this->typesMapping, static function (string $a, string $b): int { if (is_a($a, $b, true)) { @@ -61,4 +62,9 @@ public function getTypesMapping(): array { return $this->typesMapping; } + + public function getDefaultType(): ?string + { + return $this->defaultType; + } } diff --git a/Mapping/Factory/ClassMetadataFactoryCompiler.php b/Mapping/Factory/ClassMetadataFactoryCompiler.php index 1e9202b7d..7ec3e0ace 100644 --- a/Mapping/Factory/ClassMetadataFactoryCompiler.php +++ b/Mapping/Factory/ClassMetadataFactoryCompiler.php @@ -55,6 +55,7 @@ private function generateDeclaredClassMetadata(array $classMetadatas): string $classDiscriminatorMapping = $classMetadata->getClassDiscriminatorMapping() ? [ $classMetadata->getClassDiscriminatorMapping()->getTypeProperty(), $classMetadata->getClassDiscriminatorMapping()->getTypesMapping(), + $classMetadata->getClassDiscriminatorMapping()->getDefaultType(), ] : null; $compiled .= \sprintf("\n'%s' => %s,", $classMetadata->getName(), VarExporter::export([ diff --git a/Mapping/Factory/CompiledClassMetadataFactory.php b/Mapping/Factory/CompiledClassMetadataFactory.php index ec25d7440..759da166d 100644 --- a/Mapping/Factory/CompiledClassMetadataFactory.php +++ b/Mapping/Factory/CompiledClassMetadataFactory.php @@ -16,8 +16,12 @@ use Symfony\Component\Serializer\Mapping\ClassMetadata; use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; +trigger_deprecation('symfony/serializer', '7.3', 'The "%s" class is deprecated.', CompiledClassMetadataFactory::class); + /** * @author Fabien Bourigault + * + * @deprecated since Symfony 7.3 */ final class CompiledClassMetadataFactory implements ClassMetadataFactoryInterface { diff --git a/Mapping/Loader/AttributeLoader.php b/Mapping/Loader/AttributeLoader.php index 13c59d1c3..bf8ab356e 100644 --- a/Mapping/Loader/AttributeLoader.php +++ b/Mapping/Loader/AttributeLoader.php @@ -59,7 +59,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool foreach ($this->loadAttributes($reflectionClass) as $attribute) { match (true) { - $attribute instanceof DiscriminatorMap => $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping($attribute->getTypeProperty(), $attribute->getMapping())), + $attribute instanceof DiscriminatorMap => $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping($attribute->getTypeProperty(), $attribute->getMapping(), $attribute->getDefaultType())), $attribute instanceof Groups => $classGroups = $attribute->getGroups(), $attribute instanceof Context => $classContextAttribute = $attribute, default => null, diff --git a/Mapping/Loader/XmlFileLoader.php b/Mapping/Loader/XmlFileLoader.php index 44ba89df1..ac6fee2db 100644 --- a/Mapping/Loader/XmlFileLoader.php +++ b/Mapping/Loader/XmlFileLoader.php @@ -107,7 +107,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping( (string) $xml->{'discriminator-map'}->attributes()->{'type-property'}, - $mapping + $mapping, + $xml->{'discriminator-map'}->attributes()->{'default-type'} ?? null )); } diff --git a/Mapping/Loader/YamlFileLoader.php b/Mapping/Loader/YamlFileLoader.php index ca71cbcba..898ae9f19 100644 --- a/Mapping/Loader/YamlFileLoader.php +++ b/Mapping/Loader/YamlFileLoader.php @@ -133,7 +133,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping( $yaml['discriminator_map']['type_property'], - $yaml['discriminator_map']['mapping'] + $yaml['discriminator_map']['mapping'], + $yaml['discriminator_map']['default_type'] ?? null )); } diff --git a/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd b/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd index f5f6cca9f..06c6ddfb1 100644 --- a/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd +++ b/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd @@ -47,6 +47,7 @@ + diff --git a/Normalizer/AbstractNormalizer.php b/Normalizer/AbstractNormalizer.php index 04f378c46..4aba4b0b6 100644 --- a/Normalizer/AbstractNormalizer.php +++ b/Normalizer/AbstractNormalizer.php @@ -164,19 +164,19 @@ public function __construct( */ protected function isCircularReference(object $object, array &$context): bool { - $objectHash = spl_object_hash($object); + $objectId = spl_object_id($object); $circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT]; - if (isset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash])) { - if ($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] >= $circularReferenceLimit) { - unset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]); + if (isset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectId])) { + if ($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectId] >= $circularReferenceLimit) { + unset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectId]); return true; } - ++$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]; + ++$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectId]; } else { - $context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] = 1; + $context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectId] = 1; } return false; diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 00d2e3b00..c346aafa8 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -154,7 +154,7 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return \is_object($data) && !$data instanceof \Traversable; } - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { $context['_read_attributes'] = true; @@ -164,14 +164,14 @@ public function normalize(mixed $object, ?string $format = null, array $context $this->validateCallbackContext($context); - if ($this->isCircularReference($object, $context)) { - return $this->handleCircularReference($object, $format, $context); + if ($this->isCircularReference($data, $context)) { + return $this->handleCircularReference($data, $format, $context); } - $data = []; + $normalizedData = []; $stack = []; - $attributes = $this->getAttributes($object, $format, $context); - $class = ($this->objectClassResolver)($object); + $attributes = $this->getAttributes($data, $format, $context); + $class = ($this->objectClassResolver)($data); $classMetadata = $this->classMetadataFactory?->getMetadataFor($class); $attributesMetadata = $this->classMetadataFactory?->getMetadataFor($class)->getAttributesMetadata(); if (isset($context[self::MAX_DEPTH_HANDLER])) { @@ -189,12 +189,12 @@ public function normalize(mixed $object, ?string $format = null, array $context continue; } - $attributeContext = $this->getAttributeNormalizationContext($object, $attribute, $context); + $attributeContext = $this->getAttributeNormalizationContext($data, $attribute, $context); try { - $attributeValue = $attribute === $this->classDiscriminatorResolver?->getMappingForMappedObject($object)?->getTypeProperty() - ? $this->classDiscriminatorResolver?->getTypeForMappedObject($object) - : $this->getAttributeValue($object, $attribute, $format, $attributeContext); + $attributeValue = $attribute === $this->classDiscriminatorResolver?->getMappingForMappedObject($data)?->getTypeProperty() + ? $this->classDiscriminatorResolver?->getTypeForMappedObject($data) + : $this->getAttributeValue($data, $attribute, $format, $attributeContext); } catch (UninitializedPropertyException|\Error $e) { if (($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) && $this->isUninitializedValueError($e)) { continue; @@ -203,17 +203,17 @@ public function normalize(mixed $object, ?string $format = null, array $context } if ($maxDepthReached) { - $attributeValue = $maxDepthHandler($attributeValue, $object, $attribute, $format, $attributeContext); + $attributeValue = $maxDepthHandler($attributeValue, $data, $attribute, $format, $attributeContext); } - $stack[$attribute] = $this->applyCallbacks($attributeValue, $object, $attribute, $format, $attributeContext); + $stack[$attribute] = $this->applyCallbacks($attributeValue, $data, $attribute, $format, $attributeContext); } foreach ($stack as $attribute => $attributeValue) { - $attributeContext = $this->getAttributeNormalizationContext($object, $attribute, $context); + $attributeContext = $this->getAttributeNormalizationContext($data, $attribute, $context); if (null === $attributeValue || \is_scalar($attributeValue)) { - $data = $this->updateData($data, $attribute, $attributeValue, $class, $format, $attributeContext, $attributesMetadata, $classMetadata); + $normalizedData = $this->updateData($normalizedData, $attribute, $attributeValue, $class, $format, $attributeContext, $attributesMetadata, $classMetadata); continue; } @@ -223,15 +223,15 @@ public function normalize(mixed $object, ?string $format = null, array $context $childContext = $this->createChildContext($attributeContext, $attribute, $format); - $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $childContext), $class, $format, $attributeContext, $attributesMetadata, $classMetadata); + $normalizedData = $this->updateData($normalizedData, $attribute, $this->serializer->normalize($attributeValue, $format, $childContext), $class, $format, $attributeContext, $attributesMetadata, $classMetadata); } $preserveEmptyObjects = $context[self::PRESERVE_EMPTY_OBJECTS] ?? $this->defaultContext[self::PRESERVE_EMPTY_OBJECTS] ?? false; - if ($preserveEmptyObjects && !$data) { + if ($preserveEmptyObjects && !$normalizedData) { return new \ArrayObject(); } - return $data; + return $normalizedData; } protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, ?string $format = null): object @@ -512,6 +512,18 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass } } + if (is_numeric($data) && XmlEncoder::FORMAT === $format) { + // encoder parsed them wrong, so they might need to be transformed back + switch ($builtinType) { + case LegacyType::BUILTIN_TYPE_STRING: + return (string) $data; + case LegacyType::BUILTIN_TYPE_FLOAT: + return (float) $data; + case LegacyType::BUILTIN_TYPE_INT: + return (int) $data; + } + } + if (null !== $collectionValueType && LegacyType::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) { $builtinType = LegacyType::BUILTIN_TYPE_OBJECT; $class = $collectionValueType->getClassName().'[]'; @@ -748,6 +760,18 @@ private function validateAndDenormalize(Type $type, string $currentClass, string } } + if (is_numeric($data) && XmlEncoder::FORMAT === $format) { + // encoder parsed them wrong, so they might need to be transformed back + switch ($typeIdentifier) { + case TypeIdentifier::STRING: + return (string) $data; + case TypeIdentifier::FLOAT: + return (float) $data; + case TypeIdentifier::INT: + return (int) $data; + } + } + if ($collectionValueType) { try { $collectionValueBaseType = $collectionValueType; @@ -1155,7 +1179,7 @@ private function getMappedClass(array $data, string $class, array $context): str return $class; } - if (null === $type = $data[$mapping->getTypeProperty()] ?? null) { + if (null === $type = $data[$mapping->getTypeProperty()] ?? $mapping->getDefaultType()) { throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false); } diff --git a/Normalizer/BackedEnumNormalizer.php b/Normalizer/BackedEnumNormalizer.php index 3d8e7e7c5..7d3659c8c 100644 --- a/Normalizer/BackedEnumNormalizer.php +++ b/Normalizer/BackedEnumNormalizer.php @@ -33,13 +33,13 @@ public function getSupportedTypes(?string $format): array ]; } - public function normalize(mixed $object, ?string $format = null, array $context = []): int|string + public function normalize(mixed $data, ?string $format = null, array $context = []): int|string { - if (!$object instanceof \BackedEnum) { + if (!$data instanceof \BackedEnum) { throw new InvalidArgumentException('The data must belong to a backed enumeration.'); } - return $object->value; + return $data->value; } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool diff --git a/Normalizer/ConstraintViolationListNormalizer.php b/Normalizer/ConstraintViolationListNormalizer.php index eda3b758e..92e036384 100644 --- a/Normalizer/ConstraintViolationListNormalizer.php +++ b/Normalizer/ConstraintViolationListNormalizer.php @@ -43,7 +43,7 @@ public function getSupportedTypes(?string $format): array ]; } - public function normalize(mixed $object, ?string $format = null, array $context = []): array + public function normalize(mixed $data, ?string $format = null, array $context = []): array { if (\array_key_exists(self::PAYLOAD_FIELDS, $context)) { $payloadFieldsToSerialize = $context[self::PAYLOAD_FIELDS]; @@ -59,7 +59,7 @@ public function normalize(mixed $object, ?string $format = null, array $context $violations = []; $messages = []; - foreach ($object as $violation) { + foreach ($data as $violation) { $propertyPath = $this->nameConverter ? $this->nameConverter->normalize($violation->getPropertyPath(), null, $format, $context) : $violation->getPropertyPath(); $violationEntry = [ diff --git a/Normalizer/CustomNormalizer.php b/Normalizer/CustomNormalizer.php index d97108312..444c41354 100644 --- a/Normalizer/CustomNormalizer.php +++ b/Normalizer/CustomNormalizer.php @@ -30,9 +30,9 @@ public function getSupportedTypes(?string $format): array ]; } - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { - return $object->normalize($this->serializer, $format, $context); + return $data->normalize($this->serializer, $format, $context); } public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed diff --git a/Normalizer/DataUriNormalizer.php b/Normalizer/DataUriNormalizer.php index 5ee076be6..57ad724c4 100644 --- a/Normalizer/DataUriNormalizer.php +++ b/Normalizer/DataUriNormalizer.php @@ -47,27 +47,27 @@ public function getSupportedTypes(?string $format): array return self::SUPPORTED_TYPES; } - public function normalize(mixed $object, ?string $format = null, array $context = []): string + public function normalize(mixed $data, ?string $format = null, array $context = []): string { - if (!$object instanceof \SplFileInfo) { + if (!$data instanceof \SplFileInfo) { throw new InvalidArgumentException('The object must be an instance of "\SplFileInfo".'); } - $mimeType = $this->getMimeType($object); - $splFileObject = $this->extractSplFileObject($object); + $mimeType = $this->getMimeType($data); + $splFileObject = $this->extractSplFileObject($data); - $data = ''; + $splFileData = ''; $splFileObject->rewind(); while (!$splFileObject->eof()) { - $data .= $splFileObject->fgets(); + $splFileData .= $splFileObject->fgets(); } if ('text' === explode('/', $mimeType, 2)[0]) { - return \sprintf('data:%s,%s', $mimeType, rawurlencode($data)); + return \sprintf('data:%s,%s', $mimeType, rawurlencode($splFileData)); } - return \sprintf('data:%s;base64,%s', $mimeType, base64_encode($data)); + return \sprintf('data:%s;base64,%s', $mimeType, base64_encode($splFileData)); } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool diff --git a/Normalizer/DateIntervalNormalizer.php b/Normalizer/DateIntervalNormalizer.php index 05d1a8529..1ad81ec61 100644 --- a/Normalizer/DateIntervalNormalizer.php +++ b/Normalizer/DateIntervalNormalizer.php @@ -43,13 +43,13 @@ public function getSupportedTypes(?string $format): array /** * @throws InvalidArgumentException */ - public function normalize(mixed $object, ?string $format = null, array $context = []): string + public function normalize(mixed $data, ?string $format = null, array $context = []): string { - if (!$object instanceof \DateInterval) { + if (!$data instanceof \DateInterval) { throw new InvalidArgumentException('The object must be an instance of "\DateInterval".'); } - return $object->format($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY]); + return $data->format($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY]); } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool diff --git a/Normalizer/DateTimeNormalizer.php b/Normalizer/DateTimeNormalizer.php index dfc498c19..a136ec227 100644 --- a/Normalizer/DateTimeNormalizer.php +++ b/Normalizer/DateTimeNormalizer.php @@ -56,9 +56,9 @@ public function getSupportedTypes(?string $format): array /** * @throws InvalidArgumentException */ - public function normalize(mixed $object, ?string $format = null, array $context = []): int|float|string + public function normalize(mixed $data, ?string $format = null, array $context = []): int|float|string { - if (!$object instanceof \DateTimeInterface) { + if (!$data instanceof \DateTimeInterface) { throw new InvalidArgumentException('The object must implement the "\DateTimeInterface".'); } @@ -66,14 +66,14 @@ public function normalize(mixed $object, ?string $format = null, array $context $timezone = $this->getTimezone($context); if (null !== $timezone) { - $object = clone $object; - $object = $object->setTimezone($timezone); + $data = clone $data; + $data = $data->setTimezone($timezone); } return match ($context[self::CAST_KEY] ?? $this->defaultContext[self::CAST_KEY] ?? false) { - 'int' => (int) $object->format($dateTimeFormat), - 'float' => (float) $object->format($dateTimeFormat), - default => $object->format($dateTimeFormat), + 'int' => (int) $data->format($dateTimeFormat), + 'float' => (float) $data->format($dateTimeFormat), + default => $data->format($dateTimeFormat), }; } diff --git a/Normalizer/DateTimeZoneNormalizer.php b/Normalizer/DateTimeZoneNormalizer.php index f4528a03d..cdb6eb2f0 100644 --- a/Normalizer/DateTimeZoneNormalizer.php +++ b/Normalizer/DateTimeZoneNormalizer.php @@ -31,13 +31,13 @@ public function getSupportedTypes(?string $format): array /** * @throws InvalidArgumentException */ - public function normalize(mixed $object, ?string $format = null, array $context = []): string + public function normalize(mixed $data, ?string $format = null, array $context = []): string { - if (!$object instanceof \DateTimeZone) { + if (!$data instanceof \DateTimeZone) { throw new InvalidArgumentException('The object must be an instance of "\DateTimeZone".'); } - return $object->getName(); + return $data->getName(); } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool diff --git a/Normalizer/FormErrorNormalizer.php b/Normalizer/FormErrorNormalizer.php index 9ef13a669..c91d7d528 100644 --- a/Normalizer/FormErrorNormalizer.php +++ b/Normalizer/FormErrorNormalizer.php @@ -22,20 +22,20 @@ final class FormErrorNormalizer implements NormalizerInterface public const TYPE = 'type'; public const CODE = 'status_code'; - public function normalize(mixed $object, ?string $format = null, array $context = []): array + public function normalize(mixed $data, ?string $format = null, array $context = []): array { - $data = [ + $error = [ 'title' => $context[self::TITLE] ?? 'Validation Failed', 'type' => $context[self::TYPE] ?? 'https://symfony.com/errors/form', 'code' => $context[self::CODE] ?? null, - 'errors' => $this->convertFormErrorsToArray($object), + 'errors' => $this->convertFormErrorsToArray($data), ]; - if (0 !== \count($object->all())) { - $data['children'] = $this->convertFormChildrenToArray($object); + if (0 !== \count($data->all())) { + $error['children'] = $this->convertFormChildrenToArray($data); } - return $data; + return $error; } public function getSupportedTypes(?string $format): array diff --git a/Normalizer/GetSetMethodNormalizer.php b/Normalizer/GetSetMethodNormalizer.php index 4d2d7829a..17731067c 100644 --- a/Normalizer/GetSetMethodNormalizer.php +++ b/Normalizer/GetSetMethodNormalizer.php @@ -103,7 +103,7 @@ private function isSetMethod(\ReflectionMethod $method): bool && 3 < \strlen($method->name) && str_starts_with($method->name, 'set') && !ctype_lower($method->name[3]) - ; + ; } protected function extractAttributes(object $object, ?string $format = null, array $context = []): array diff --git a/Normalizer/JsonSerializableNormalizer.php b/Normalizer/JsonSerializableNormalizer.php index 31c224175..3bc4f280e 100644 --- a/Normalizer/JsonSerializableNormalizer.php +++ b/Normalizer/JsonSerializableNormalizer.php @@ -21,13 +21,13 @@ */ final class JsonSerializableNormalizer extends AbstractNormalizer { - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { - if ($this->isCircularReference($object, $context)) { - return $this->handleCircularReference($object, $format, $context); + if ($this->isCircularReference($data, $context)) { + return $this->handleCircularReference($data, $format, $context); } - if (!$object instanceof \JsonSerializable) { + if (!$data instanceof \JsonSerializable) { throw new InvalidArgumentException(\sprintf('The object must implement "%s".', \JsonSerializable::class)); } @@ -35,7 +35,7 @@ public function normalize(mixed $object, ?string $format = null, array $context throw new LogicException('Cannot normalize object because injected serializer is not a normalizer.'); } - return $this->serializer->normalize($object->jsonSerialize(), $format, $context); + return $this->serializer->normalize($data->jsonSerialize(), $format, $context); } public function getSupportedTypes(?string $format): array diff --git a/Normalizer/MimeMessageNormalizer.php b/Normalizer/MimeMessageNormalizer.php index 633edf369..5b1213389 100644 --- a/Normalizer/MimeMessageNormalizer.php +++ b/Normalizer/MimeMessageNormalizer.php @@ -62,25 +62,25 @@ public function setSerializer(SerializerInterface $serializer): void $this->normalizer->setSerializer($serializer); } - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { - if ($object instanceof Headers) { + if ($data instanceof Headers) { $ret = []; - foreach ($this->headersProperty->getValue($object) as $name => $header) { + foreach ($this->headersProperty->getValue($data) as $name => $header) { $ret[$name] = $this->serializer->normalize($header, $format, $context); } return $ret; } - $ret = $this->normalizer->normalize($object, $format, $context); + $ret = $this->normalizer->normalize($data, $format, $context); - if ($object instanceof AbstractPart) { - $ret['class'] = $object::class; + if ($data instanceof AbstractPart) { + $ret['class'] = $data::class; unset($ret['seekable'], $ret['cid'], $ret['handle']); } - if ($object instanceof RawMessage && \array_key_exists('message', $ret) && null === $ret['message']) { + if ($data instanceof RawMessage && \array_key_exists('message', $ret) && null === $ret['message']) { unset($ret['message']); } diff --git a/Normalizer/NumberNormalizer.php b/Normalizer/NumberNormalizer.php new file mode 100644 index 000000000..de68a406e --- /dev/null +++ b/Normalizer/NumberNormalizer.php @@ -0,0 +1,79 @@ + + * + * 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 BcMath\Number; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; + +/** + * Normalizes {@see Number} and {@see \GMP} to a string. + */ +final class NumberNormalizer implements NormalizerInterface, DenormalizerInterface +{ + public function getSupportedTypes(?string $format): array + { + return [ + Number::class => true, + \GMP::class => true, + ]; + } + + public function normalize(mixed $data, ?string $format = null, array $context = []): string + { + if (!$data instanceof Number && !$data instanceof \GMP) { + throw new InvalidArgumentException(\sprintf('The data must be an instance of "%s" or "%s".', Number::class, \GMP::class)); + } + + return (string) $data; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof Number || $data instanceof \GMP; + } + + /** + * @throws NotNormalizableValueException + */ + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): Number|\GMP + { + if (!\is_string($data) && !\is_int($data)) { + throw $this->createNotNormalizableValueException($type, $data, $context); + } + + try { + return match ($type) { + Number::class => new Number($data), + \GMP::class => new \GMP($data), + default => throw new InvalidArgumentException(\sprintf('Only "%s" and "%s" types are supported.', Number::class, \GMP::class)), + }; + } catch (\ValueError $e) { + throw $this->createNotNormalizableValueException($type, $data, $context, $e); + } + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + return \in_array($type, [Number::class, \GMP::class], true) && null !== $data; + } + + private function createNotNormalizableValueException(string $type, mixed $data, array $context, ?\Throwable $previous = null): NotNormalizableValueException + { + $message = match ($type) { + Number::class => 'The data must be a "string" representing a decimal number, or an "int".', + \GMP::class => 'The data must be a "string" representing an integer, or an "int".', + }; + + return NotNormalizableValueException::createForUnexpectedDataType($message, $data, ['string', 'int'], $context['deserialization_path'] ?? null, true, 0, $previous); + } +} diff --git a/Normalizer/ProblemNormalizer.php b/Normalizer/ProblemNormalizer.php index 08aca6796..8f255e6ce 100644 --- a/Normalizer/ProblemNormalizer.php +++ b/Normalizer/ProblemNormalizer.php @@ -57,7 +57,7 @@ public function normalize(mixed $object, ?string $format = null, array $context throw new InvalidArgumentException(\sprintf('The object must implement "%s".', FlattenException::class)); } - $data = []; + $error = []; $context += $this->defaultContext; $debug = $this->debug && ($context['debug'] ?? true); $exception = $context['exception'] ?? null; @@ -67,7 +67,7 @@ public function normalize(mixed $object, ?string $format = null, array $context if ($exception instanceof PartialDenormalizationException) { $trans = $this->translator ? $this->translator->trans(...) : fn ($m, $p) => strtr($m, $p); $template = 'This value should be of type {{ type }}.'; - $data = [ + $error = [ self::TYPE => 'https://symfony.com/errors/validation', self::TITLE => 'Validation Failed', 'violations' => array_map( @@ -84,27 +84,27 @@ public function normalize(mixed $object, ?string $format = null, array $context $exception->getErrors() ), ]; - $data['detail'] = implode("\n", array_map(fn ($e) => $e['propertyPath'].': '.$e['title'], $data['violations'])); + $error['detail'] = implode("\n", array_map(fn ($e) => $e['propertyPath'].': '.$e['title'], $error['violations'])); } elseif (($exception instanceof ValidationFailedException || $exception instanceof MessageValidationFailedException) && $this->serializer instanceof NormalizerInterface && $this->serializer->supportsNormalization($exception->getViolations(), $format, $context) ) { - $data = $this->serializer->normalize($exception->getViolations(), $format, $context); + $error = $this->serializer->normalize($exception->getViolations(), $format, $context); } } - $data = [ - self::TYPE => $data[self::TYPE] ?? $context[self::TYPE] ?? 'https://tools.ietf.org/html/rfc2616#section-10', - self::TITLE => $data[self::TITLE] ?? $context[self::TITLE] ?? 'An error occurred', + $error = [ + self::TYPE => $error[self::TYPE] ?? $context[self::TYPE] ?? 'https://tools.ietf.org/html/rfc2616#section-10', + self::TITLE => $error[self::TITLE] ?? $context[self::TITLE] ?? 'An error occurred', self::STATUS => $context[self::STATUS] ?? $object->getStatusCode(), - 'detail' => $data['detail'] ?? ($debug ? $object->getMessage() : $object->getStatusText()), - ] + $data; + 'detail' => $error['detail'] ?? ($debug ? $object->getMessage() : $object->getStatusText()), + ] + $error; if ($debug) { - $data['class'] = $object->getClass(); - $data['trace'] = $object->getTrace(); + $error['class'] = $object->getClass(); + $error['trace'] = $object->getTrace(); } - return $data; + return $error; } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool diff --git a/Normalizer/TranslatableNormalizer.php b/Normalizer/TranslatableNormalizer.php index 463616e72..d365598b5 100644 --- a/Normalizer/TranslatableNormalizer.php +++ b/Normalizer/TranslatableNormalizer.php @@ -34,13 +34,13 @@ public function __construct( /** * @throws InvalidArgumentException */ - public function normalize(mixed $object, ?string $format = null, array $context = []): string + public function normalize(mixed $data, ?string $format = null, array $context = []): string { - if (!$object instanceof TranslatableInterface) { - throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The object must implement the "%s".', TranslatableInterface::class), $object, [TranslatableInterface::class]); + if (!$data instanceof TranslatableInterface) { + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The object must implement the "%s".', TranslatableInterface::class), $data, [TranslatableInterface::class]); } - return $object->trans($this->translator, $context[self::NORMALIZATION_LOCALE_KEY] ?? $this->defaultContext[self::NORMALIZATION_LOCALE_KEY]); + return $data->trans($this->translator, $context[self::NORMALIZATION_LOCALE_KEY] ?? $this->defaultContext[self::NORMALIZATION_LOCALE_KEY]); } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool diff --git a/Normalizer/UidNormalizer.php b/Normalizer/UidNormalizer.php index 0bee1f704..eb624eed1 100644 --- a/Normalizer/UidNormalizer.php +++ b/Normalizer/UidNormalizer.php @@ -48,16 +48,13 @@ public function getSupportedTypes(?string $format): array ]; } - /** - * @param AbstractUid $object - */ - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { return match ($context[self::NORMALIZATION_FORMAT_KEY] ?? $this->defaultContext[self::NORMALIZATION_FORMAT_KEY]) { - self::NORMALIZATION_FORMAT_CANONICAL => (string) $object, - self::NORMALIZATION_FORMAT_BASE58 => $object->toBase58(), - self::NORMALIZATION_FORMAT_BASE32 => $object->toBase32(), - self::NORMALIZATION_FORMAT_RFC4122 => $object->toRfc4122(), + self::NORMALIZATION_FORMAT_CANONICAL => (string) $data, + self::NORMALIZATION_FORMAT_BASE58 => $data->toBase58(), + self::NORMALIZATION_FORMAT_BASE32 => $data->toBase32(), + self::NORMALIZATION_FORMAT_RFC4122 => $data->toRfc4122(), default => throw new LogicException(\sprintf('The "%s" format is not valid.', $context[self::NORMALIZATION_FORMAT_KEY] ?? $this->defaultContext[self::NORMALIZATION_FORMAT_KEY])), }; } diff --git a/SerializerInterface.php b/SerializerInterface.php index b883dbea5..7ee63a777 100644 --- a/SerializerInterface.php +++ b/SerializerInterface.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Serializer; +use Symfony\Component\Serializer\Exception\ExceptionInterface; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; + /** * @author Jordi Boggiano */ @@ -20,6 +24,10 @@ interface SerializerInterface * Serializes data in the appropriate format. * * @param array $context Options normalizers/encoders have access to + * + * @throws NotNormalizableValueException Occurs when a value cannot be normalized + * @throws UnexpectedValueException Occurs when a value cannot be encoded + * @throws ExceptionInterface Occurs for all the other cases of serialization-related errors */ public function serialize(mixed $data, string $format, array $context = []): string; @@ -35,6 +43,10 @@ public function serialize(mixed $data, string $format, array $context = []): str * @psalm-return (TType is class-string ? TObject : mixed) * * @phpstan-return ($type is class-string ? TObject : mixed) + * + * @throws NotNormalizableValueException Occurs when a value cannot be denormalized + * @throws UnexpectedValueException Occurs when a value cannot be decoded + * @throws ExceptionInterface Occurs for all the other cases of serialization-related errors */ public function deserialize(mixed $data, string $type, string $format, array $context = []): mixed; } diff --git a/Tests/Attribute/ContextTest.php b/Tests/Attribute/ContextTest.php index cfe175050..e012f7bf7 100644 --- a/Tests/Attribute/ContextTest.php +++ b/Tests/Attribute/ContextTest.php @@ -50,9 +50,9 @@ public function testAsFirstArg() $context = new Context(['foo' => 'bar']); self::assertSame(['foo' => 'bar'], $context->getContext()); - self::assertEmpty($context->getNormalizationContext()); - self::assertEmpty($context->getDenormalizationContext()); - self::assertEmpty($context->getGroups()); + self::assertSame([], $context->getNormalizationContext()); + self::assertSame([], $context->getDenormalizationContext()); + self::assertSame([], $context->getGroups()); } public function testAsContextArg() @@ -60,9 +60,9 @@ public function testAsContextArg() $context = new Context(context: ['foo' => 'bar']); self::assertSame(['foo' => 'bar'], $context->getContext()); - self::assertEmpty($context->getNormalizationContext()); - self::assertEmpty($context->getDenormalizationContext()); - self::assertEmpty($context->getGroups()); + self::assertSame([], $context->getNormalizationContext()); + self::assertSame([], $context->getDenormalizationContext()); + self::assertSame([], $context->getGroups()); } /** @@ -86,7 +86,7 @@ public static function provideValidInputs(): iterable -normalizationContext: [] -denormalizationContext: [] } -DUMP +DUMP, ]; yield 'named arguments: with normalization context option' => [ @@ -100,7 +100,7 @@ public static function provideValidInputs(): iterable ] -denormalizationContext: [] } -DUMP +DUMP, ]; yield 'named arguments: with denormalization context option' => [ @@ -114,7 +114,7 @@ public static function provideValidInputs(): iterable "foo" => "bar", ] } -DUMP +DUMP, ]; yield 'named arguments: with groups option as string' => [ @@ -130,7 +130,7 @@ public static function provideValidInputs(): iterable -normalizationContext: [] -denormalizationContext: [] } -DUMP +DUMP, ]; yield 'named arguments: with groups option as array' => [ @@ -147,7 +147,7 @@ public static function provideValidInputs(): iterable -normalizationContext: [] -denormalizationContext: [] } -DUMP +DUMP, ]; } } diff --git a/Tests/Attribute/DiscriminatorMapTest.php b/Tests/Attribute/DiscriminatorMapTest.php index 497bc6201..e33fe7e06 100644 --- a/Tests/Attribute/DiscriminatorMapTest.php +++ b/Tests/Attribute/DiscriminatorMapTest.php @@ -40,9 +40,16 @@ public function testExceptionWithEmptyTypeProperty() new DiscriminatorMap(typeProperty: '', mapping: ['foo' => 'FooClass']); } - public function testExceptionWitEmptyMappingProperty() + public function testExceptionWithEmptyMappingProperty() { $this->expectException(InvalidArgumentException::class); new DiscriminatorMap(typeProperty: 'type', mapping: []); } + + public function testExceptionWithMissingDefaultTypeInMapping() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(\sprintf('Default type "bar" given to "%s" must be present in "mapping" types.', DiscriminatorMap::class)); + new DiscriminatorMap(typeProperty: 'type', mapping: ['foo' => 'FooClass'], defaultType: 'bar'); + } } diff --git a/Tests/CacheWarmer/CompiledClassMetadataCacheWarmerTest.php b/Tests/CacheWarmer/CompiledClassMetadataCacheWarmerTest.php index 9d354270e..c9f5081b6 100644 --- a/Tests/CacheWarmer/CompiledClassMetadataCacheWarmerTest.php +++ b/Tests/CacheWarmer/CompiledClassMetadataCacheWarmerTest.php @@ -18,6 +18,9 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryCompiler; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +/** + * @group legacy + */ final class CompiledClassMetadataCacheWarmerTest extends TestCase { public function testItImplementsCacheWarmerInterface() diff --git a/Tests/Command/DebugCommandTest.php b/Tests/Command/DebugCommandTest.php index 7bfdf93dd..ffba4f497 100644 --- a/Tests/Command/DebugCommandTest.php +++ b/Tests/Command/DebugCommandTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Tests\Dummy\DummyClassOne; +use Symfony\Component\Serializer\Tests\Dummy\DummyClassWithDiscriminatorMap; /** * @author Loïc Frémont @@ -79,6 +80,41 @@ public function testOutputWithClassArgument() ); } + public function testOutputWithDiscriminatorMapClass() + { + $command = new DebugCommand(new ClassMetadataFactory(new AttributeLoader())); + + $tester = new CommandTester($command); + $tester->execute(['class' => DummyClassWithDiscriminatorMap::class], ['decorated' => false]); + + $this->assertSame(<< [], | + | | "maxDepth" => null, | + | | "serializedName" => null, | + | | "serializedPath" => null, | + | | "ignore" => false, | + | | "normalizationContexts" => [], | + | | "denormalizationContexts" => [], | + | | "discriminatorMap" => [ | + | | "one" => "Symfony\Component\Serializer\Tests\Dummy\DummyClassOne", | + | | "two" => "Symfony\Component\Serializer\Tests\Dummy\DummyClassTwo" | + | | ] | + | | ] | + +----------+------------------------------------------------------------------------+ + + TXT, + $tester->getDisplay(true), + ); + } + public function testOutputWithInvalidClassArgument() { $serializer = $this->createMock(ClassMetadataFactoryInterface::class); diff --git a/Tests/Context/Encoder/XmlEncoderContextBuilderTest.php b/Tests/Context/Encoder/XmlEncoderContextBuilderTest.php index 2f71c6012..4175751b0 100644 --- a/Tests/Context/Encoder/XmlEncoderContextBuilderTest.php +++ b/Tests/Context/Encoder/XmlEncoderContextBuilderTest.php @@ -47,6 +47,7 @@ public function testWithers(array $values) ->withVersion($values[XmlEncoder::VERSION]) ->withCdataWrapping($values[XmlEncoder::CDATA_WRAPPING]) ->withCdataWrappingPattern($values[XmlEncoder::CDATA_WRAPPING_PATTERN]) + ->withIgnoreEmptyAttributes($values[XmlEncoder::IGNORE_EMPTY_ATTRIBUTES]) ->toArray(); $this->assertSame($values, $context); @@ -69,6 +70,7 @@ public static function withersDataProvider(): iterable XmlEncoder::VERSION => '1.0', XmlEncoder::CDATA_WRAPPING => false, XmlEncoder::CDATA_WRAPPING_PATTERN => '/[<>&"\']/', + XmlEncoder::IGNORE_EMPTY_ATTRIBUTES => true, ]]; yield 'With null values' => [[ @@ -86,6 +88,7 @@ public static function withersDataProvider(): iterable XmlEncoder::VERSION => null, XmlEncoder::CDATA_WRAPPING => null, XmlEncoder::CDATA_WRAPPING_PATTERN => null, + XmlEncoder::IGNORE_EMPTY_ATTRIBUTES => null, ]]; } } diff --git a/Tests/DataCollector/SerializerDataCollectorTest.php b/Tests/DataCollector/SerializerDataCollectorTest.php index 6a26565a8..0ea4ad3aa 100644 --- a/Tests/DataCollector/SerializerDataCollectorTest.php +++ b/Tests/DataCollector/SerializerDataCollectorTest.php @@ -373,17 +373,17 @@ public function testNamedSerializers() $this->assertSame('default', $collectedData['normalize'][0]['name']); $this->assertSame('ObjectNormalizer', $collectedData['normalize'][0]['normalizer']['class']); - $this->assertEmpty($collectedData['encode']); - $this->assertEmpty($collectedData['deserialize']); - $this->assertEmpty($collectedData['denormalize']); - $this->assertEmpty($collectedData['decode']); + $this->assertSame([], $collectedData['encode']); + $this->assertSame([], $collectedData['deserialize']); + $this->assertSame([], $collectedData['denormalize']); + $this->assertSame([], $collectedData['decode']); $this->assertSame(4, $dataCollector->getHandledCount('api')); $collectedData = $dataCollector->getData('api'); - $this->assertEmpty($collectedData['serialize']); - $this->assertEmpty($collectedData['normalize']); + $this->assertSame([], $collectedData['serialize']); + $this->assertSame([], $collectedData['normalize']); $this->assertSame('api', $collectedData['encode'][0]['name']); $this->assertSame('JsonEncoder', $collectedData['encode'][0]['encoder']['class']); diff --git a/Tests/Debug/TraceableSerializerTest.php b/Tests/Debug/TraceableSerializerTest.php index d697b270f..909243321 100644 --- a/Tests/Debug/TraceableSerializerTest.php +++ b/Tests/Debug/TraceableSerializerTest.php @@ -142,7 +142,7 @@ public function deserialize(mixed $data, string $type, string $format, array $co return 'deserialized'; } - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { return 'normalized'; } diff --git a/Tests/DependencyInjection/SerializerPassTest.php b/Tests/DependencyInjection/SerializerPassTest.php index 88ec02b87..dc8d0c757 100644 --- a/Tests/DependencyInjection/SerializerPassTest.php +++ b/Tests/DependencyInjection/SerializerPassTest.php @@ -19,6 +19,8 @@ use Symfony\Component\Serializer\Debug\TraceableNormalizer; use Symfony\Component\Serializer\Debug\TraceableSerializer; use Symfony\Component\Serializer\DependencyInjection\SerializerPass; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\SerializerInterface; @@ -656,6 +658,8 @@ public function testNamedSerializersAreRegistered() $this->assertTrue($container->hasAlias(\sprintf('%s $apiSerializer', SerializerInterface::class))); $this->assertTrue($container->hasDefinition('serializer.api2')); $this->assertTrue($container->hasAlias(\sprintf('%s $api2Serializer', SerializerInterface::class))); + $this->assertTrue($container->hasAlias(\sprintf('%s $api2Normalizer', NormalizerInterface::class))); + $this->assertTrue($container->hasAlias(\sprintf('%s $api2Denormalizer', DenormalizerInterface::class))); } public function testNormalizersAndEncodersAreDecoratedAndOrderedWhenCollectingDataForNamedSerializers() diff --git a/Tests/Dummy/DummyClassTwo.php b/Tests/Dummy/DummyClassTwo.php new file mode 100644 index 000000000..8bb5311e1 --- /dev/null +++ b/Tests/Dummy/DummyClassTwo.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Dummy; + +class DummyClassTwo +{ +} diff --git a/Tests/Dummy/DummyClassWithDiscriminatorMap.php b/Tests/Dummy/DummyClassWithDiscriminatorMap.php new file mode 100644 index 000000000..50044bf24 --- /dev/null +++ b/Tests/Dummy/DummyClassWithDiscriminatorMap.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Dummy; + +use Symfony\Component\Serializer\Attribute\DiscriminatorMap; + +#[DiscriminatorMap(typeProperty: 'type', mapping: [ + 'one' => DummyClassOne::class, + 'two' => DummyClassTwo::class, +])] +class DummyClassWithDiscriminatorMap +{ + public string $type; +} diff --git a/Tests/Encoder/XmlEncoderTest.php b/Tests/Encoder/XmlEncoderTest.php index 0eb332e80..78699983f 100644 --- a/Tests/Encoder/XmlEncoderTest.php +++ b/Tests/Encoder/XmlEncoderTest.php @@ -1004,4 +1004,17 @@ private function createXmlWithDateTimeField(): string ', $this->exampleDateTimeString); } + + public function testEncodeIgnoringEmptyAttribute() + { + $expected = <<<'XML' + +Test + +XML; + + $data = ['#' => 'Test', '@attribute' => '', '@attribute2' => null]; + + $this->assertEquals($expected, $this->encoder->encode($data, 'xml', ['ignore_empty_attributes' => true])); + } } diff --git a/Tests/Fixtures/AbstractNormalizerDummy.php b/Tests/Fixtures/AbstractNormalizerDummy.php index f5bf565a1..3945a1080 100644 --- a/Tests/Fixtures/AbstractNormalizerDummy.php +++ b/Tests/Fixtures/AbstractNormalizerDummy.php @@ -30,7 +30,7 @@ public function getAllowedAttributes(string|object $classOrObject, array $contex return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString); } - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { } diff --git a/Tests/Fixtures/Attributes/AbstractDummy.php b/Tests/Fixtures/Attributes/AbstractDummy.php index a8c15fccb..f85874c1b 100644 --- a/Tests/Fixtures/Attributes/AbstractDummy.php +++ b/Tests/Fixtures/Attributes/AbstractDummy.php @@ -17,7 +17,7 @@ 'first' => AbstractDummyFirstChild::class, 'second' => AbstractDummySecondChild::class, 'third' => AbstractDummyThirdChild::class, -])] +], defaultType: 'third')] abstract class AbstractDummy { public $foo; diff --git a/Tests/Fixtures/Attributes/SerializedNameAttributeDummy.php b/Tests/Fixtures/Attributes/SerializedNameAttributeDummy.php new file mode 100644 index 000000000..968ea2bb9 --- /dev/null +++ b/Tests/Fixtures/Attributes/SerializedNameAttributeDummy.php @@ -0,0 +1,11 @@ +serializer->serialize($envelope->message, 'xml'); + $xmlContent = $this->serializer->serialize($data->message, 'xml'); $encodedContent = base64_encode($xmlContent); diff --git a/Tests/Fixtures/EnvelopedMessageNormalizer.php b/Tests/Fixtures/EnvelopedMessageNormalizer.php index 812dbf015..e907b00a5 100644 --- a/Tests/Fixtures/EnvelopedMessageNormalizer.php +++ b/Tests/Fixtures/EnvelopedMessageNormalizer.php @@ -18,10 +18,10 @@ */ class EnvelopedMessageNormalizer implements NormalizerInterface { - public function normalize($message, ?string $format = null, array $context = []): array + public function normalize(mixed $data, ?string $format = null, array $context = []): array { return [ - 'text' => $message->text, + 'text' => $data->text, ]; } diff --git a/Tests/Fixtures/serialization.xml b/Tests/Fixtures/serialization.xml index 512736db4..a03c546ed 100644 --- a/Tests/Fixtures/serialization.xml +++ b/Tests/Fixtures/serialization.xml @@ -35,7 +35,7 @@ - + diff --git a/Tests/Fixtures/serialization.yml b/Tests/Fixtures/serialization.yml index 4371016e3..ef6129243 100644 --- a/Tests/Fixtures/serialization.yml +++ b/Tests/Fixtures/serialization.yml @@ -32,6 +32,7 @@ mapping: first: 'Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild' second: 'Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild' + default_type: first attributes: foo: ~ 'Symfony\Component\Serializer\Tests\Fixtures\Attributes\IgnoreDummy': diff --git a/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php b/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php index 40dcb5015..aec9bcc91 100644 --- a/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php +++ b/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php @@ -15,6 +15,10 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryCompiler; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummy; +use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild; +use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild; +use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyThirdChild; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\MaxDepthDummy; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\SerializedNameDummy; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\SerializedPathDummy; @@ -40,6 +44,7 @@ public function testItDumpMetadata() $classMetatadataFactory = new ClassMetadataFactory(new AttributeLoader()); $dummyMetadata = $classMetatadataFactory->getMetadataFor(Dummy::class); + $abstractDummyMetadata = $classMetatadataFactory->getMetadataFor(AbstractDummy::class); $maxDepthDummyMetadata = $classMetatadataFactory->getMetadataFor(MaxDepthDummy::class); $serializedNameDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedNameDummy::class); $serializedPathDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedPathDummy::class); @@ -47,6 +52,7 @@ public function testItDumpMetadata() $code = (new ClassMetadataFactoryCompiler())->compile([ $dummyMetadata, + $abstractDummyMetadata, $maxDepthDummyMetadata, $serializedNameDummyMetadata, $serializedPathDummyMetadata, @@ -56,7 +62,7 @@ public function testItDumpMetadata() file_put_contents($this->dumpPath, $code); $compiledMetadata = require $this->dumpPath; - $this->assertCount(5, $compiledMetadata); + $this->assertCount(6, $compiledMetadata); $this->assertArrayHasKey(Dummy::class, $compiledMetadata); $this->assertEquals([ @@ -69,6 +75,22 @@ public function testItDumpMetadata() null, ], $compiledMetadata[Dummy::class]); + $this->assertArrayHasKey(AbstractDummy::class, $compiledMetadata); + $this->assertEquals([ + [ + 'foo' => [[], null, null, null], + ], + [ + 'type', + [ + 'first' => AbstractDummyFirstChild::class, + 'second' => AbstractDummySecondChild::class, + 'third' => AbstractDummyThirdChild::class, + ], + 'third', + ], + ], $compiledMetadata[AbstractDummy::class]); + $this->assertArrayHasKey(MaxDepthDummy::class, $compiledMetadata); $this->assertEquals([ [ diff --git a/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php b/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php index ff54fb96b..e77a8bf3e 100644 --- a/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php +++ b/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php @@ -21,6 +21,8 @@ /** * @author Fabien Bourigault + * + * @group legacy */ final class CompiledClassMetadataFactoryTest extends TestCase { diff --git a/Tests/Mapping/Loader/AttributeLoaderTest.php b/Tests/Mapping/Loader/AttributeLoaderTest.php index 2af244a6f..16d64f25d 100644 --- a/Tests/Mapping/Loader/AttributeLoaderTest.php +++ b/Tests/Mapping/Loader/AttributeLoaderTest.php @@ -85,7 +85,7 @@ public function testLoadDiscriminatorMap() 'first' => AbstractDummyFirstChild::class, 'second' => AbstractDummySecondChild::class, 'third' => AbstractDummyThirdChild::class, - ])); + ], 'third')); $expected->addAttributeMetadata(new AttributeMetadata('foo')); $expected->getReflectionClass(); diff --git a/Tests/Mapping/Loader/XmlFileLoaderTest.php b/Tests/Mapping/Loader/XmlFileLoaderTest.php index c0298129e..45b5aeb10 100644 --- a/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -109,7 +109,7 @@ public function testLoadDiscriminatorMap() $expected = new ClassMetadata(AbstractDummy::class, new ClassDiscriminatorMapping('type', [ 'first' => AbstractDummyFirstChild::class, 'second' => AbstractDummySecondChild::class, - ])); + ], 'second')); $expected->addAttributeMetadata(new AttributeMetadata('foo')); diff --git a/Tests/Mapping/Loader/YamlFileLoaderTest.php b/Tests/Mapping/Loader/YamlFileLoaderTest.php index 48e95aecd..4997f1648 100644 --- a/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -126,7 +126,7 @@ public function testLoadDiscriminatorMap() $expected = new ClassMetadata(AbstractDummy::class, new ClassDiscriminatorMapping('type', [ 'first' => AbstractDummyFirstChild::class, 'second' => AbstractDummySecondChild::class, - ])); + ], 'first')); $expected->addAttributeMetadata(new AttributeMetadata('foo')); diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index 270b65f33..7068b8c8e 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -560,6 +560,41 @@ public function hasMetadataFor($value): bool $this->assertInstanceOf(DummySecondChildQuux::class, $normalizedData->quux); } + public function testDenormalizeWithDiscriminatorMapUsesCorrectClassnameWithDefaultType() + { + $factory = new ClassMetadataFactory(new AttributeLoader()); + + $loaderMock = new class implements ClassMetadataFactoryInterface { + public function getMetadataFor($value): ClassMetadataInterface + { + if (AbstractDummy::class === $value) { + return new ClassMetadata( + AbstractDummy::class, + new ClassDiscriminatorMapping('type', [ + 'first' => AbstractDummyFirstChild::class, + 'second' => AbstractDummySecondChild::class, + ], 'second') + ); + } + + throw new InvalidArgumentException(\sprintf('"%s" is not handled.', $value)); + } + + public function hasMetadataFor($value): bool + { + return AbstractDummy::class === $value; + } + }; + + $discriminatorResolver = new ClassDiscriminatorFromClassMetadata($loaderMock); + $normalizer = new AbstractObjectNormalizerDummy($factory, null, new PhpDocExtractor(), $discriminatorResolver); + $serializer = new Serializer([$normalizer]); + $normalizer->setSerializer($serializer); + $normalizedData = $normalizer->denormalize(['foo' => 'foo', 'baz' => 'baz', 'quux' => ['value' => 'quux']], AbstractDummy::class); + + $this->assertInstanceOf(DummySecondChildQuux::class, $normalizedData->quux); + } + public function testDenormalizeWithDiscriminatorMapAndObjectToPopulateUsesCorrectClassname() { $factory = new ClassMetadataFactory(new AttributeLoader()); diff --git a/Tests/Normalizer/NumberNormalizerTest.php b/Tests/Normalizer/NumberNormalizerTest.php new file mode 100644 index 000000000..56d4776b2 --- /dev/null +++ b/Tests/Normalizer/NumberNormalizerTest.php @@ -0,0 +1,225 @@ + + * + * 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 BcMath\Number; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Normalizer\NumberNormalizer; + +class NumberNormalizerTest extends TestCase +{ + private NumberNormalizer $normalizer; + + protected function setUp(): void + { + $this->normalizer = new NumberNormalizer(); + } + + /** + * @dataProvider supportsNormalizationProvider + */ + public function testSupportsNormalization(mixed $data, bool $expected) + { + $this->assertSame($expected, $this->normalizer->supportsNormalization($data)); + } + + public static function supportsNormalizationProvider(): iterable + { + if (class_exists(\GMP::class)) { + yield 'GMP object' => [new \GMP('0b111'), true]; + } + + if (class_exists(Number::class)) { + yield 'Number object' => [new Number('1.23'), true]; + } + + yield 'object with similar properties as Number' => [(object) ['value' => '1.23', 'scale' => 2], false]; + yield 'stdClass' => [new \stdClass(), false]; + yield 'string' => ['1.23', false]; + yield 'float' => [1.23, false]; + yield 'null' => [null, false]; + } + + /** + * @requires PHP 8.4 + * @requires extension bcmath + * + * @dataProvider normalizeGoodBcMathNumberValueProvider + */ + public function testNormalizeBcMathNumber(Number $data, string $expected) + { + $this->assertSame($expected, $this->normalizer->normalize($data)); + } + + public static function normalizeGoodBcMathNumberValueProvider(): iterable + { + if (class_exists(Number::class)) { + yield 'Number with scale=2' => [new Number('1.23'), '1.23']; + yield 'Number with scale=0' => [new Number('1'), '1']; + yield 'Number with integer' => [new Number(123), '123']; + } + } + + /** + * @requires extension gmp + * + * @dataProvider normalizeGoodGmpValueProvider + */ + public function testNormalizeGmp(\GMP $data, string $expected) + { + $this->assertSame($expected, $this->normalizer->normalize($data)); + } + + public static function normalizeGoodGmpValueProvider(): iterable + { + if (class_exists(\GMP::class)) { + yield 'GMP hex' => [new \GMP('0x10'), '16']; + yield 'GMP base=10' => [new \GMP('10'), '10']; + } + } + + /** + * @dataProvider normalizeBadValueProvider + */ + public function testNormalizeBadValueThrows(mixed $data) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The data must be an instance of "BcMath\Number" or "GMP".'); + + $this->normalizer->normalize($data); + } + + public static function normalizeBadValueProvider(): iterable + { + yield 'stdClass' => [new \stdClass()]; + yield 'string' => ['1.23']; + yield 'null' => [null]; + } + + /** + * @requires PHP 8.4 + * @requires extension bcmath + */ + public function testSupportsBcMathNumberDenormalization() + { + $this->assertFalse($this->normalizer->supportsDenormalization(null, Number::class)); + } + + /** + * @requires extension gmp + */ + public function testSupportsGmpDenormalization() + { + $this->assertFalse($this->normalizer->supportsDenormalization(null, \GMP::class)); + } + + public function testDoesNotSupportOtherValuesDenormalization() + { + $this->assertFalse($this->normalizer->supportsDenormalization(null, \stdClass::class)); + } + + /** + * @requires PHP 8.4 + * @requires extension bcmath + * + * @dataProvider denormalizeGoodBcMathNumberValueProvider + */ + public function testDenormalizeBcMathNumber(string|int $data, string $type, Number $expected) + { + $this->assertEquals($expected, $this->normalizer->denormalize($data, $type)); + } + + public static function denormalizeGoodBcMathNumberValueProvider(): iterable + { + if (class_exists(Number::class)) { + yield 'Number, string with decimal point' => ['1.23', Number::class, new Number('1.23')]; + yield 'Number, integer as string' => ['123', Number::class, new Number('123')]; + yield 'Number, integer' => [123, Number::class, new Number('123')]; + } + } + + /** + * @requires extension gmp + * + * @dataProvider denormalizeGoodGmpValueProvider + */ + public function testDenormalizeGmp(string|int $data, string $type, \GMP $expected) + { + $this->assertEquals($expected, $this->normalizer->denormalize($data, $type)); + } + + public static function denormalizeGoodGmpValueProvider(): iterable + { + if (class_exists(\GMP::class)) { + yield 'GMP, large number' => ['9223372036854775808', \GMP::class, new \GMP('9223372036854775808')]; + yield 'GMP, integer' => [123, \GMP::class, new \GMP('123')]; + } + } + + /** + * @requires PHP 8.4 + * @requires extension bcmath + * + * @dataProvider denormalizeBadBcMathNumberValueProvider + */ + public function testDenormalizeBadBcMathNumberValueThrows(mixed $data, string $type, string $expectedException, string $expectedExceptionMessage) + { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->normalizer->denormalize($data, $type); + } + + public static function denormalizeBadBcMathNumberValueProvider(): iterable + { + $stringOrDecimalExpectedMessage = 'The data must be a "string" representing a decimal number, or an "int".'; + yield 'Number, null' => [null, Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; + yield 'Number, boolean' => [true, Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; + yield 'Number, object' => [new \stdClass(), Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; + yield 'Number, non-numeric string' => ['foobar', Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; + yield 'Number, float' => [1.23, Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; + } + + /** + * @requires extension gmp + * + * @dataProvider denormalizeBadGmpValueProvider + */ + public function testDenormalizeBadGmpValueThrows(mixed $data, string $type, string $expectedException, string $expectedExceptionMessage) + { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->normalizer->denormalize($data, $type); + } + + public static function denormalizeBadGmpValueProvider(): iterable + { + $stringOrIntExpectedMessage = 'The data must be a "string" representing an integer, or an "int".'; + yield 'GMP, null' => [null, \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; + yield 'GMP, boolean' => [true, \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; + yield 'GMP, object' => [new \stdClass(), \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; + yield 'GMP, non-numeric string' => ['foobar', \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; + yield 'GMP, scale > 0' => ['1.23', \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; + yield 'GMP, float' => [1.23, \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; + } + + public function testDenormalizeBadValueThrows() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Only "BcMath\Number" and "GMP" types are supported.'); + + $this->normalizer->denormalize('1.23', \stdClass::class); + } +} diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index d45586b44..439dce056 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -199,8 +199,7 @@ public function testDenormalizeEmptyXmlArray() 'xml' ); - $this->assertIsArray($obj->bar); - $this->assertEmpty($obj->bar); + $this->assertSame([], $obj->bar); } public function testDenormalizeWithObject() diff --git a/Tests/Normalizer/TestNormalizer.php b/Tests/Normalizer/TestNormalizer.php index 941fef42b..6c6e93066 100644 --- a/Tests/Normalizer/TestNormalizer.php +++ b/Tests/Normalizer/TestNormalizer.php @@ -20,7 +20,7 @@ */ class TestNormalizer implements NormalizerInterface { - public function normalize($object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { return null; } diff --git a/Tests/SerializerTest.php b/Tests/SerializerTest.php index bbf82920c..16b88a32d 100644 --- a/Tests/SerializerTest.php +++ b/Tests/SerializerTest.php @@ -33,6 +33,7 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; @@ -54,6 +55,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummy; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild; +use Symfony\Component\Serializer\Tests\Fixtures\Attributes\SerializedNameAttributeDummy; use Symfony\Component\Serializer\Tests\Fixtures\DenormalizableDummy; use Symfony\Component\Serializer\Tests\Fixtures\DummyFirstChildQuux; use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface; @@ -65,7 +67,6 @@ use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumProperty; use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull; use Symfony\Component\Serializer\Tests\Fixtures\DummyWithVariadicParameter; -use Symfony\Component\Serializer\Tests\Fixtures\DummyWithVariadicProperty; use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy; use Symfony\Component\Serializer\Tests\Fixtures\FooImplementationDummy; use Symfony\Component\Serializer\Tests\Fixtures\FooInterfaceDummyDenormalizer; @@ -795,6 +796,18 @@ public function testDeserializeNullableIntInXml() $this->assertNull($obj->value); } + public function testDeserializeIntAsStringPropertyInXML() + { + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $nameConverter = new MetadataAwareNameConverter($classMetadataFactory); + $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); + $serializer = new Serializer([new ObjectNormalizer($classMetadataFactory, $nameConverter, null, $extractor)], ['xml' => new XmlEncoder()]); + + $obj = $serializer->deserialize('', SerializedNameAttributeDummy::class, 'xml'); + + $this->assertSame('123', $obj->foo); + } + public function testUnionTypeDeserializable() { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());