From 7305a1cb74cae21798f473ac030ca5b1e313da8d Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 29 Sep 2026 17:06:36 +0200 Subject: [PATCH 1/3] fix(symfony): keep denormalize on decorated normalizers Since #7713 each item normalizer has a dedicated denormalizer registered one priority higher, so a user decorator of the normalizer that also implements DenormalizerInterface was never reached for denormalization. When such a decorator exists, detach the matching denormalizer from the serializer chain and trigger a deprecation pointing to the denormalizer service. Fixes #8550 --- src/Symfony/Bundle/ApiPlatformBundle.php | 3 + .../ItemNormalizerDecorationBcPass.php | 90 ++++++++++++++++++ ...rmalizingJsonLdItemNormalizerDecorator.php | 66 +++++++++++++ .../NormalizeOnlyItemNormalizerDecorator.php | 38 ++++++++ ...ratedItemNormalizerDenormalizationTest.php | 80 ++++++++++++++++ .../ItemNormalizerDecorationBcPassTest.php | 92 +++++++++++++++++++ 6 files changed, 369 insertions(+) create mode 100644 src/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPass.php create mode 100644 tests/Fixtures/TestBundle/Serializer/Decorator/DenormalizingJsonLdItemNormalizerDecorator.php create mode 100644 tests/Fixtures/TestBundle/Serializer/Decorator/NormalizeOnlyItemNormalizerDecorator.php create mode 100644 tests/Functional/DecoratedItemNormalizerDenormalizationTest.php create mode 100644 tests/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPassTest.php diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index f887630f0d5..c2ba301c25b 100644 --- a/src/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Symfony/Bundle/ApiPlatformBundle.php @@ -22,6 +22,7 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\FilterPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\ItemNormalizerDecorationBcPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\JsonStreamerTransformerPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\McpRegistryPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass; @@ -66,6 +67,8 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new MutatorPass()); $container->addCompilerPass(new McpRegistryPass()); $container->addCompilerPass(new PropertyInfoTagPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -100); + // Must run after AutowireAsDecoratorPass (priority 100) so #[AsDecorator] is already a decorated service. + $container->addCompilerPass(new ItemNormalizerDecorationBcPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10); // Must run after Symfony's TransformerPass so we can rely on the value_object_transformer tag being processed. $container->addCompilerPass(new JsonStreamerTransformerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -10); } diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPass.php new file mode 100644 index 00000000000..7c92e2c133e --- /dev/null +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPass.php @@ -0,0 +1,90 @@ + + * + * 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\Symfony\Bundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + +/** + * Item normalizers were split into a normalizer and a denormalizer registered one priority higher. + * A user decorator of the normalizer that also implements {@see DenormalizerInterface} would never + * be reached for denormalization, so the denormalizer is detached from the serializer chain and + * denormalization goes through the decorated normalizer again. + * + * @internal + * + * @todo remove in 6.0 + */ +final class ItemNormalizerDecorationBcPass implements CompilerPassInterface +{ + private const PAIRS = [ + 'api_platform.serializer.normalizer.item' => 'api_platform.serializer.denormalizer.item', + 'api_platform.jsonld.normalizer.item' => 'api_platform.jsonld.denormalizer.item', + 'api_platform.jsonapi.normalizer.item' => 'api_platform.jsonapi.denormalizer.item', + 'api_platform.graphql.normalizer.item' => 'api_platform.graphql.denormalizer.item', + ]; + + public function process(ContainerBuilder $container): void + { + foreach (self::PAIRS as $normalizerId => $denormalizerId) { + if (!$container->hasDefinition($normalizerId) || !$container->hasDefinition($denormalizerId)) { + continue; + } + + $decorators = $this->findUserDecorators($container, $normalizerId); + if ([] === $decorators || [] !== $this->findUserDecorators($container, $denormalizerId, false)) { + continue; + } + + $container->getDefinition($denormalizerId)->clearTag('serializer.normalizer'); + + foreach ($decorators as $id) { + trigger_deprecation('api-platform/core', '4.4', 'Service "%s" decorates "%s" and implements "%s": denormalization is routed through the decorated normalizer for backward compatibility. Decorate "%s" instead.', $id, $normalizerId, DenormalizerInterface::class, $denormalizerId); + } + } + } + + /** + * @return list + */ + private function findUserDecorators(ContainerBuilder $container, string $decoratedId, bool $onlyDenormalizers = true): array + { + $ids = []; + foreach ($container->getDefinitions() as $id => $definition) { + if (str_starts_with((string) $id, 'api_platform.') || ($definition->getDecoratedService()[0] ?? null) !== $decoratedId) { + continue; + } + + if ($onlyDenormalizers && !$this->implementsDenormalizer($container, $definition->getClass())) { + continue; + } + + $ids[] = $id; + } + + return $ids; + } + + private function implementsDenormalizer(ContainerBuilder $container, ?string $class): bool + { + if (null === $class) { + return false; + } + + $class = $container->getParameterBag()->resolveValue($class); + + return \is_string($class) && true === $container->getReflectionClass($class, false)?->implementsInterface(DenormalizerInterface::class); + } +} diff --git a/tests/Fixtures/TestBundle/Serializer/Decorator/DenormalizingJsonLdItemNormalizerDecorator.php b/tests/Fixtures/TestBundle/Serializer/Decorator/DenormalizingJsonLdItemNormalizerDecorator.php new file mode 100644 index 00000000000..e9b1d249db2 --- /dev/null +++ b/tests/Fixtures/TestBundle/Serializer/Decorator/DenormalizingJsonLdItemNormalizerDecorator.php @@ -0,0 +1,66 @@ + + * + * 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\Serializer\Decorator; + +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ValidateOnce; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\SerializerAwareInterface; +use Symfony\Component\Serializer\SerializerInterface; + +#[AsDecorator('api_platform.jsonld.normalizer.item')] +final class DenormalizingJsonLdItemNormalizerDecorator implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface +{ + public function __construct(#[AutowireDecorated] private readonly NormalizerInterface&DenormalizerInterface&SerializerAwareInterface $inner) + { + } + + public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return $this->inner->normalize($data, $format, $context); + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $this->inner->supportsNormalization($data, $format, $context); + } + + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $object = $this->inner->denormalize($data, $type, $format, $context); + + if ($object instanceof ValidateOnce) { + $object->name = 'decorated'; + } + + return $object; + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + return $this->inner->supportsDenormalization($data, $type, $format, $context); + } + + public function setSerializer(SerializerInterface $serializer): void + { + $this->inner->setSerializer($serializer); + } + + public function getSupportedTypes(?string $format): array + { + return $this->inner->getSupportedTypes($format); + } +} diff --git a/tests/Fixtures/TestBundle/Serializer/Decorator/NormalizeOnlyItemNormalizerDecorator.php b/tests/Fixtures/TestBundle/Serializer/Decorator/NormalizeOnlyItemNormalizerDecorator.php new file mode 100644 index 00000000000..ea52b59e9d9 --- /dev/null +++ b/tests/Fixtures/TestBundle/Serializer/Decorator/NormalizeOnlyItemNormalizerDecorator.php @@ -0,0 +1,38 @@ + + * + * 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\Serializer\Decorator; + +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +final class NormalizeOnlyItemNormalizerDecorator implements NormalizerInterface +{ + public function __construct(private readonly NormalizerInterface $inner) + { + } + + public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return $this->inner->normalize($data, $format, $context); + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $this->inner->supportsNormalization($data, $format, $context); + } + + public function getSupportedTypes(?string $format): array + { + return $this->inner->getSupportedTypes($format); + } +} diff --git a/tests/Functional/DecoratedItemNormalizerDenormalizationTest.php b/tests/Functional/DecoratedItemNormalizerDenormalizationTest.php new file mode 100644 index 00000000000..755f3b20c0f --- /dev/null +++ b/tests/Functional/DecoratedItemNormalizerDenormalizationTest.php @@ -0,0 +1,80 @@ + + * + * 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\ValidateOnce; +use ApiPlatform\Tests\Fixtures\TestBundle\Serializer\Decorator\DenormalizingJsonLdItemNormalizerDecorator; +use ApiPlatform\Tests\SetupClassResourcesTrait; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +class DecoratedItemNormalizerAppKernel extends \AppKernel +{ + public function getCacheDir(): string + { + return parent::getCacheDir().'/decorated_item_normalizer'; + } + + public function getLogDir(): string + { + return parent::getLogDir().'/decorated_item_normalizer'; + } + + protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void + { + parent::configureContainer($c, $loader); + + $loader->load(static function (ContainerBuilder $container): void { + $container->register(DenormalizingJsonLdItemNormalizerDecorator::class) + ->setAutowired(true) + ->setAutoconfigured(true); + }); + } +} + +final class DecoratedItemNormalizerDenormalizationTest extends ApiTestCase +{ + use SetupClassResourcesTrait; + + protected static ?bool $alwaysBootKernel = false; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [ValidateOnce::class]; + } + + protected static function getKernelClass(): string + { + return DecoratedItemNormalizerAppKernel::class; + } + + #[IgnoreDeprecations] + public function testDecoratorOfTheItemNormalizerStillDenormalizes(): void + { + $this->expectUserDeprecationMessage('Since api-platform/core 4.4: Calling "denormalize()" on "ApiPlatform\JsonLd\Serializer\ItemNormalizer" is deprecated, use "ApiPlatform\JsonLd\Serializer\ItemDenormalizer" instead.'); + + $response = self::createClient()->request('POST', '/validate_once', [ + 'headers' => ['Content-Type' => 'application/ld+json', 'Accept' => 'application/ld+json'], + 'json' => ['name' => 'original'], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertSame('decorated', $response->toArray()['name']); + } +} diff --git a/tests/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPassTest.php b/tests/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPassTest.php new file mode 100644 index 00000000000..efa5bd9139c --- /dev/null +++ b/tests/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPassTest.php @@ -0,0 +1,92 @@ + + * + * 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\Symfony\Bundle\DependencyInjection\Compiler; + +use ApiPlatform\Elasticsearch\Serializer\ItemNormalizer as ElasticsearchItemNormalizer; +use ApiPlatform\JsonLd\Serializer\ItemDenormalizer as JsonLdItemDenormalizer; +use ApiPlatform\JsonLd\Serializer\ItemNormalizer as JsonLdItemNormalizer; +use ApiPlatform\Serializer\ItemDenormalizer; +use ApiPlatform\Serializer\ItemNormalizer; +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\ItemNormalizerDecorationBcPass; +use ApiPlatform\Tests\Fixtures\TestBundle\Serializer\Decorator\DenormalizingJsonLdItemNormalizerDecorator; +use ApiPlatform\Tests\Fixtures\TestBundle\Serializer\Decorator\NormalizeOnlyItemNormalizerDecorator; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; + +final class ItemNormalizerDecorationBcPassTest extends TestCase +{ + #[IgnoreDeprecations] + public function testDenormalizingDecoratorDetachesDenormalizerFromSerializerChain(): void + { + $this->expectUserDeprecationMessage('Since api-platform/core 4.4: Service "user.item_normalizer" decorates "api_platform.jsonld.normalizer.item" and implements "Symfony\Component\Serializer\Normalizer\DenormalizerInterface": denormalization is routed through the decorated normalizer for backward compatibility. Decorate "api_platform.jsonld.denormalizer.item" instead.'); + + $container = $this->createContainer(); + $container->setDefinition('user.item_normalizer', (new Definition(DenormalizingJsonLdItemNormalizerDecorator::class)) + ->setDecoratedService('api_platform.jsonld.normalizer.item')); + + (new ItemNormalizerDecorationBcPass())->process($container); + + $this->assertFalse($container->getDefinition('api_platform.jsonld.denormalizer.item')->hasTag('serializer.normalizer')); + $this->assertTrue($container->getDefinition('api_platform.jsonld.normalizer.item')->hasTag('serializer.normalizer')); + $this->assertTrue($container->getDefinition('api_platform.serializer.denormalizer.item')->hasTag('serializer.normalizer')); + } + + public function testNormalizeOnlyDecoratorKeepsDenormalizerTag(): void + { + $container = $this->createContainer(); + $container->setDefinition('user.item_normalizer', (new Definition(NormalizeOnlyItemNormalizerDecorator::class)) + ->setDecoratedService('api_platform.jsonld.normalizer.item')); + + (new ItemNormalizerDecorationBcPass())->process($container); + + $this->assertTrue($container->getDefinition('api_platform.jsonld.denormalizer.item')->hasTag('serializer.normalizer')); + } + + public function testDecoratorOfTheDenormalizerKeepsDenormalizerTag(): void + { + $container = $this->createContainer(); + $container->setDefinition('user.item_normalizer', (new Definition(DenormalizingJsonLdItemNormalizerDecorator::class)) + ->setDecoratedService('api_platform.jsonld.normalizer.item')); + $container->setDefinition('user.item_denormalizer', (new Definition(DenormalizingJsonLdItemNormalizerDecorator::class)) + ->setDecoratedService('api_platform.jsonld.denormalizer.item')); + + (new ItemNormalizerDecorationBcPass())->process($container); + + $this->assertTrue($container->getDefinition('api_platform.jsonld.denormalizer.item')->hasTag('serializer.normalizer')); + } + + public function testApiPlatformDecoratorKeepsDenormalizerTag(): void + { + $container = $this->createContainer(); + $container->setDefinition('api_platform.elasticsearch.normalizer.item', (new Definition(ElasticsearchItemNormalizer::class)) + ->setDecoratedService('api_platform.serializer.normalizer.item')); + + (new ItemNormalizerDecorationBcPass())->process($container); + + $this->assertTrue($container->getDefinition('api_platform.serializer.denormalizer.item')->hasTag('serializer.normalizer')); + } + + private function createContainer(): ContainerBuilder + { + $container = new ContainerBuilder(); + $container->setDefinition('api_platform.serializer.normalizer.item', (new Definition(ItemNormalizer::class))->addTag('serializer.normalizer', ['priority' => -895])); + $container->setDefinition('api_platform.serializer.denormalizer.item', (new Definition(ItemDenormalizer::class))->addTag('serializer.normalizer', ['priority' => -894])); + $container->setDefinition('api_platform.jsonld.normalizer.item', (new Definition(JsonLdItemNormalizer::class))->addTag('serializer.normalizer', ['priority' => -890])); + $container->setDefinition('api_platform.jsonld.denormalizer.item', (new Definition(JsonLdItemDenormalizer::class))->addTag('serializer.normalizer', ['priority' => -889])); + + return $container; + } +} From b562435d6659b72d2d5c30d78679bc032b2caec8 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 29 Sep 2026 17:13:54 +0200 Subject: [PATCH 2/3] chore(symfony): pin bc pass ordering and es case --- src/Symfony/Bundle/ApiPlatformBundle.php | 2 +- .../Compiler/ItemNormalizerDecorationBcPass.php | 5 +---- .../ItemNormalizerDecorationBcPassTest.php | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index c2ba301c25b..2908a7ada44 100644 --- a/src/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Symfony/Bundle/ApiPlatformBundle.php @@ -67,7 +67,7 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new MutatorPass()); $container->addCompilerPass(new McpRegistryPass()); $container->addCompilerPass(new PropertyInfoTagPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -100); - // Must run after AutowireAsDecoratorPass (priority 100) so #[AsDecorator] is already a decorated service. + // Runs after AutowireAsDecoratorPass (100) and before FrameworkBundle's SerializerPass (0), which reads the serializer.normalizer tag. $container->addCompilerPass(new ItemNormalizerDecorationBcPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10); // Must run after Symfony's TransformerPass so we can rely on the value_object_transformer tag being processed. $container->addCompilerPass(new JsonStreamerTransformerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -10); diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPass.php index 7c92e2c133e..25235c5e6e8 100644 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPass.php +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPass.php @@ -18,10 +18,7 @@ use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; /** - * Item normalizers were split into a normalizer and a denormalizer registered one priority higher. - * A user decorator of the normalizer that also implements {@see DenormalizerInterface} would never - * be reached for denormalization, so the denormalizer is detached from the serializer chain and - * denormalization goes through the decorated normalizer again. + * Detaches an item denormalizer from the serializer chain when a user decorator of the matching normalizer implements {@see DenormalizerInterface}. * * @internal * diff --git a/tests/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPassTest.php b/tests/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPassTest.php index efa5bd9139c..06c767f3ef4 100644 --- a/tests/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPassTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPassTest.php @@ -79,6 +79,22 @@ public function testApiPlatformDecoratorKeepsDenormalizerTag(): void $this->assertTrue($container->getDefinition('api_platform.serializer.denormalizer.item')->hasTag('serializer.normalizer')); } + #[IgnoreDeprecations] + public function testApiPlatformDecoratorOfTheDenormalizerDoesNotCountAsMigrated(): void + { + $this->expectUserDeprecationMessage('Since api-platform/core 4.4: Service "user.item_normalizer" decorates "api_platform.serializer.normalizer.item" and implements "Symfony\\Component\\Serializer\\Normalizer\\DenormalizerInterface": denormalization is routed through the decorated normalizer for backward compatibility. Decorate "api_platform.serializer.denormalizer.item" instead.'); + + $container = $this->createContainer(); + $container->setDefinition('user.item_normalizer', (new Definition(DenormalizingJsonLdItemNormalizerDecorator::class)) + ->setDecoratedService('api_platform.serializer.normalizer.item')); + $container->setDefinition('api_platform.elasticsearch.denormalizer.item', (new Definition(ElasticsearchItemNormalizer::class)) + ->setDecoratedService('api_platform.serializer.denormalizer.item')); + + (new ItemNormalizerDecorationBcPass())->process($container); + + $this->assertFalse($container->getDefinition('api_platform.serializer.denormalizer.item')->hasTag('serializer.normalizer')); + } + private function createContainer(): ContainerBuilder { $container = new ContainerBuilder(); From c024d84e659059d1713a45ab0b446bb7418f2c93 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 29 Sep 2026 17:25:05 +0200 Subject: [PATCH 3/3] test(symfony): capture bc pass deprecations locally In the full suite, earlier kernel boots leave Symfony ErrorHandler registered, which swallows the deprecation before expectUserDeprecationMessage sees it. Capture it with a local error handler, as AttributeFilterPassTest does. --- .../ItemNormalizerDecorationBcPassTest.php | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/tests/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPassTest.php b/tests/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPassTest.php index 06c767f3ef4..7d16230b07e 100644 --- a/tests/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPassTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/Compiler/ItemNormalizerDecorationBcPassTest.php @@ -21,23 +21,34 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\ItemNormalizerDecorationBcPass; use ApiPlatform\Tests\Fixtures\TestBundle\Serializer\Decorator\DenormalizingJsonLdItemNormalizerDecorator; use ApiPlatform\Tests\Fixtures\TestBundle\Serializer\Decorator\NormalizeOnlyItemNormalizerDecorator; -use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; final class ItemNormalizerDecorationBcPassTest extends TestCase { - #[IgnoreDeprecations] public function testDenormalizingDecoratorDetachesDenormalizerFromSerializerChain(): void { - $this->expectUserDeprecationMessage('Since api-platform/core 4.4: Service "user.item_normalizer" decorates "api_platform.jsonld.normalizer.item" and implements "Symfony\Component\Serializer\Normalizer\DenormalizerInterface": denormalization is routed through the decorated normalizer for backward compatibility. Decorate "api_platform.jsonld.denormalizer.item" instead.'); + $expected = 'Since api-platform/core 4.4: Service "user.item_normalizer" decorates "api_platform.jsonld.normalizer.item" and implements "Symfony\Component\Serializer\Normalizer\DenormalizerInterface": denormalization is routed through the decorated normalizer for backward compatibility. Decorate "api_platform.jsonld.denormalizer.item" instead.'; $container = $this->createContainer(); $container->setDefinition('user.item_normalizer', (new Definition(DenormalizingJsonLdItemNormalizerDecorator::class)) ->setDecoratedService('api_platform.jsonld.normalizer.item')); - (new ItemNormalizerDecorationBcPass())->process($container); + $deprecations = []; + set_error_handler(static function (int $type, string $message) use (&$deprecations): bool { + $deprecations[] = $message; + + return true; + }, \E_USER_DEPRECATED); + + try { + (new ItemNormalizerDecorationBcPass())->process($container); + } finally { + restore_error_handler(); + } + + $this->assertSame([$expected], $deprecations); $this->assertFalse($container->getDefinition('api_platform.jsonld.denormalizer.item')->hasTag('serializer.normalizer')); $this->assertTrue($container->getDefinition('api_platform.jsonld.normalizer.item')->hasTag('serializer.normalizer')); @@ -79,10 +90,9 @@ public function testApiPlatformDecoratorKeepsDenormalizerTag(): void $this->assertTrue($container->getDefinition('api_platform.serializer.denormalizer.item')->hasTag('serializer.normalizer')); } - #[IgnoreDeprecations] public function testApiPlatformDecoratorOfTheDenormalizerDoesNotCountAsMigrated(): void { - $this->expectUserDeprecationMessage('Since api-platform/core 4.4: Service "user.item_normalizer" decorates "api_platform.serializer.normalizer.item" and implements "Symfony\\Component\\Serializer\\Normalizer\\DenormalizerInterface": denormalization is routed through the decorated normalizer for backward compatibility. Decorate "api_platform.serializer.denormalizer.item" instead.'); + $expected = 'Since api-platform/core 4.4: Service "user.item_normalizer" decorates "api_platform.serializer.normalizer.item" and implements "Symfony\\Component\\Serializer\\Normalizer\\DenormalizerInterface": denormalization is routed through the decorated normalizer for backward compatibility. Decorate "api_platform.serializer.denormalizer.item" instead.'; $container = $this->createContainer(); $container->setDefinition('user.item_normalizer', (new Definition(DenormalizingJsonLdItemNormalizerDecorator::class)) @@ -90,7 +100,20 @@ public function testApiPlatformDecoratorOfTheDenormalizerDoesNotCountAsMigrated( $container->setDefinition('api_platform.elasticsearch.denormalizer.item', (new Definition(ElasticsearchItemNormalizer::class)) ->setDecoratedService('api_platform.serializer.denormalizer.item')); - (new ItemNormalizerDecorationBcPass())->process($container); + $deprecations = []; + set_error_handler(static function (int $type, string $message) use (&$deprecations): bool { + $deprecations[] = $message; + + return true; + }, \E_USER_DEPRECATED); + + try { + (new ItemNormalizerDecorationBcPass())->process($container); + } finally { + restore_error_handler(); + } + + $this->assertSame([$expected], $deprecations); $this->assertFalse($container->getDefinition('api_platform.serializer.denormalizer.item')->hasTag('serializer.normalizer')); }