8000 Add `NumberNormalizer` · symfony/symfony@449dcbd · GitHub
[go: up one dir, main page]

Skip to content

Commit 449dcbd

Browse files
valtzumtarld
andcommitted
Add NumberNormalizer
Co-authored-by: Mathias Arlaud <mathias.arlaud@gmail.com>
1 parent 3905379 commit 449dcbd

File tree

6 files changed

+213
-0
lines changed

6 files changed

+213
-0
lines changed

psalm.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
<referencedClass name="UnitEnum"/>
3333
<!-- These classes have been added in PHP 8.2 -->
3434
<referencedClass name="Random\*"/>
35+
<!-- These classes have been added in PHP 8.4 -->
36+
<referencedClass name="BcMath\Number"/>
3537
</errorLevel>
3638
</UndefinedClass>
3739
<UndefinedDocblockClass>

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1933,6 +1933,11 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
19331933
$container->removeDefinition('serializer.normalizer.mime_message');
19341934
}
19351935

1936+
// BC layer Serializer < 7.3
1937+
if (!class_exists(NumberNormalizer::class)) {
1938+
$container->removeDefinition('serializer.normalizer.number');
1939+
}
1940+
19361941
// BC layer Serializer < 7.2
19371942
if (!class_exists(SnakeCaseToCamelCaseNameConverter::class)) {
19381943
$container->removeDefinition('serializer.name_converter.snake_case_to_camel_case');

src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
4545
use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer;
4646
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
47+
use Symfony\Component\Serializer\Normalizer\NumberNormalizer;
4748
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
4849
use Symfony\Component\Serializer\Normalizer\ProblemNormalizer;
4950
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
@@ -221,5 +222,8 @@
221222

222223
->set('serializer.normalizer.backed_enum', BackedEnumNormalizer::class)
223224
->tag('serializer.normalizer', ['built_in' => true, 'priority' => -915])
225+
226+
->set('serializer.normalizer.number', NumberNormalizer::class)
227+
->tag('serializer.normalizer', ['built_in' => true, 'priority' => -915])
224228
;
225229
};

src/Symfony/Component/Serializer/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Deprecate the `CompiledClassMetadataFactory` and `CompiledClassMetadataCacheWarmer` classes
8+
* Add `NumberNormalizer` to normalize `BcMath\Number` as `string`
89

