-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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
|
||
{ | ||
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
|
||
} | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.']; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.