Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[Serializer] Add NumberNormalizer #59670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions 2 psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
<referencedClass name="UnitEnum"/>
<!-- These classes have been added in PHP 8.2 -->
<referencedClass name="Random\*"/>
<!-- These classes have been added in PHP 8.4 -->
<referencedClass name="BcMath\Number"/>
</errorLevel>
</UndefinedClass>
<UndefinedDocblockClass>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NumberNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\String\LazyString;
Expand Down Expand Up @@ -1933,6 +1934,11 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
$container->removeDefinition('serializer.normalizer.mime_message');
}

// BC layer Serializer < 7.3
if (!class_exists(NumberNormalizer::class)) {
$container->removeDefinition('serializer.normalizer.number');
}

// BC layer Serializer < 7.2
if (!class_exists(SnakeCaseToCamelCaseNameConverter::class)) {
$container->removeDefinition('serializer.name_converter.snake_case_to_camel_case');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NumberNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\ProblemNormalizer;
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
Expand Down Expand Up @@ -221,5 +222,8 @@

->set('serializer.normalizer.backed_enum', BackedEnumNormalizer::class)
->tag('serializer.normalizer', ['built_in' => true, 'priority' => -915])

->set('serializer.normalizer.number', NumberNormalizer::class)
->tag('serializer.normalizer', ['built_in' => true, 'priority' => -915])
;
};
1 change: 1 addition & 0 deletions 1 src/Symfony/Component/Serializer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

* Deprecate the `CompiledClassMetadataFactory` and `CompiledClassMetadataCacheWarmer` classes
* Register `NormalizerInterface` and `DenormalizerInterface` aliases for named serializers
* Add `NumberNormalizer` to normalize `BcMath\Number` and `GMP` as `string`

7.2
---
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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

Check failure on line 23 in src/Symfony/Component/Serializer/Normalizer/NumberNormalizer.php

View workflow job for this annotation

GitHub Actions / Psalm

LessSpecificImplementedReturnType

src/Symfony/Component/Serializer/Normalizer/NumberNormalizer.php:23:57: LessSpecificImplementedReturnType: The inherited return type 'array<string, bool|null>' for Symfony\Component\Serializer\Normalizer\NormalizerInterface::getSupportedTypes is more specific than the implemented return type for Symfony\Component\Serializer\Normalizer\NumberNormalizer::getsupportedtypes 'array<array-key, mixed>' (see https://psalm.dev/166)

Check failure on line 23 in src/Symfony/Component/Serializer/Normalizer/NumberNormalizer.php

View workflow job for this annotation

GitHub Actions / Psalm

LessSpecificImplementedReturnType

src/Symfony/Component/Serializer/Normalizer/NumberNormalizer.php:23:57: LessSpecificImplementedReturnType: The inherited return type 'array<string, bool|null>' for Symfony\Component\Serializer\Normalizer\DenormalizerInterface::getSupportedTypes is more specific than the implemented return type for Symfony\Component\Serializer\Normalizer\NumberNormalizer::getsupportedtypes 'array<array-key, mixed>' (see https://psalm.dev/166)

Check failure on line 23 in src/Symfony/Component/Serializer/Normalizer/NumberNormalizer.php

View workflow job for this annotation

GitHub Actions / Psalm

LessSpecificImplementedReturnType

src/Symfony/Component/Serializer/Normalizer/NumberNormalizer.php:23:57: LessSpecificImplementedReturnType: The inherited return type 'array<string, bool|null>' for Symfony\Component\Serializer\Normalizer\NormalizerInterface::getSupportedTypes is more specific than the implemented return type for Symfony\Component\Serializer\Normalizer\NumberNormalizer::getsupportedtypes 'array<array-key, mixed>' (see https://psalm.dev/166)

Check failure on line 23 in src/Symfony/Component/Serializer/Normalizer/NumberNormalizer.php

View workflow job for this annotation

GitHub Actions / Psalm

LessSpecificImplementedReturnType

src/Symfony/Component/Serializer/Normalizer/NumberNormalizer.php:23:57: LessSpecificImplementedReturnType: The inherited return type 'array<string, bool|null>' for Symfony\Component\Serializer\Normalizer\DenormalizerInterface::getSupportedTypes is more specific than the implemented return type for Symfony\Component\Serializer\Normalizer\NumberNormalizer::getsupportedtypes 'array<array-key, mixed>' (see https://psalm.dev/166)
{
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;

Check failure on line 37 in src/Symfony/Component/Serializer/Normalizer/NumberNormalizer.php

View workflow job for this annotation

GitHub Actions / Psalm

InvalidCast

src/Symfony/Component/Serializer/Normalizer/NumberNormalizer.php:37:25: InvalidCast: GMP cannot be cast to string (see https://psalm.dev/103)

Check failure on line 37 in src/Symfony/Component/Serializer/Normalizer/NumberNormalizer.php

View workflow job for this annotation

GitHub Actions / Psalm

InvalidCast

src/Symfony/Component/Serializer/Normalizer/NumberNormalizer.php:37:25: InvalidCast: GMP cannot be cast to string (see https://psalm.dev/103)
fabpot marked this conversation as resolved.
Show resolved Hide resolved
}

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;
xabbuh marked this conversation as resolved.
Show resolved Hide resolved
}

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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;

/**
* @requires PHP 8.4
* @requires extension bcmath
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that we need either bcmath and gmp, I think this should be separated

* @requires extension gmp
*/
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
{
yield 'GMP object' => [new \GMP('0b111'), true];
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];
}

/**
* @dataProvider normalizeGoodValueProvider
*/
public function testNormalize(mixed $data, mixed $expected)
{
$this->assertSame($expected, $this->normalizer->normalize($data));
}

public static function normalizeGoodValueProvider(): iterable
{
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'];
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];
}

/**
* @dataProvider supportsDenormalizationProvider
*/
public function testSupportsDenormalization(mixed $data, string $type, bool $expected)
{
$this->assertSame($expected, $this->normalizer->supportsDenormalization($data, $type));
}

public static function supportsDenormalizationProvider(): iterable
{
yield 'null value, Number' => [null, Number::class, false];
yield 'null value, GMP' => [null, \GMP::class, false];
yield 'null value, unmatching type' => [null, \stdClass::class, false];
}

/**
* @dataProvider denormalizeGoodValueProvider
*/
public function testDenormalize(mixed $data, string $type, mixed $expected)
{
$this->assertEquals($expected, $this->normalizer->denormalize($data, $type));
}

public static function denormalizeGoodValueProvider(): iterable
{
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')];
yield 'GMP, large number' => ['9223372036854775808', \GMP::class, new \GMP('9223372036854775808')];
yield 'GMP, integer' => [123, \GMP::class, new \GMP('123')];
}

/**
* @dataProvider denormalizeBadValueProvider
*/
public function testDenormalizeBadValueThrows(mixed $data, string $type, string $expectedException, string $expectedExceptionMessage)
{
$this->expectException($expectedException);
$this->expectExceptionMessage($expectedExceptionMessage);

$this->normalizer->denormalize($data, $type);
}

public static function denormalizeBadValueProvider(): 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];

$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];

yield 'unsupported type' => ['1.23', \stdClass::class, InvalidArgumentException::class, 'Only "BcMath\Number" and "GMP" types are supported.'];
}
}
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.