Skip to content

Commit 7d3be64

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

File tree

6 files changed

+214
-0
lines changed

6 files changed

+214
-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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter;
171171
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
172172
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
173+
use Symfony\Component\Serializer\Normalizer\NumberNormalizer;
173174
use Symfony\Component\Serializer\Serializer;
174175
use Symfony\Component\Stopwatch\Stopwatch;
175176
use Symfony\Component\String\LazyString;
@@ -1933,6 +1934,11 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
19331934
$container->removeDefinition('serializer.normalizer.mime_message');
19341935
}
19351936

1937+
// BC layer Serializer < 7.3
1938+
if (!class_exists(NumberNormalizer::class)) {
1939+
$container->removeDefinition('serializer.normalizer.number');
1940+
}
1941+
19361942
// BC layer Serializer < 7.2
19371943
if (!class_exists(SnakeCaseToCamelCaseNameConverter::class)) {
19381944
$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
@@ -6,6 +6,7 @@ CHANGELOG
66

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

1011
7.2
1112
---
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy