Skip to content

Commit 43e9064

Browse files
committed
Add NumberNormalizer
1 parent d4566b2 commit 43e9064

File tree

5 files changed

+216
-2
lines changed

5 files changed

+216
-2
lines changed

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/Bundle/FrameworkBundle/composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
"symfony/scheduler": "^6.4.4|^7.0.4",
6060
"symfony/security-bundle": "^6.4|^7.0",
6161
"symfony/semaphore": "^6.4|^7.0",
62-
"symfony/serializer": "^7.1",
62+
"symfony/serializer": "^7.3",
6363
"symfony/stopwatch": "^6.4|^7.0",
6464
"symfony/string": "^6.4|^7.0",
6565
"symfony/translation": "^6.4.3|^7.0",
@@ -99,7 +99,7 @@
9999
"symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4",
100100
"symfony/security-csrf": "<7.2",
101101
"symfony/security-core": "<6.4",
102-
"symfony/serializer": "<7.1",
102+
"symfony/serializer": "<7.3",
103103
"symfony/stopwatch": "<6.4",
104104
"symfony/translation": "<6.4.3",
105105
"symfony/twig-bridge": "<6.4",

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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
/**
24+
* If true, will denormalize any invalid value into null.
25+
*/
26+
public const ALLOW_INVALID_VALUES = 'allow_invalid_values';
27+
28+
public function getSupportedTypes(?string $format): array
29+
{
30+
return [
31+
Number::class => true,
32+
];
33+
}
34+
35+
public function normalize(mixed $data, ?string $format = null, array $context = []): int|string
36+
{
37+
if (!$data instanceof Number) {
38+
throw new InvalidArgumentException('The data must be an instance of '.Number::class.'.');
39+
}
40+
41+
return (string) $data;
42+
}
43+
44+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
45+
{
46+
return $data instanceof Number;
47+
}
48+
49+
/**
50+
* @throws NotNormalizableValueException
51+
*/
52+
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): ?Number
53+
{
54+
if (Number::class !== $type) {
55+
throw new InvalidArgumentException('Only '.Number::class.' supported.');
56+
}
57+
58+
if (null === $data && ($context[self::ALLOW_INVALID_VALUES] ?? false)) {
59+
return null;
60+
}
61+
62+
if (!\is_int($data) && !\is_string($data)) {
63+
throw NotNormalizableValueException::createForUnexpectedDataType('The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as a '.Number::class.'.', $data, ['int', 'string'], $context['deserialization_path'] ?? null, true);
64+
}
65+
66+
try {
67+
return new Number($data);
68+
} catch (\ValueError $e) {
69+
if ($context[self::ALLOW_INVALID_VALUES] ?? false) {
70+
return null;
71+
}
72+
73+
throw NotNormalizableValueException::createForUnexpectedDataType('The data must represent a number', $data, [$type], $context['deserialization_path'] ?? null, true, 0, $e);
74+
}
75+
}
76+
77+
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
78+
{
79+
return Number::class === $type;
80+
}
81+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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+
public function testSupportsNormalization()
34+
{
35+
$this->assertTrue($this->normalizer->supportsNormalization(new Number('1.23')));
36+
$this->assertFalse($this->normalizer->supportsNormalization((object) ['value' => '1.23', 'scale' => 2]));
37+
$this->assertFalse($this->normalizer->supportsNormalization('1.23'));
38+
$this->assertFalse($this->normalizer->supportsNormalization(1.23));
39+
$this->assertFalse($this->normalizer->supportsNormalization(null));
40+
}
41+
42+
public function testNormalize()
43+
{
44+
$this->assertSame('1.23', $this->normalizer->normalize(new Number('1.23')));
45+
$this->assertSame('1', $this->normalizer->normalize(new Number('1')));
46+
$this->assertSame('123', $this->normalizer->normalize(new Number(123)));
47+
}
48+
49+
public function testNormalizeBadObjectTypeThrowsException()
50+
{
51+
$this->expectException(InvalidArgumentException::class);
52+
$this->normalizer->normalize(new \stdClass());
53+
}
54+
55+
public function testSupportsDenormalization()
56+
{
57+
$this->assertTrue($this->normalizer->supportsDenormalization(null, Number::class));
58+
$this->assertFalse($this->normalizer->supportsDenormalization(null, \stdClass::class));
59+
}
60+
61+
public function testDenormalize()
62+
{
63+
$this->assertEquals(new Number('1.23'), $number = $this->normalizer->denormalize('1.23', Number::class));
64+
$this->assertEquals(new Number('123'), $this->normalizer->denormalize('123', Number::class));
65+
$this->assertEquals(new Number('123'), $this->normalizer->denormalize(123, Number::class));
66+
}
67+
68+
public function testDenormalizeNullValueThrowsException()
69+
{
70+
$this->expectException(NotNormalizableValueException::class);
71+
$this->normalizer->denormalize(null, Number::class);
72+
}
73+
74+
public function testDenormalizeBooleanValueThrowsException()
75+
{
76+
$this->expectException(NotNormalizableValueException::class);
77+
$this->normalizer->denormalize(true, Number::class);
78+
}
79+
80+
public function testDenormalizeObjectThrowsException()
81+
{
82+
$this->expectException(NotNormalizableValueException::class);
83+
$this->normalizer->denormalize(new \stdClass(), Number::class);
84+
}
85+
86+
public function testDenormalizeBadNumberValueThrowsException()
87+
{
88+
$this->expectException(NotNormalizableValueException::class);
89+
$this->expectExceptionMessage('The data must represent a number');
90+
91+
$this->normalizer->denormalize('foobar', Number::class);
92+
}
93+
94+
public function testNormalizeShouldThrowExceptionForNonNumberObjects()
95+
{
96+
$this->expectException(\InvalidArgumentException::class);
97+
$this->expectExceptionMessage('The data must be an instance of BcMath\Number.');
98+
99+
$this->normalizer->normalize(\stdClass::class);
100+
}
101+
102+
public function testDenormalizeShouldThrowExceptionForNonNumberObjects()
103+
{
104+
$this->expectException(\InvalidArgumentException::class);
105+
$this->expectExceptionMessage('Only BcMath\Number supported.');
106+
107+
$this->normalizer->denormalize('1.23', \stdClass::class);
108+
}
109+
110+
public function testDenormalizeShouldThrowExceptionForFloats()
111+
{
112+
$this->expectException(NotNormalizableValueException::class);
113+
$this->expectExceptionMessage('The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as a BcMath\Number.');
114+
115+
$this->normalizer->denormalize(1.23, Number::class);
116+
}
117+
118+
public function testSupportsNormalizationShouldFailForNonNumberObjects()
119+
{
120+
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
121+
}
122+
123+
public function testItIgnoresInvalidValuesIfContextIsPassed()
124+
{
125+
$this->assertNull($this->normalizer->denormalize('foo', Number::class, null, [NumberNormalizer::ALLOW_INVALID_VALUES => true]));
126+
$this->assertNull($this->normalizer->denormalize(null, Number::class, null, [NumberNormalizer::ALLOW_INVALID_VALUES => true]));
127+
}
128+
}

0 commit comments

Comments
 (0)