From 5feddfa2b8a10b7db6e41731f7130a99e471d2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Cimb=C3=A1l?= Date: Sun, 8 Mar 2026 16:38:33 +0100 Subject: [PATCH 1/3] fix(symfony): clear SkolemIriConverter state between requests via ResetInterface --- src/Symfony/Bundle/Resources/config/api.php | 3 +- src/Symfony/Routing/SkolemIriConverter.php | 9 +- .../Routing/SkolemIriConverterTest.php | 105 ++++++++++++++++++ 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 tests/Symfony/Routing/SkolemIriConverterTest.php diff --git a/src/Symfony/Bundle/Resources/config/api.php b/src/Symfony/Bundle/Resources/config/api.php index c1685a0c4ae..0daa6e7efd5 100644 --- a/src/Symfony/Bundle/Resources/config/api.php +++ b/src/Symfony/Bundle/Resources/config/api.php @@ -194,7 +194,8 @@ ->tag('routing.loader'); $services->set('api_platform.symfony.iri_converter.skolem', SkolemIriConverter::class) - ->args([service('api_platform.router')]); + ->args([service('api_platform.router')]) + ->tag('kernel.reset', ['method' => 'reset']); $services->set('api_platform.api.identifiers_extractor', IdentifiersExtractor::class) ->args([ diff --git a/src/Symfony/Routing/SkolemIriConverter.php b/src/Symfony/Routing/SkolemIriConverter.php index e62d074cad2..4912a424292 100644 --- a/src/Symfony/Routing/SkolemIriConverter.php +++ b/src/Symfony/Routing/SkolemIriConverter.php @@ -18,13 +18,14 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\UrlGeneratorInterface; use Symfony\Component\Routing\RouterInterface; +use Symfony\Contracts\Service\ResetInterface; /** * {@inheritdoc} * * @author Antoine Bluchet */ -final class SkolemIriConverter implements IriConverterInterface +final class SkolemIriConverter implements IriConverterInterface, ResetInterface { public static string $skolemUriTemplate = '/.well-known/genid/{id}'; @@ -70,4 +71,10 @@ public function getIriFromResource(object|string $resource, int $referenceType = return $this->router->generate('api_genid', ['id' => $id], $referenceType); } + + public function reset(): void + { + $this->objectHashMap = new \SplObjectStorage(); + $this->classHashMap = []; + } } diff --git a/tests/Symfony/Routing/SkolemIriConverterTest.php b/tests/Symfony/Routing/SkolemIriConverterTest.php new file mode 100644 index 00000000000..62e7e5ee5e4 --- /dev/null +++ b/tests/Symfony/Routing/SkolemIriConverterTest.php @@ -0,0 +1,105 @@ + + * + * 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\Routing; + +use ApiPlatform\Symfony\Routing\SkolemIriConverter; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Routing\RouterInterface; +use Symfony\Contracts\Service\ResetInterface; + +class SkolemIriConverterTest extends TestCase +{ + public function testImplementsResetInterface(): void + { + $router = $this->createStub(RouterInterface::class); + $converter = new SkolemIriConverter($router); + + $this->assertInstanceOf(ResetInterface::class, $converter); + } + + public function testResetClearsObjectHashMap(): void + { + $generatedIds = []; + $router = $this->createStub(RouterInterface::class); + $router->method('generate')->willReturnCallback(static function (string $name, array $params) use (&$generatedIds) { + $generatedIds[] = $params['id']; + + return '/.well-known/genid/'.$params['id']; + }); + + $converter = new SkolemIriConverter($router); + + $resource = new \stdClass(); + $converter->getIriFromResource($resource); + $firstId = $generatedIds[0]; + + $converter->getIriFromResource($resource); + $this->assertSame($firstId, $generatedIds[1]); + + $converter->reset(); + + $converter->getIriFromResource($resource); + $this->assertNotSame($firstId, $generatedIds[2]); + } + + public function testResetClearsClassHashMap(): void + { + $generatedIds = []; + $router = $this->createStub(RouterInterface::class); + $router->method('generate')->willReturnCallback(static function (string $name, array $params) use (&$generatedIds) { + $generatedIds[] = $params['id']; + + return '/.well-known/genid/'.$params['id']; + }); + + $converter = new SkolemIriConverter($router); + + $converter->getIriFromResource(\stdClass::class); + $firstId = $generatedIds[0]; + + $converter->getIriFromResource(\stdClass::class); + $this->assertSame($firstId, $generatedIds[1]); + + $converter->reset(); + + $converter->getIriFromResource(\stdClass::class); + $this->assertNotSame($firstId, $generatedIds[2]); + } + + public function testResetAllowsConverterToBeReused(): void + { + $generatedIds = []; + $router = $this->createStub(RouterInterface::class); + $router->method('generate')->willReturnCallback(static function (string $name, array $params) use (&$generatedIds) { + $generatedIds[] = $params['id']; + + return '/.well-known/genid/'.$params['id']; + }); + + $converter = new SkolemIriConverter($router); + + // Simulate multiple request cycles + for ($i = 0; $i < 3; ++$i) { + $resource = new \stdClass(); + $converter->getIriFromResource($resource); + $converter->getIriFromResource('SomeClass'); + $converter->reset(); + } + + // Each cycle should generate 2 new IDs (object + class), total 6 + $this->assertCount(6, $generatedIds); + // All IDs should be unique (no stale cache) + $this->assertCount(6, array_unique($generatedIds)); + } +} From 048b8117658b08606264380fa59bfc610112f537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Cimb=C3=A1l?= Date: Sun, 8 Mar 2026 16:58:27 +0100 Subject: [PATCH 2/3] fix(symfony): clear SkolemIriConverter state between requests via ResetInterface --- src/Symfony/Bundle/Resources/config/api.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/Resources/config/api.php b/src/Symfony/Bundle/Resources/config/api.php index 0daa6e7efd5..c1685a0c4ae 100644 --- a/src/Symfony/Bundle/Resources/config/api.php +++ b/src/Symfony/Bundle/Resources/config/api.php @@ -194,8 +194,7 @@ ->tag('routing.loader'); $services->set('api_platform.symfony.iri_converter.skolem', SkolemIriConverter::class) - ->args([service('api_platform.router')]) - ->tag('kernel.reset', ['method' => 'reset']); + ->args([service('api_platform.router')]); $services->set('api_platform.api.identifiers_extractor', IdentifiersExtractor::class) ->args([ From 9c480975ba31cbad2fed92d2908264b03572be04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Cimb=C3=A1l?= Date: Sun, 8 Mar 2026 17:25:43 +0100 Subject: [PATCH 3/3] fix(symfony): clear SkolemIriConverter state between requests via ResetInterface --- tests/Symfony/Routing/SkolemIriConverterTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Symfony/Routing/SkolemIriConverterTest.php b/tests/Symfony/Routing/SkolemIriConverterTest.php index 62e7e5ee5e4..a2cf2bed0f1 100644 --- a/tests/Symfony/Routing/SkolemIriConverterTest.php +++ b/tests/Symfony/Routing/SkolemIriConverterTest.php @@ -93,7 +93,7 @@ public function testResetAllowsConverterToBeReused(): void for ($i = 0; $i < 3; ++$i) { $resource = new \stdClass(); $converter->getIriFromResource($resource); - $converter->getIriFromResource('SomeClass'); + $converter->getIriFromResource(\stdClass::class); $converter->reset(); }