910
7.2
1011
---
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Normalizer;
13+
14+
use BcMath\Number;
15+
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
16+
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
17+
18+
/**
19+
* Normalizes a {@see Number} to a string.
20+
*/
21+
final class NumberNormalizer implements NormalizerInterface, DenormalizerInterface
22+
{
23+
public function getSupportedTypes(?string $format): array
24+
{
25+
return [
26+
Number::class => true,
27+
];
28+
}
29+
30+
public function normalize(mixed $data, ?string $format = null, array $context = []): string
31+
{
32+
if (!$data instanceof Number) {
33+
throw new InvalidArgumentException(\sprintf('The data must be an instance of "%s".', Number::class));
34+
}
35+
36+
return (string) $data;
37+
}
38+
39+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
40+
{
41+
return $data instanceof Number;
42+
}
43+
44+
/**
45+
* @throws NotNormalizableValueException
46+
*/
47+
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): Number
48+
{
49+
if (Number::class !== $type) {
50+
throw new InvalidArgumentException(\sprintf('Only "%s" type is supported.', Number::class));
51+
}
52+
53+
if (!\is_string($data) && !\is_int($data)) {
54+
throw NotNormalizableValueException::createForUnexpectedDataType('The data is neither a "string" nor an "int", you should pass a "string" that represents a decimal number, or an "int".', $data, ['string', 'int'], $context['deserialization_path'] ?? null, true);
55+
}
56+
57+
try {
58+
return new Number($data);
59+
} catch (\ValueError $e) {
60+
throw NotNormalizableValueException::createForUnexpectedDataType('The data must represent a decimal number', $data, ['string', 'int'], $context['deserialization_path'] ?? null, true, 0, $e);
61+
}
62+
}
63+
64+
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
65+
{
66+
return Number::class === $type && null !== $data;
67+
}
68+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Normalizer;
13+
14+
use BcMath\Number;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
17+
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
18+
use Symfony\Component\Serializer\Normalizer\NumberNormalizer;
19+
20+
/**
21+
* @requires PHP 8.4
22+
* @requires extension bcmath
23+
*/
24+
class NumberNormalizerTest extends TestCase
25+
{
26+
private NumberNormalizer $normalizer;
27+
28+
protected function setUp(): void
29+
{
30+
$this->normalizer = new NumberNormalizer();
31+
}
32+
33+
/**
34+
* @dataProvider supportsNormalizationProvider
35+
*/
36+
public function testSupportsNormalization(mixed $data, bool $expected)
37+
{
38+
$this->assertSame($expected, $this->normalizer->supportsNormalization($data));
39+
}
40+
41+
public static function supportsNormalizationProvider(): iterable
42+
{
43+
yield 'Number object' => [new Number('1.23'), true];
44+
yield 'object with similar properties as Number' => [(object) ['value' => '1.23', 'scale' => 2], false];
45+
yield 'stdClass' => [new \stdClass(), false];
46+
yield 'string' => ['1.23', false];
47+
yield 'float' => [1.23, false];
48+
yield 'null' => [null, false];
49+
}
50+
51+
/**
52+
* @dataProvider normalizeGoodValueProvider
53+
*/
54+
public function testNormalize(mixed $data, mixed $expected)
55+
{
56+
$this->assertSame($expected, $this->normalizer->normalize($data));
57+
}
58+
59+
public static function normalizeGoodValueProvider(): iterable
60+
{
61+
yield 'Number with scale=2' => [new Number('1.23'), '1.23'];
62+
yield 'Number with scale=0' => [new Number('1'), '1'];
63+
yield 'Number with integer' => [new Number(123), '123'];
64+
}
65+
66+
/**
67+
* @dataProvider normalizeBadValueProvider
68+
*/
69+
public function testNormalizeBadValueThrows(mixed $data)
70+
{
71+
$this->expectException(InvalidArgumentException::class);
72+
$this->expectExceptionMessage('The data must be an instance of "BcMath\Number".');
73+
74+
$this->normalizer->normalize($data);
75+
}
76+
77+
public static function normalizeBadValueProvider(): iterable
78+
{
79+
yield 'stdClass' => [new \stdClass()];
80+
yield 'string' => ['1.23'];
81+
yield 'null' => [null];
82+
}
83+
84+
/**
85+
* @dataProvider supportsDenormalizationProvider
86+
*/
87+
public function testSupportsDenormalization(mixed $data, string $type, bool $expected)
88+
{
89+
$this->assertSame($expected, $this->normalizer->supportsDenormalization($data, $type));
90+
}
91+
92+
public static function supportsDenormalizationProvider(): iterable
93+
{
94+
yield 'null value, matching type' => [null, Number::class, false];
95+
yield 'null value, unmatching type' => [null, \stdClass::class, false];
96+
}
97+
98+
/**
99+
* @dataProvider denormalizeGoodValueProvider
100+
*/
101+
public function testDenormalize(mixed $data, string $type, mixed $expected)
102+
{
103+
$this->assertEquals($expected, $this->normalizer->denormalize($data, $type));
104+
}
105+
106+
public static function denormalizeGoodValueProvider(): iterable
107+
{
108+
yield 'string with decimal point' => ['1.23', Number::class, new Number('1.23')];
109+
yield 'integer as string' => ['123', Number::class, new Number('123')];
110+
yield 'integer' => [123, Number::class, new Number('123')];
111+
}
112+
113+
/**
114+
* @dataProvider denormalizeBadValueProvider
115+
*/
116+
public function testDenormalizeBadValueThrows(mixed $data, string $type, string $expectedException, string $expectedExceptionMessage)
117+
{
118+
$this->expectException($expectedException);
119+
$this->expectExceptionMessage($expectedExceptionMessage);
120+
121+
$this->normalizer->denormalize($data, $type);
122+
}
123+
124+
public static function denormalizeBadValueProvider(): iterable
125+
{
126+
yield 'null' => [null, Number::class, NotNormalizableValueException::class, 'The data is neither a "string" nor an "int", you should pass a "string" that represents a decimal number, or an "int".'];
127+
yield 'boolean' => [true, Number::class, NotNormalizableValueException::class, 'The data is neither a "string" nor an "int", you should pass a "string" that represents a decimal number, or an "int".'];
128+
yield 'object' => [new \stdClass(), Number::class, NotNormalizableValueException::class, 'The data is neither a "string" nor an "int", you should pass a "string" that represents a decimal number, or an "int".'];
129+
yield 'non-numeric string' => ['foobar', Number::class, NotNormalizableValueException::class, 'The data must represent a decimal number'];
130+
yield 'unsupported type' => ['1.23', \stdClass::class, InvalidArgumentException::class, 'Only "BcMath\Number" type is supported.'];
131+
yield 'float' => [1.23, Number::class, NotNormalizableValueException::class, 'The data is neither a "string" nor an "int", you should pass a "string" that represents a decimal number, or an "int".'];
132+
}
133+
}

0 commit comments

Comments
 (0)
0