From b0c49399a0063def6849225e53266a92a961e14b Mon Sep 17 00:00:00 2001 From: aaa2000 Date: Sat, 8 Nov 2025 18:38:16 +0100 Subject: [PATCH] feat(serializer): render BCMath\Number (PHP 8.4+) as string instead of object --- .../Factory/SchemaPropertyMetadataFactory.php | 11 +++ .../ApiPlatformExtension.php | 10 +++ .../TestBundle/ApiResource/MathNumber.php | 43 ++++++++++ tests/Functional/MathNumberTest.php | 81 +++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 tests/Fixtures/TestBundle/ApiResource/MathNumber.php create mode 100644 tests/Functional/MathNumberTest.php diff --git a/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php b/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php index 90f8ddea3fc..b5e4df3c688 100644 --- a/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php +++ b/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php @@ -319,6 +319,10 @@ private function getClassSchemaDefinition(?string $className, ?bool $readableLin return ['type' => 'string', 'format' => 'binary']; } + if (is_a($className, \BcMath\Number::class, true)) { + return ['type' => 'string', 'format' => 'string']; + } + $isResourceClass = $this->isResourceClass($className); if (!$isResourceClass && is_a($className, \BackedEnum::class, true)) { $enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases()); @@ -509,6 +513,13 @@ private function getLegacyClassType(?string $className, bool $nullable, ?bool $r ]; } + if (is_a($className, \BcMath\Number::class, true)) { + return [ + 'type' => 'string', + 'format' => 'string', + ]; + } + $isResourceClass = $this->isResourceClass($className); if (!$isResourceClass && is_a($className, \BackedEnum::class, true)) { $enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases()); diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 6def72a0ec2..73027f904f9 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -66,6 +66,7 @@ use Symfony\Component\JsonStreamer\JsonStreamWriter; use Symfony\Component\ObjectMapper\ObjectMapper; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\Normalizer\NumberNormalizer; use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Yaml\Yaml; @@ -267,6 +268,15 @@ private function registerCommonConfiguration(ContainerBuilder $container, array $loader->load('symfony/uid.php'); } + // symfony/serializer:7.3 added the NumberNormalizer + // symfony/framework-bundle:7.3 added the serializer.normalizer.number` service + // if symfony/serializer >= 7.3 and symfony/framework-bundle < 7.3, the service is registred + if (class_exists(NumberNormalizer::class) && !$container->has('serializer.normalizer.number')) { + $numberNormalizerDefinition = new Definition(NumberNormalizer::class); + $numberNormalizerDefinition->addTag('serializer.normalizer', ['built_in' => true, 'priority' => -915]); + $container->setDefinition('serializer.normalizer.number', $numberNormalizerDefinition); + } + $defaultContext = ['hydra_prefix' => $config['serializer']['hydra_prefix']] + ($container->hasParameter('serializer.default_context') ? $container->getParameter('serializer.default_context') : []); $container->setParameter('api_platform.serializer.default_context', $defaultContext); diff --git a/tests/Fixtures/TestBundle/ApiResource/MathNumber.php b/tests/Fixtures/TestBundle/ApiResource/MathNumber.php new file mode 100644 index 00000000000..8ba0971b97e --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/MathNumber.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Post; + +if (class_exists(\BcMath\Number::class)) { + #[Get( + provider: [self::class, 'provide'], + )] + #[Post] + class MathNumber + { + #[ApiProperty(identifier: true)] + public int $id; + + #[ApiProperty(property: 'value')] + public ?\BcMath\Number $value; + + public static function provide(Operation $operation, array $uriVariables = [], array $context = []): self + { + $mathNumber = new self(); + $mathNumber->id = $uriVariables['id']; + $mathNumber->value = new \BcMath\Number('300.55'); + + return $mathNumber; + } + } +} diff --git a/tests/Functional/MathNumberTest.php b/tests/Functional/MathNumberTest.php new file mode 100644 index 00000000000..04dab45903a --- /dev/null +++ b/tests/Functional/MathNumberTest.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Functional; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\MathNumber; +use ApiPlatform\Tests\SetupClassResourcesTrait; +use PHPUnit\Framework\Attributes\RequiresPhp; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; +use Symfony\Component\Serializer\Normalizer\NumberNormalizer; + +#[RequiresPhp('^8.4')] +#[RequiresPhpExtension('bcmath')] +final class MathNumberTest extends ApiTestCase +{ + use SetupClassResourcesTrait; + + protected static ?bool $alwaysBootKernel = false; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [MathNumber::class]; + } + + protected function setUp(): void + { + if (!class_exists(NumberNormalizer::class)) { + $this->markTestSkipped('Requires BcMath/Number and symfony/serialiser >=7.3'); + } + + parent::setUp(); + } + + public function testGetMathNumber(): void + { + self::createClient()->request('GET', '/math_numbers/1', ['headers' => ['Accept' => 'application/ld+json']]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonEquals([ + '@context' => '/contexts/MathNumber', + '@id' => '/math_numbers/1', + '@type' => 'MathNumber', + 'id' => 1, + 'value' => '300.55', + ]); + } + + public function testPostMathNumber(): void + { + self::createClient()->request('POST', '/math_numbers', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'id' => 2, + 'value' => '120.23', + ], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonEquals([ + '@context' => '/contexts/MathNumber', + '@id' => '/math_numbers/2', + '@type' => 'MathNumber', + 'id' => 2, + 'value' => '120.23', + ]); + } +}