From b524b65ec62be1929ec24ab0703a199983031ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Moitie=CC=81?= Date: Thu, 11 Jun 2026 12:54:29 +0100 Subject: [PATCH 1/2] Pass scalar overrides explicitly to keep lazy type loading lazy On webonyx/graphql-php >= 15.31.0, the first lookup of a built-in scalar triggers scalar-override discovery, which resolves the lazy types callable and eagerly builds every type in the schema from the AST on every request. When SchemaConfig::setScalarOverrides is available, determine the overrides cheaply from the document AST and pass them explicitly so the scan is skipped and the types callable stays unresolved. Fixes #2771. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 4 ++ src/Schema/SchemaBuilder.php | 18 ++++++++ tests/Unit/Schema/SchemaBuilderTest.php | 55 +++++++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65a8992ad8..f8975c0931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +### Fixed + +- Keep lazy type loading lazy on `webonyx/graphql-php >= 15.31.0` by passing scalar overrides explicitly when `SchemaConfig::setScalarOverrides` is available https://github.com/nuwave/lighthouse/pull/2772 + ## v6.67.0 ### Changed diff --git a/src/Schema/SchemaBuilder.php b/src/Schema/SchemaBuilder.php index b6df5884f1..d5bdce4395 100644 --- a/src/Schema/SchemaBuilder.php +++ b/src/Schema/SchemaBuilder.php @@ -4,6 +4,7 @@ use GraphQL\GraphQL; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; use GraphQL\Type\SchemaConfig; @@ -70,6 +71,23 @@ protected function build(DocumentAST $documentAST): Schema fn (): array => $this->typeRegistry->possibleTypes(), ); + // Passing scalar overrides explicitly prevents the first lookup of a built-in scalar + // from discovering them by resolving the lazy types callable, which would eagerly + // build every type in the schema, see https://github.com/nuwave/lighthouse/issues/2771. + // TODO remove this check when the minimum version of webonyx/graphql-php includes the method + if (method_exists($config, 'setScalarOverrides')) { + $scalarOverrides = []; + foreach (Type::BUILT_IN_SCALAR_NAMES as $name) { + if (isset($documentAST->types[$name])) { + $type = $this->typeRegistry->get($name); + assert($type instanceof ScalarType); + $scalarOverrides[] = $type; + } + } + + $config->setScalarOverrides($scalarOverrides); + } + // There is no way to resolve directives lazily, so we convert them eagerly $directiveFactory = new DirectiveFactory( new ExecutableTypeNodeConverter($this->typeRegistry), diff --git a/tests/Unit/Schema/SchemaBuilderTest.php b/tests/Unit/Schema/SchemaBuilderTest.php index a9962c8a56..a7db0da6ca 100644 --- a/tests/Unit/Schema/SchemaBuilderTest.php +++ b/tests/Unit/Schema/SchemaBuilderTest.php @@ -8,9 +8,13 @@ use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\ScalarType; +use GraphQL\Type\Definition\Type; use Nuwave\Lighthouse\Schema\RootType; use Nuwave\Lighthouse\Schema\SchemaBuilder; +use Nuwave\Lighthouse\Schema\TypeRegistry; use Tests\TestCase; +use Tests\Utils\Scalars\Email; final class SchemaBuilderTest extends TestCase { @@ -209,6 +213,57 @@ public function testExtendTypes(): void $this->assertSame('yo?', $type->getField('bar')->description); } + public function testRegistersExplicitlyEmptyScalarOverrides(): void + { + $schema = $this->buildSchemaWithPlaceholderQuery(''); + + $config = $schema->getConfig(); + if (! method_exists($config, 'getScalarOverrides')) { + $this->markTestSkipped('Requires a version of webonyx/graphql-php that supports SchemaConfig::setScalarOverrides.'); + } + + $this->assertSame([], $config->getScalarOverrides()); + } + + public function testRegistersRedefinedBuiltInScalarsAsScalarOverrides(): void + { + $schema = $this->buildSchemaWithPlaceholderQuery(/** @lang GraphQL */ ' + scalar String @scalar(class: "Email") + '); + + $config = $schema->getConfig(); + if (! method_exists($config, 'getScalarOverrides')) { + $this->markTestSkipped('Requires a version of webonyx/graphql-php that supports SchemaConfig::setScalarOverrides.'); + } + + $scalarOverrides = $config->getScalarOverrides(); + $this->assertNotNull($scalarOverrides); + $this->assertCount(1, $scalarOverrides); + + $stringOverride = reset($scalarOverrides); + $this->assertInstanceOf(Email::class, $stringOverride); + $this->assertSame(Type::STRING, $stringOverride->name); + } + + public function testBuiltInScalarLookupDoesNotResolveAllTypes(): void + { + if (! method_exists(SchemaConfig::class, 'setScalarOverrides')) { + $this->markTestSkipped('Requires a version of webonyx/graphql-php that supports SchemaConfig::setScalarOverrides.'); + } + + $schema = $this->buildSchemaWithPlaceholderQuery(/** @lang GraphQL */ ' + type Foo { + bar: Int + } + '); + + $booleanType = $schema->getType(Type::BOOLEAN); + $this->assertInstanceOf(ScalarType::class, $booleanType); + + $typeRegistry = $this->app->make(TypeRegistry::class); + $this->assertArrayNotHasKey('Foo', $typeRegistry->resolvedTypes()); + } + public function testResolvesEnumDefaultValuesToInternalValues(): void { $schema = $this->buildSchema(/** @lang GraphQL */ <<<'GRAPHQL' From 52f92bc9e632d2ce643741ff7207559589cb9070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Moitie=CC=81?= Date: Wed, 22 Jul 2026 15:42:34 +0100 Subject: [PATCH 2/2] Detect scalar overrides in TypeRegistry, fix test guard Move override detection into TypeRegistry::scalarOverrides() so it also covers types registered programmatically via overwrite()/overwriteLazy(), not only SDL definitions - the types scan on graphql-php >= 15.31 finds those too, so explicit overrides must match. Fix the laziness test guard referencing SchemaConfig without an import, which made PHPStan fail on CI and silently skipped the test at runtime. Co-Authored-By: Claude Fable 5 --- src/Schema/SchemaBuilder.php | 12 +---------- src/Schema/TypeRegistry.php | 26 +++++++++++++++++++++++ tests/Unit/Schema/SchemaBuilderTest.php | 28 ++++++++++++++++++++++--- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/Schema/SchemaBuilder.php b/src/Schema/SchemaBuilder.php index d5bdce4395..5f6d6c7022 100644 --- a/src/Schema/SchemaBuilder.php +++ b/src/Schema/SchemaBuilder.php @@ -4,7 +4,6 @@ use GraphQL\GraphQL; use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; use GraphQL\Type\SchemaConfig; @@ -76,16 +75,7 @@ protected function build(DocumentAST $documentAST): Schema // build every type in the schema, see https://github.com/nuwave/lighthouse/issues/2771. // TODO remove this check when the minimum version of webonyx/graphql-php includes the method if (method_exists($config, 'setScalarOverrides')) { - $scalarOverrides = []; - foreach (Type::BUILT_IN_SCALAR_NAMES as $name) { - if (isset($documentAST->types[$name])) { - $type = $this->typeRegistry->get($name); - assert($type instanceof ScalarType); - $scalarOverrides[] = $type; - } - } - - $config->setScalarOverrides($scalarOverrides); + $config->setScalarOverrides($this->typeRegistry->scalarOverrides()); } // There is no way to resolve directives lazily, so we convert them eagerly diff --git a/src/Schema/TypeRegistry.php b/src/Schema/TypeRegistry.php index 9d23e6a55c..676cedcb99 100644 --- a/src/Schema/TypeRegistry.php +++ b/src/Schema/TypeRegistry.php @@ -239,6 +239,32 @@ public function possibleTypes(): array return array_filter($this->types); } + /** + * Built-in scalar types that are overridden in this schema. + * + * A scalar override is a type named after a built-in scalar such as `String`, + * defined in the schema or registered programmatically. + * + * @return array + */ + public function scalarOverrides(): array + { + $overrides = []; + foreach (Type::BUILT_IN_SCALAR_NAMES as $name) { + if ( + isset($this->types[$name]) + || isset($this->documentAST->types[$name]) + || isset($this->lazyTypes[$name]) + ) { + $override = $this->get($name); + assert($override instanceof ScalarType); + $overrides[] = $override; + } + } + + return $overrides; + } + /** * Get the types that are currently resolved. * diff --git a/tests/Unit/Schema/SchemaBuilderTest.php b/tests/Unit/Schema/SchemaBuilderTest.php index a7db0da6ca..6d341e0f0f 100644 --- a/tests/Unit/Schema/SchemaBuilderTest.php +++ b/tests/Unit/Schema/SchemaBuilderTest.php @@ -240,23 +240,45 @@ public function testRegistersRedefinedBuiltInScalarsAsScalarOverrides(): void $this->assertNotNull($scalarOverrides); $this->assertCount(1, $scalarOverrides); - $stringOverride = reset($scalarOverrides); + $stringOverride = $scalarOverrides[Type::STRING]; $this->assertInstanceOf(Email::class, $stringOverride); $this->assertSame(Type::STRING, $stringOverride->name); } - public function testBuiltInScalarLookupDoesNotResolveAllTypes(): void + public function testRegistersProgrammaticallyOverwrittenBuiltInScalarsAsScalarOverrides(): void { - if (! method_exists(SchemaConfig::class, 'setScalarOverrides')) { + $typeRegistry = $this->app->make(TypeRegistry::class); + $typeRegistry->overwrite(new Email(['name' => Type::STRING])); + + $schema = $this->buildSchemaWithPlaceholderQuery(''); + + $config = $schema->getConfig(); + if (! method_exists($config, 'getScalarOverrides')) { $this->markTestSkipped('Requires a version of webonyx/graphql-php that supports SchemaConfig::setScalarOverrides.'); } + $scalarOverrides = $config->getScalarOverrides(); + $this->assertNotNull($scalarOverrides); + $this->assertCount(1, $scalarOverrides); + + $stringOverride = $scalarOverrides[Type::STRING]; + $this->assertInstanceOf(Email::class, $stringOverride); + $this->assertSame(Type::STRING, $stringOverride->name); + } + + public function testBuiltInScalarLookupDoesNotResolveAllTypes(): void + { $schema = $this->buildSchemaWithPlaceholderQuery(/** @lang GraphQL */ ' type Foo { bar: Int } '); + $config = $schema->getConfig(); + if (! method_exists($config, 'getScalarOverrides')) { + $this->markTestSkipped('Requires a version of webonyx/graphql-php that supports SchemaConfig::setScalarOverrides.'); + } + $booleanType = $schema->getType(Type::BOOLEAN); $this->assertInstanceOf(ScalarType::class, $booleanType);