diff --git a/src/TypeGenerator.php b/src/TypeGenerator.php index 1220fa679..a330a1ae8 100644 --- a/src/TypeGenerator.php +++ b/src/TypeGenerator.php @@ -125,6 +125,16 @@ public function mapAnnotatedObject(string $annotatedObjectClassName): MutableInt $type->description = $resolvedDescription; } + // Instantiating the annotated object via the container (above) can reentrantly resolve and + // register this very type — e.g. a dependency in the container graph references it. If so, + // reuse that instance rather than return the duplicate we just built, which would later trip + // the registry identity check in RecursiveTypeMapper (the first-request crash in long-lived + // workers). mapFactoryMethod() guards its own cache the same way after its container->get. + // See https://github.com/thecodingmachine/graphqlite/issues/531 + if ($this->typeRegistry->hasType($type->name)) { + return $this->typeRegistry->getMutableInterface($type->name); + } + return $type; } diff --git a/tests/TypeGeneratorTest.php b/tests/TypeGeneratorTest.php index 131e4ec22..3a15de506 100644 --- a/tests/TypeGeneratorTest.php +++ b/tests/TypeGeneratorTest.php @@ -51,4 +51,36 @@ public function testextendAnnotatedObjectException(): void $this->expectException(MissingAnnotationException::class); $typeGenerator->extendAnnotatedObject(new stdClass(), $type); } + + public function testMapAnnotatedObjectReusesTypeRegisteredReentrantlyDuringContainerGet(): void + { + // Reproduces the cold-registry race behind #531: instantiating the annotated object via the + // container reentrantly resolves (and registers) this same type. mapAnnotatedObject must + // reuse that instance rather than build a duplicate — otherwise RecursiveTypeMapper's + // identity check throws "Cached type in registry is not the type returned by type mapper." + // on the first request in long-lived workers (Swoole/RoadRunner/FrankenPHP). + $typeRegistry = $this->getTypeRegistry(); + $reentrantlyRegistered = new MutableObjectType(['name' => 'TestObject', 'fields' => []], TypeFoo::class); + + $container = new LazyContainer([ + TypeFoo::class => static function () use ($typeRegistry, $reentrantlyRegistered) { + if (! $typeRegistry->hasType('TestObject')) { + $typeRegistry->registerType($reentrantlyRegistered); + } + + return new TypeFoo(); + }, + ]); + + $typeGenerator = new TypeGenerator( + $this->getAnnotationReader(), + new NamingStrategy(), + $typeRegistry, + $container, + $this->getTypeMapper(), + $this->getFieldsBuilder(), + ); + + $this->assertSame($reentrantlyRegistered, $typeGenerator->mapAnnotatedObject(TypeFoo::class)); + } }