From 3ae9aaf524785155630921b988f4b1c4521bb03d Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 10 Mar 2026 10:08:27 +0100 Subject: [PATCH 1/6] Support per-schema scalar overrides via types config and typeLoader Allows users to override built-in scalars (e.g. a custom String that trims whitespace) on a per-schema basis, without the global side effects of Type::overrideStandardTypes(). Users pass custom scalars via the types config or typeLoader. The executor re-resolves standard scalars from the schema so overrides apply even for fields declared with Type::string() etc. https://github.com/webonyx/graphql-php/issues/1424 Co-Authored-By: Claude Opus 4.6 --- benchmarks/ScalarOverrideBench.php | 109 +++++++++ .../06-per-schema-scalar-override/example.php | 70 ++++++ src/Executor/ReferenceExecutor.php | 7 +- src/Type/Schema.php | 101 ++++++++ tests/Executor/ExecutorLazySchemaTest.php | 3 + tests/Type/ScalarOverridesTest.php | 229 ++++++++++++++++++ tests/Type/StandardTypesTest.php | 67 +++++ 7 files changed, 585 insertions(+), 1 deletion(-) create mode 100644 benchmarks/ScalarOverrideBench.php create mode 100644 examples/06-per-schema-scalar-override/example.php create mode 100644 tests/Type/ScalarOverridesTest.php diff --git a/benchmarks/ScalarOverrideBench.php b/benchmarks/ScalarOverrideBench.php new file mode 100644 index 000000000..e6e6c0b03 --- /dev/null +++ b/benchmarks/ScalarOverrideBench.php @@ -0,0 +1,109 @@ + Type::STRING, + 'serialize' => static fn ($value): string => strtoupper((string) $value), + ]); + + $queryTypeBaseline = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => Type::string(), + 'resolve' => static fn (): string => 'hello world', + ], + ], + ]); + $this->schemaBaseline = new Schema([ + 'query' => $queryTypeBaseline, + ]); + + $queryTypeLoader = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => Type::string(), + 'resolve' => static fn (): string => 'hello world', + ], + ], + ]); + $typesForLoader = ['Query' => $queryTypeLoader, 'String' => $uppercaseString]; + $this->schemaTypeLoader = new Schema([ + 'query' => $queryTypeLoader, + 'typeLoader' => static fn (string $name): ?Type => $typesForLoader[$name] ?? null, + ]); + + $queryTypeTypes = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => Type::string(), + 'resolve' => static fn (): string => 'hello world', + ], + ], + ]); + $this->schemaTypes = new Schema([ + 'query' => $queryTypeTypes, + 'types' => [$uppercaseString], + ]); + } + + public function benchGetTypeWithoutOverride(): void + { + $this->schemaBaseline->getType('String'); + } + + public function benchGetTypeWithTypeLoaderOverride(): void + { + $this->schemaTypeLoader->getType('String'); + } + + public function benchGetTypeWithTypesOverride(): void + { + $this->schemaTypes->getType('String'); + } + + public function benchExecuteWithoutOverride(): void + { + GraphQL::executeQuery($this->schemaBaseline, '{ greeting }'); + } + + public function benchExecuteWithTypeLoaderOverride(): void + { + GraphQL::executeQuery($this->schemaTypeLoader, '{ greeting }'); + } + + public function benchExecuteWithTypesOverride(): void + { + GraphQL::executeQuery($this->schemaTypes, '{ greeting }'); + } +} diff --git a/examples/06-per-schema-scalar-override/example.php b/examples/06-per-schema-scalar-override/example.php new file mode 100644 index 000000000..9aa5bd87b --- /dev/null +++ b/examples/06-per-schema-scalar-override/example.php @@ -0,0 +1,70 @@ + Type::STRING, + 'serialize' => static fn ($value): string => trim((string) $value), + 'parseValue' => static fn ($value): string => trim((string) $value), + 'parseLiteral' => static fn ($valueNode): string => trim($valueNode->value), +]); + +$queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => Type::string(), + 'args' => [ + 'name' => ['type' => Type::string()], + ], + 'resolve' => static fn ($root, array $args): string => " Hello, {$args['name']}! ", + ], + ], +]); + +// Override via types config +$schemaViaTypes = new Schema([ + 'query' => $queryType, + 'types' => [$trimmedString], +]); + +$result = GraphQL::executeQuery($schemaViaTypes, '{ greeting(name: "World") }'); +echo "Override via types config:\n"; +echo json_encode($result->toArray(), JSON_THROW_ON_ERROR) . "\n\n"; + +// Override via typeLoader +$queryTypeForLoader = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => Type::string(), + 'args' => [ + 'name' => ['type' => Type::string()], + ], + 'resolve' => static fn ($root, array $args): string => " Hello, {$args['name']}! ", + ], + ], +]); + +$types = ['Query' => $queryTypeForLoader, 'String' => $trimmedString]; +$schemaViaTypeLoader = new Schema([ + 'query' => $queryTypeForLoader, + 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, +]); + +$result = GraphQL::executeQuery($schemaViaTypeLoader, '{ greeting(name: "World") }'); +echo "Override via typeLoader:\n"; +echo json_encode($result->toArray(), JSON_THROW_ON_ERROR) . "\n"; + +// Expected output: +// Override via types config: +// {"data":{"greeting":"Hello, World!"}} +// +// Override via typeLoader: +// {"data":{"greeting":"Hello, World!"}} diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index 53c52dd8a..ea2c76f5c 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -911,11 +911,16 @@ protected function completeValue( // Account for invalid schema definition when typeLoader returns different // instance than `resolveType` or $field->getType() or $arg->getType() assert( - $returnType === $this->exeContext->schema->getType($returnType->name), + $returnType === $this->exeContext->schema->getType($returnType->name) + || in_array($returnType->name, Type::STANDARD_TYPE_NAMES, true), SchemaValidationContext::duplicateType($this->exeContext->schema, "{$info->parentType}.{$info->fieldName}", $returnType->name) ); if ($returnType instanceof LeafType) { + // Fields declared with Type::string() etc. reference global singletons, + // but the schema may have per-schema scalar overrides. + $returnType = $this->exeContext->schema->resolveStandardScalar($returnType->name) ?? $returnType; + return $this->completeLeafValue($returnType, $result); } diff --git a/src/Type/Schema.php b/src/Type/Schema.php index 03fc0034a..f812c291b 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -14,6 +14,7 @@ use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\NamedType; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; use GraphQL\Utils\InterfaceImplementations; @@ -66,6 +67,9 @@ class Schema /** True when $resolvedTypes contains all possible schema types. */ private bool $fullyLoaded = false; + /** @var array */ + private array $standardScalarOverrides = []; + /** @var array */ private array $validationErrors; @@ -126,6 +130,12 @@ public function getTypeMap(): array // Reset order of user provided types, since calls to getType() may have loaded them $this->resolvedTypes = []; + // Separate built-in scalar overrides to avoid identity conflicts + // with Type::string() etc. references in field definitions during extractTypes. + /** @var array $scalarOverrides */ + $scalarOverrides = []; + $standardTypes = Type::getStandardTypes(); + foreach ($types as $typeOrLazyType) { /** @var Type|callable(): Type $typeOrLazyType */ $type = self::resolveType($typeOrLazyType); @@ -133,6 +143,15 @@ public function getTypeMap(): array /** @var string $typeName Necessary assertion for PHPStan + PHP 8.2 */ $typeName = $type->name; + + if ($type instanceof ScalarType + && isset($standardTypes[$typeName]) + && $type !== $standardTypes[$typeName] + ) { + $scalarOverrides[$typeName] = $type; + continue; + } + assert( ! isset($this->resolvedTypes[$typeName]) || $type === $this->resolvedTypes[$typeName], "Schema must contain unique named types but contains multiple types named \"{$type}\" (see https://webonyx.github.io/graphql-php/type-definitions/#type-registry).", @@ -166,6 +185,25 @@ public function getTypeMap(): array } TypeInfo::extractTypes(Introspection::_schema(), $allReferencedTypes); + // Apply scalar overrides after all extractions, replacing the + // global singletons with user-provided instances. + foreach ($scalarOverrides as $name => $override) { + $allReferencedTypes[$name] = $override; + } + + if (isset($this->config->typeLoader)) { + foreach (Type::STANDARD_TYPE_NAMES as $scalarName) { + if (isset($scalarOverrides[$scalarName])) { + continue; + } + + $type = ($this->config->typeLoader)($scalarName); + if ($type instanceof ScalarType && $type->name === $scalarName && $type !== $standardTypes[$scalarName]) { + $allReferencedTypes[$scalarName] = $type; + } + } + } + $this->resolvedTypes = $allReferencedTypes; $this->fullyLoaded = true; } @@ -311,6 +349,69 @@ public function getType(string $name): ?Type return $this->resolvedTypes[$name] = self::resolveType($type); } + /** + * Returns a per-schema standard scalar override if one exists, + * or null if the global singleton should be used. + * + * Used by the executor to apply per-schema scalar overrides even + * for fields declared with Type::string() etc. + */ + public function resolveStandardScalar(string $name): ?ScalarType + { + if (array_key_exists($name, $this->standardScalarOverrides)) { + return $this->standardScalarOverrides[$name]; + } + + $standardTypes = Type::getStandardTypes(); + if (! isset($standardTypes[$name])) { + return null; + } + + if (isset($this->resolvedTypes[$name]) && $this->resolvedTypes[$name] !== $standardTypes[$name]) { + $type = $this->resolvedTypes[$name]; + assert($type instanceof ScalarType); + $this->standardScalarOverrides[$name] = $type; + + return $type; + } + + $override = $this->fullyLoaded + ? null + : $this->findStandardScalarOverride($name, $standardTypes[$name]); + $this->standardScalarOverrides[$name] = $override; + + return $override; + } + + private function findStandardScalarOverride(string $name, ScalarType $standardType): ?ScalarType + { + $types = $this->config->types; + if (is_callable($types)) { + $types = $types(); + } + + foreach ($types as $typeOrLazyType) { + /** @var Type|callable(): Type $typeOrLazyType */ + $type = self::resolveType($typeOrLazyType); + if ($type instanceof ScalarType && $type->name === $name && $type !== $standardType) { + $this->resolvedTypes[$name] = $type; + + return $type; + } + } + + if (isset($this->config->typeLoader)) { + $type = ($this->config->typeLoader)($name); + if ($type instanceof ScalarType && $type->name === $name && $type !== $standardType) { + $this->resolvedTypes[$name] = $type; + + return $type; + } + } + + return null; + } + /** @throws InvariantViolation */ public function hasType(string $name): bool { diff --git a/tests/Executor/ExecutorLazySchemaTest.php b/tests/Executor/ExecutorLazySchemaTest.php index 0ffedc6bb..97f87bcb5 100644 --- a/tests/Executor/ExecutorLazySchemaTest.php +++ b/tests/Executor/ExecutorLazySchemaTest.php @@ -213,6 +213,7 @@ public function testSimpleQuery(): void 'Query.fields', 'SomeObject', 'SomeObject.fields', + 'String', ]; self::assertSame($expected, $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); self::assertSame($expectedExecutorCalls, $this->calls); @@ -379,6 +380,7 @@ public function testDeepQuery(): void 'Query' => true, 'SomeObject' => true, 'OtherObject' => true, + 'String' => true, ], $this->loadedTypes ); @@ -387,6 +389,7 @@ public function testDeepQuery(): void 'Query.fields', 'SomeObject', 'SomeObject.fields', + 'String', ], $this->calls ); diff --git a/tests/Type/ScalarOverridesTest.php b/tests/Type/ScalarOverridesTest.php new file mode 100644 index 000000000..ebc224e53 --- /dev/null +++ b/tests/Type/ScalarOverridesTest.php @@ -0,0 +1,229 @@ + */ + private static array $originalStandardTypes; + + public static function setUpBeforeClass(): void + { + self::$originalStandardTypes = Type::getStandardTypes(); + } + + public function tearDown(): void + { + parent::tearDown(); + Type::overrideStandardTypes(self::$originalStandardTypes); + } + + public function testTypeLoaderOverrideWorksEndToEnd(): void + { + $uppercaseString = self::createUppercaseString(); + $queryType = self::createQueryType(); + $types = ['Query' => $queryType, 'String' => $uppercaseString]; + + $schema = new Schema([ + 'query' => $queryType, + 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + ]); + + $schema->assertValid(); + + $result = GraphQL::executeQuery($schema, '{ greeting }'); + + self::assertSame(['data' => ['greeting' => 'HELLO WORLD']], $result->toArray()); + } + + public function testTypeLoaderOverrideWorksInProductionMode(): void + { + $assertActive = (int) ini_get('assert.active'); + @ini_set('assert.active', '0'); + + try { + $uppercaseString = self::createUppercaseString(); + $queryType = self::createQueryType(); + $types = ['Query' => $queryType, 'String' => $uppercaseString]; + + $schema = new Schema([ + 'query' => $queryType, + 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + ]); + + $result = GraphQL::executeQuery($schema, '{ greeting }'); + + self::assertSame(['data' => ['greeting' => 'HELLO WORLD']], $result->toArray()); + } finally { + @ini_set('assert.active', (string) $assertActive); + } + } + + public function testTypesConfigOverrideWorksEndToEnd(): void + { + $uppercaseString = self::createUppercaseString(); + + $schema = new Schema([ + 'query' => self::createQueryType(), + 'types' => [$uppercaseString], + ]); + + $schema->assertValid(); + + $result = GraphQL::executeQuery($schema, '{ greeting }'); + + self::assertSame(['data' => ['greeting' => 'HELLO WORLD']], $result->toArray()); + } + + public function testTypesConfigOverrideWorksWithAssumeValid(): void + { + $uppercaseString = self::createUppercaseString(); + + $config = SchemaConfig::create([ + 'query' => self::createQueryType(), + 'types' => [$uppercaseString], + ]); + $config->setAssumeValid(true); + + $schema = new Schema($config); + + $result = GraphQL::executeQuery($schema, '{ greeting }'); + + self::assertSame(['data' => ['greeting' => 'HELLO WORLD']], $result->toArray()); + } + + public function testIntrospectionUsesOverriddenScalar(): void + { + $uppercaseString = self::createUppercaseString(); + $queryType = self::createQueryType(); + $types = ['Query' => $queryType, 'String' => $uppercaseString]; + + $schema = new Schema([ + 'query' => $queryType, + 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + ]); + + $result = GraphQL::executeQuery($schema, '{ __type(name: "Query") { fields { name } } }'); + + $data = $result->toArray()['data'] ?? []; + $fields = $data['__type']['fields']; + $fieldNames = array_column($fields, 'name'); + + self::assertContains('GREETING', $fieldNames); + } + + public function testTwoSchemasWithDifferentOverridesAreIndependent(): void + { + $uppercaseString = new CustomScalarType([ + 'name' => Type::STRING, + 'serialize' => static fn ($value): string => strtoupper((string) $value), + ]); + $reversedString = new CustomScalarType([ + 'name' => Type::STRING, + 'serialize' => static fn ($value): string => strrev((string) $value), + ]); + + $queryTypeA = self::createQueryType(); + $typesA = ['Query' => $queryTypeA, 'String' => $uppercaseString]; + $schemaA = new Schema([ + 'query' => $queryTypeA, + 'typeLoader' => static fn (string $name): ?Type => $typesA[$name] ?? null, + ]); + + $queryTypeB = self::createQueryType(); + $typesB = ['Query' => $queryTypeB, 'String' => $reversedString]; + $schemaB = new Schema([ + 'query' => $queryTypeB, + 'typeLoader' => static fn (string $name): ?Type => $typesB[$name] ?? null, + ]); + + $resultA = GraphQL::executeQuery($schemaA, '{ greeting }'); + $resultB = GraphQL::executeQuery($schemaB, '{ greeting }'); + + self::assertSame(['data' => ['greeting' => 'HELLO WORLD']], $resultA->toArray()); + self::assertSame(['data' => ['greeting' => 'dlrow olleh']], $resultB->toArray()); + } + + public function testNonOverriddenScalarsAreUnaffected(): void + { + $uppercaseString = self::createUppercaseString(); + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => Type::string(), + 'resolve' => static fn (): string => 'hello world', + ], + 'count' => [ + 'type' => Type::int(), + 'resolve' => static fn (): int => 42, + ], + 'ratio' => [ + 'type' => Type::float(), + 'resolve' => static fn (): float => 3.14, + ], + 'active' => [ + 'type' => Type::boolean(), + 'resolve' => static fn (): bool => true, + ], + 'identifier' => [ + 'type' => Type::id(), + 'resolve' => static fn (): string => 'abc-123', + ], + ], + ]); + + $types = ['Query' => $queryType, 'String' => $uppercaseString]; + + $schema = new Schema([ + 'query' => $queryType, + 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + ]); + + $result = GraphQL::executeQuery($schema, '{ greeting count ratio active identifier }'); + $data = $result->toArray()['data'] ?? []; + + self::assertSame('HELLO WORLD', $data['greeting']); + self::assertSame(42, $data['count']); + self::assertSame(3.14, $data['ratio']); + self::assertTrue($data['active']); + self::assertSame('abc-123', $data['identifier']); + } + + /** @throws InvariantViolation */ + private static function createUppercaseString(): CustomScalarType + { + return new CustomScalarType([ + 'name' => Type::STRING, + 'serialize' => static fn ($value): string => strtoupper((string) $value), + ]); + } + + /** @throws InvariantViolation */ + private static function createQueryType(): ObjectType + { + return new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => Type::string(), + 'resolve' => static fn (): string => 'hello world', + ], + ], + ]); + } +} diff --git a/tests/Type/StandardTypesTest.php b/tests/Type/StandardTypesTest.php index 6a569f45e..afec2d818 100644 --- a/tests/Type/StandardTypesTest.php +++ b/tests/Type/StandardTypesTest.php @@ -3,12 +3,14 @@ namespace GraphQL\Tests\Type; use GraphQL\Error\InvariantViolation; +use GraphQL\GraphQL; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Introspection; +use GraphQL\Type\Schema; use PHPUnit\Framework\TestCase; final class StandardTypesTest extends TestCase @@ -147,6 +149,71 @@ public function testCachesShouldResetWhenOverridingStandardTypes(): void self::assertSame($newString, $newDeprecatedDirective->args[0]->getType()); } + /** @see ScalarOverridesTest for the per-schema alternative */ + public function testGlobalOverrideAffectsSchemaExecution(): void + { + $uppercaseString = self::createUppercaseString(); + Type::overrideStandardTypes([$uppercaseString]); + + $schema = new Schema([ + 'query' => self::createQueryType(), + ]); + + $result = GraphQL::executeQuery($schema, '{ greeting }'); + + self::assertSame(['data' => ['greeting' => 'HELLO WORLD']], $result->toArray()); + } + + /** + * Documents the exact problem from https://github.com/webonyx/graphql-php/issues/1424. + * + * @see ScalarOverridesTest for the per-schema alternative + */ + public function testStaticOverrideAffectsAllSchemas(): void + { + $schemaA = new Schema([ + 'query' => self::createQueryType(), + ]); + + $uppercaseString = self::createUppercaseString(); + Type::overrideStandardTypes([$uppercaseString]); + + new Schema([ + 'query' => self::createQueryType(), + ]); + + // Schema A was built before the override. Its query type fields reference + // the old String singleton, but introspection types (rebuilt by + // overrideStandardTypes) now hold the new String. When getTypeMap() + // encounters both instances during extractTypes, it throws. + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage('contains multiple types named "String"'); + $schemaA->getTypeMap(); + } + + /** @throws InvariantViolation */ + private static function createUppercaseString(): CustomScalarType + { + return new CustomScalarType([ + 'name' => Type::STRING, + 'serialize' => static fn ($value): string => strtoupper((string) $value), + ]); + } + + /** @throws InvariantViolation */ + private static function createQueryType(): ObjectType + { + return new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => Type::string(), + 'resolve' => static fn (): string => 'hello world', + ], + ], + ]); + } + /** @throws InvariantViolation */ private static function createCustomScalarType(string $name): CustomScalarType { From 5aca890e6fe4f8594a37cdcebef508629205f206 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 10 Mar 2026 10:22:50 +0100 Subject: [PATCH 2/6] Simplify scalar override resolution by flipping getType() order Instead of a separate resolveStandardScalar()/findStandardScalarOverride() code path, flip getType() to check loadType() before standard types. This lets the typeLoader naturally override built-in scalars like String, eliminating 3 methods/properties and ~65 lines of duplicate resolution logic. Co-Authored-By: Claude Opus 4.6 --- src/Executor/ReferenceExecutor.php | 7 +-- src/Type/Schema.php | 74 ++---------------------------- 2 files changed, 8 insertions(+), 73 deletions(-) diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index ea2c76f5c..6a82cca7d 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -917,9 +917,10 @@ protected function completeValue( ); if ($returnType instanceof LeafType) { - // Fields declared with Type::string() etc. reference global singletons, - // but the schema may have per-schema scalar overrides. - $returnType = $this->exeContext->schema->resolveStandardScalar($returnType->name) ?? $returnType; + $schemaType = $this->exeContext->schema->getType($returnType->name); + if ($schemaType instanceof LeafType) { + $returnType = $schemaType; + } return $this->completeLeafValue($returnType, $result); } diff --git a/src/Type/Schema.php b/src/Type/Schema.php index f812c291b..8ded0b770 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -67,9 +67,6 @@ class Schema /** True when $resolvedTypes contains all possible schema types. */ private bool $fullyLoaded = false; - /** @var array */ - private array $standardScalarOverrides = []; - /** @var array */ private array $validationErrors; @@ -336,77 +333,14 @@ public function getType(string $name): ?Type return $introspectionTypes[$name]; } - $standardTypes = Type::getStandardTypes(); - if (isset($standardTypes[$name])) { - return $standardTypes[$name]; - } - $type = $this->loadType($name); - if ($type === null) { - return null; - } - - return $this->resolvedTypes[$name] = self::resolveType($type); - } - - /** - * Returns a per-schema standard scalar override if one exists, - * or null if the global singleton should be used. - * - * Used by the executor to apply per-schema scalar overrides even - * for fields declared with Type::string() etc. - */ - public function resolveStandardScalar(string $name): ?ScalarType - { - if (array_key_exists($name, $this->standardScalarOverrides)) { - return $this->standardScalarOverrides[$name]; + if ($type !== null) { + return $this->resolvedTypes[$name] = self::resolveType($type); } $standardTypes = Type::getStandardTypes(); - if (! isset($standardTypes[$name])) { - return null; - } - - if (isset($this->resolvedTypes[$name]) && $this->resolvedTypes[$name] !== $standardTypes[$name]) { - $type = $this->resolvedTypes[$name]; - assert($type instanceof ScalarType); - $this->standardScalarOverrides[$name] = $type; - - return $type; - } - - $override = $this->fullyLoaded - ? null - : $this->findStandardScalarOverride($name, $standardTypes[$name]); - $this->standardScalarOverrides[$name] = $override; - - return $override; - } - - private function findStandardScalarOverride(string $name, ScalarType $standardType): ?ScalarType - { - $types = $this->config->types; - if (is_callable($types)) { - $types = $types(); - } - - foreach ($types as $typeOrLazyType) { - /** @var Type|callable(): Type $typeOrLazyType */ - $type = self::resolveType($typeOrLazyType); - if ($type instanceof ScalarType && $type->name === $name && $type !== $standardType) { - $this->resolvedTypes[$name] = $type; - - return $type; - } - } - - if (isset($this->config->typeLoader)) { - $type = ($this->config->typeLoader)($name); - if ($type instanceof ScalarType && $type->name === $name && $type !== $standardType) { - $this->resolvedTypes[$name] = $type; - - return $type; - } + if (isset($standardTypes[$name])) { + return $this->resolvedTypes[$name] = $standardTypes[$name]; } return null; From 7c93c86095a8fe8127f30433642b9bafe7396fc2 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Wed, 11 Mar 2026 17:23:37 +0100 Subject: [PATCH 3/6] Add built-in naming (builtInScalars, builtInDirectives) and deprecate standard/internal names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns with GraphQL spec terminology ("built-in scalars", "built-in directives"). New well-named alternatives ship alongside deprecated shims for backward compat: - Type::builtInScalars() / Type::BUILT_IN_SCALAR_NAMES — replaces getStandardTypes() / STANDARD_TYPE_NAMES - Type::builtInTypes() gains @api — replaces builtInTypes() (already well-named) - Directive::builtInDirectives() — replaces getInternalDirectives() - Directive::isBuiltInDirective() — replaces isSpecifiedDirective() - GraphQL facade methods deprecated, pointing to Type/Directive counterparts - Type constants included in generated class-reference docs (stable public API) - Internal references updated; StandardTypesTest left as deprecated regression test Co-Authored-By: Claude Sonnet 4.6 --- .ai/AGENTS.md | 6 +++ README.md | 5 +- docs/class-reference.md | 81 +++++++++++++++++++++++++++--- generate-class-reference.php | 2 +- src/Executor/ReferenceExecutor.php | 2 +- src/GraphQL.php | 12 +++-- src/Type/Definition/Directive.php | 20 +++++++- src/Type/Definition/Type.php | 80 ++++++++++++++++++++--------- src/Type/Schema.php | 21 ++++---- src/Utils/BuildClientSchema.php | 2 +- tests/Type/ScalarOverridesTest.php | 2 +- 11 files changed, 183 insertions(+), 50 deletions(-) diff --git a/.ai/AGENTS.md b/.ai/AGENTS.md index d801459e2..89151a197 100644 --- a/.ai/AGENTS.md +++ b/.ai/AGENTS.md @@ -20,6 +20,12 @@ make bench # Run PHPBench benchmarks make docs # Generate class reference docs ``` +## Public API + +Elements marked with `@api` in PHPDoc are part of the stable public API. + +Constants listed in the [class-reference docs](https://webonyx.github.io/graphql-php/class-reference/) (generated via `generate-class-reference.php` with `'constants' => true`) are also stable public API, even without an `@api` tag. + ## Code and Testing Expectations - Preserve backward compatibility for public APIs unless explicitly requested. diff --git a/README.md b/README.md index f6295a729..a84a60504 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,9 @@ with a specific README file per example. This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html). Elements that belong to the public API of this package are marked with the `@api` PHPDoc tag. -Those elements are thus guaranteed to be stable within major versions. All other elements are -not part of this backwards compatibility guarantee and may change between minor or patch versions. +Constants included in the [class-reference docs](https://webonyx.github.io/graphql-php/class-reference) are also part of the public API. +Those elements are thus guaranteed to be stable within major versions. +All other elements are not part of this backwards compatibility guarantee and may change between minor or patch versions. The most recent version is actively developed on [`master`](https://github.com/webonyx/graphql-php/tree/master). Older versions are generally no longer supported, although exceptions may be made for [sponsors](#sponsors). diff --git a/docs/class-reference.md b/docs/class-reference.md index 0102d10df..f6e627c43 100644 --- a/docs/class-reference.md +++ b/docs/class-reference.md @@ -108,6 +108,8 @@ static function promiseToExecute( /** * Returns directives defined in GraphQL spec. * + * @deprecated use {@see Directive::builtInDirectives()} + * * @throws InvariantViolation * * @return array @@ -119,7 +121,9 @@ static function getStandardDirectives(): array ```php /** - * Returns types defined in GraphQL spec. + * Returns built-in scalar types defined in GraphQL spec. + * + * @deprecated use {@see Type::builtInScalars()} * * @throws InvariantViolation * @@ -136,6 +140,8 @@ static function getStandardTypes(): array * * Standard types not listed here remain untouched. * + * @deprecated prefer per-schema scalar overrides via {@see \GraphQL\Type\SchemaConfig::$types} or {@see \GraphQL\Type\SchemaConfig::$typeLoader} + * * @param array $types * * @api @@ -180,13 +186,52 @@ static function setDefaultArgsMapper(callable $fn): void ## GraphQL\Type\Definition\Type -Registry of standard GraphQL types and base class for all other types. +Registry of built-in GraphQL types and base class for all other types. + +### GraphQL\Type\Definition\Type Constants + +```php +const INT = 'Int'; +const FLOAT = 'Float'; +const STRING = 'String'; +const BOOLEAN = 'Boolean'; +const ID = 'ID'; +const BUILT_IN_SCALAR_NAMES = [ + 'Int', + 'Float', + 'String', + 'Boolean', + 'ID', +]; +const STANDARD_TYPE_NAMES = [ + 'Int', + 'Float', + 'String', + 'Boolean', + 'ID', +]; +const BUILT_IN_TYPE_NAMES = [ + 'Int', + 'Float', + 'String', + 'Boolean', + 'ID', + '__Schema', + '__Type', + '__Directive', + '__Field', + '__InputValue', + '__EnumValue', + '__TypeKind', + '__DirectiveLocation', +]; +``` ### GraphQL\Type\Definition\Type Methods ```php /** - * Returns the registered or default standard Int type. + * Returns the built-in Int scalar type. * * @api */ @@ -195,7 +240,7 @@ static function int(): GraphQL\Type\Definition\ScalarType ```php /** - * Returns the registered or default standard Float type. + * Returns the built-in Float scalar type. * * @api */ @@ -204,7 +249,7 @@ static function float(): GraphQL\Type\Definition\ScalarType ```php /** - * Returns the registered or default standard String type. + * Returns the built-in String scalar type. * * @api */ @@ -213,7 +258,7 @@ static function string(): GraphQL\Type\Definition\ScalarType ```php /** - * Returns the registered or default standard Boolean type. + * Returns the built-in Boolean scalar type. * * @api */ @@ -222,7 +267,7 @@ static function boolean(): GraphQL\Type\Definition\ScalarType ```php /** - * Returns the registered or default standard ID type. + * Returns the built-in ID scalar type. * * @api */ @@ -255,6 +300,28 @@ static function listOf($type): GraphQL\Type\Definition\ListOfType static function nonNull($type): GraphQL\Type\Definition\NonNull ``` +```php +/** + * Returns all built-in types: built-in scalars and introspection types. + * + * @api + * + * @return array + */ +static function builtInTypes(): array +``` + +```php +/** + * Returns all built-in scalar types. + * + * @api + * + * @return array + */ +static function builtInScalars(): array +``` + ```php /** * Determines if the given type is an input type. diff --git a/generate-class-reference.php b/generate-class-reference.php index 70f25e11e..841ee88b4 100644 --- a/generate-class-reference.php +++ b/generate-class-reference.php @@ -10,7 +10,7 @@ const ENTRIES = [ GraphQL\GraphQL::class => [], - GraphQL\Type\Definition\Type::class => [], + GraphQL\Type\Definition\Type::class => ['constants' => true], GraphQL\Type\Definition\ResolveInfo::class => [], GraphQL\Language\DirectiveLocation::class => ['constants' => true], GraphQL\Type\SchemaConfig::class => [], diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index 6a82cca7d..ecd3514f5 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -912,7 +912,7 @@ protected function completeValue( // instance than `resolveType` or $field->getType() or $arg->getType() assert( $returnType === $this->exeContext->schema->getType($returnType->name) - || in_array($returnType->name, Type::STANDARD_TYPE_NAMES, true), + || in_array($returnType->name, Type::BUILT_IN_SCALAR_NAMES, true), SchemaValidationContext::duplicateType($this->exeContext->schema, "{$info->parentType}.{$info->fieldName}", $returnType->name) ); diff --git a/src/GraphQL.php b/src/GraphQL.php index 919b09dec..b96d489ff 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -180,6 +180,8 @@ public static function promiseToExecute( /** * Returns directives defined in GraphQL spec. * + * @deprecated use {@see Directive::builtInDirectives()} + * * @throws InvariantViolation * * @return array @@ -188,11 +190,13 @@ public static function promiseToExecute( */ public static function getStandardDirectives(): array { - return Directive::getInternalDirectives(); + return Directive::builtInDirectives(); } /** - * Returns types defined in GraphQL spec. + * Returns built-in scalar types defined in GraphQL spec. + * + * @deprecated use {@see Type::builtInScalars()} * * @throws InvariantViolation * @@ -202,7 +206,7 @@ public static function getStandardDirectives(): array */ public static function getStandardTypes(): array { - return Type::getStandardTypes(); + return Type::builtInScalars(); } /** @@ -210,6 +214,8 @@ public static function getStandardTypes(): array * * Standard types not listed here remain untouched. * + * @deprecated prefer per-schema scalar overrides via {@see \GraphQL\Type\SchemaConfig::$types} or {@see \GraphQL\Type\SchemaConfig::$typeLoader} + * * @param array $types * * @api diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index e613b85d3..80d1ce956 100644 --- a/src/Type/Definition/Directive.php +++ b/src/Type/Definition/Directive.php @@ -76,7 +76,7 @@ public function __construct(array $config) } /** @return array */ - public static function getInternalDirectives(): array + public static function builtInDirectives(): array { return [ self::INCLUDE_NAME => self::includeDirective(), @@ -86,6 +86,16 @@ public static function getInternalDirectives(): array ]; } + /** + * @deprecated use {@see Directive::builtInDirectives()} + * + * @return array + */ + public static function getInternalDirectives(): array + { + return self::builtInDirectives(); + } + public static function includeDirective(): Directive { return self::$internalDirectives[self::INCLUDE_NAME] ??= new self([ @@ -157,9 +167,15 @@ public static function oneOfDirective(): Directive ]); } + public static function isBuiltInDirective(self $directive): bool + { + return array_key_exists($directive->name, self::builtInDirectives()); + } + + /** @deprecated use {@see Directive::isBuiltInDirective()} */ public static function isSpecifiedDirective(Directive $directive): bool { - return array_key_exists($directive->name, self::getInternalDirectives()); + return self::isBuiltInDirective($directive); } public static function resetCachedInstances(): void diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index 8dee72b8d..9fff72751 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -4,10 +4,11 @@ use GraphQL\Error\InvariantViolation; use GraphQL\Type\Introspection; +use GraphQL\Type\SchemaConfig; use GraphQL\Utils\Utils; /** - * Registry of standard GraphQL types and base class for all other types. + * Registry of built-in GraphQL types and base class for all other types. */ abstract class Type implements \JsonSerializable { @@ -17,7 +18,8 @@ abstract class Type implements \JsonSerializable public const BOOLEAN = 'Boolean'; public const ID = 'ID'; - public const STANDARD_TYPE_NAMES = [ + /** @var list */ + public const BUILT_IN_SCALAR_NAMES = [ self::INT, self::FLOAT, self::STRING, @@ -25,65 +27,79 @@ abstract class Type implements \JsonSerializable self::ID, ]; + /** + * @deprecated use {@see Type::BUILT_IN_SCALAR_NAMES} + * + * @var list + */ + public const STANDARD_TYPE_NAMES = self::BUILT_IN_SCALAR_NAMES; + + /** + * Names of all built-in types: built-in scalars and introspection types. + * + * @see Type::BUILT_IN_SCALAR_NAMES for just the built-in scalar names. + * + * @var list + */ public const BUILT_IN_TYPE_NAMES = [ - ...self::STANDARD_TYPE_NAMES, + ...self::BUILT_IN_SCALAR_NAMES, ...Introspection::TYPE_NAMES, ]; /** @var array|null */ - protected static ?array $standardTypes; + protected static ?array $builtInScalars; /** @var array|null */ protected static ?array $builtInTypes; /** - * Returns the registered or default standard Int type. + * Returns the built-in Int scalar type. * * @api */ public static function int(): ScalarType { - return static::$standardTypes[self::INT] ??= new IntType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + return static::$builtInScalars[self::INT] ??= new IntType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } /** - * Returns the registered or default standard Float type. + * Returns the built-in Float scalar type. * * @api */ public static function float(): ScalarType { - return static::$standardTypes[self::FLOAT] ??= new FloatType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + return static::$builtInScalars[self::FLOAT] ??= new FloatType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } /** - * Returns the registered or default standard String type. + * Returns the built-in String scalar type. * * @api */ public static function string(): ScalarType { - return static::$standardTypes[self::STRING] ??= new StringType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + return static::$builtInScalars[self::STRING] ??= new StringType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } /** - * Returns the registered or default standard Boolean type. + * Returns the built-in Boolean scalar type. * * @api */ public static function boolean(): ScalarType { - return static::$standardTypes[self::BOOLEAN] ??= new BooleanType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + return static::$builtInScalars[self::BOOLEAN] ??= new BooleanType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } /** - * Returns the registered or default standard ID type. + * Returns the built-in ID scalar type. * * @api */ public static function id(): ScalarType { - return static::$standardTypes[self::ID] ??= new IDType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + return static::$builtInScalars[self::ID] ??= new IDType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } /** @@ -119,7 +135,9 @@ public static function nonNull($type): NonNull } /** - * Returns all builtin in types including base scalar and introspection types. + * Returns all built-in types: built-in scalars and introspection types. + * + * @api * * @return array */ @@ -127,16 +145,18 @@ public static function builtInTypes(): array { return self::$builtInTypes ??= array_merge( Introspection::getTypes(), - self::getStandardTypes() + self::builtInScalars() ); } /** - * Returns all builtin scalar types. + * Returns all built-in scalar types. + * + * @api * * @return array */ - public static function getStandardTypes(): array + public static function builtInScalars(): array { return [ self::INT => static::int(), @@ -148,7 +168,21 @@ public static function getStandardTypes(): array } /** - * Allows partially or completely overriding the standard types. + * Returns all built-in scalar types. + * + * @deprecated use {@see Type::builtInScalars()} + * + * @return array + */ + public static function getStandardTypes(): array + { + return self::builtInScalars(); + } + + /** + * Allows partially or completely overriding the standard types globally. + * + * @deprecated prefer per-schema scalar overrides via {@see SchemaConfig::$types} or {@see SchemaConfig::$typeLoader} * * @param array $types * @@ -156,7 +190,7 @@ public static function getStandardTypes(): array */ public static function overrideStandardTypes(array $types): void { - // Reset caches that might contain instances of standard types + // Reset caches that might contain instances of built-in scalars static::$builtInTypes = null; Introspection::resetCachedInstances(); Directive::resetCachedInstances(); @@ -169,13 +203,13 @@ public static function overrideStandardTypes(array $types): void throw new InvariantViolation("Expecting instance of {$typeClass}, got {$notType}"); } - if (! in_array($type->name, self::STANDARD_TYPE_NAMES, true)) { - $standardTypeNames = implode(', ', self::STANDARD_TYPE_NAMES); + if (! in_array($type->name, self::BUILT_IN_SCALAR_NAMES, true)) { + $standardTypeNames = implode(', ', self::BUILT_IN_SCALAR_NAMES); $notStandardTypeName = Utils::printSafe($type->name); throw new InvariantViolation("Expecting one of the following names for a standard type: {$standardTypeNames}; got {$notStandardTypeName}"); } - static::$standardTypes[$type->name] = $type; + static::$builtInScalars[$type->name] = $type; } } diff --git a/src/Type/Schema.php b/src/Type/Schema.php index 8ded0b770..8708552b4 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -131,7 +131,7 @@ public function getTypeMap(): array // with Type::string() etc. references in field definitions during extractTypes. /** @var array $scalarOverrides */ $scalarOverrides = []; - $standardTypes = Type::getStandardTypes(); + $builtInScalars = Type::builtInScalars(); foreach ($types as $typeOrLazyType) { /** @var Type|callable(): Type $typeOrLazyType */ @@ -142,8 +142,8 @@ public function getTypeMap(): array $typeName = $type->name; if ($type instanceof ScalarType - && isset($standardTypes[$typeName]) - && $type !== $standardTypes[$typeName] + && isset($builtInScalars[$typeName]) + && $type !== $builtInScalars[$typeName] ) { $scalarOverrides[$typeName] = $type; continue; @@ -189,13 +189,16 @@ public function getTypeMap(): array } if (isset($this->config->typeLoader)) { - foreach (Type::STANDARD_TYPE_NAMES as $scalarName) { + foreach (Type::BUILT_IN_SCALAR_NAMES as $scalarName) { if (isset($scalarOverrides[$scalarName])) { continue; } $type = ($this->config->typeLoader)($scalarName); - if ($type instanceof ScalarType && $type->name === $scalarName && $type !== $standardTypes[$scalarName]) { + if ($type instanceof ScalarType + && $type->name === $scalarName + && $type !== $builtInScalars[$scalarName] + ) { $allReferencedTypes[$scalarName] = $type; } } @@ -338,9 +341,9 @@ public function getType(string $name): ?Type return $this->resolvedTypes[$name] = self::resolveType($type); } - $standardTypes = Type::getStandardTypes(); - if (isset($standardTypes[$name])) { - return $this->resolvedTypes[$name] = $standardTypes[$name]; + $builtInScalars = Type::builtInScalars(); + if (isset($builtInScalars[$name])) { + return $this->resolvedTypes[$name] = $builtInScalars[$name]; } return null; @@ -546,7 +549,7 @@ public function assertValid(): void throw new InvariantViolation(implode("\n\n", $this->validationErrors)); } - $internalTypes = Type::getStandardTypes() + Introspection::getTypes(); + $internalTypes = Type::builtInScalars() + Introspection::getTypes(); foreach ($this->getTypeMap() as $name => $type) { if (isset($internalTypes[$name])) { continue; diff --git a/src/Utils/BuildClientSchema.php b/src/Utils/BuildClientSchema.php index 7cd55e78b..40845936a 100644 --- a/src/Utils/BuildClientSchema.php +++ b/src/Utils/BuildClientSchema.php @@ -111,7 +111,7 @@ public function buildSchema(): Schema $schemaIntrospection = $this->introspection['__schema']; $builtInTypes = array_merge( - Type::getStandardTypes(), + Type::builtInScalars(), Introspection::getTypes() ); diff --git a/tests/Type/ScalarOverridesTest.php b/tests/Type/ScalarOverridesTest.php index ebc224e53..d93f48000 100644 --- a/tests/Type/ScalarOverridesTest.php +++ b/tests/Type/ScalarOverridesTest.php @@ -23,7 +23,7 @@ final class ScalarOverridesTest extends TestCase public static function setUpBeforeClass(): void { - self::$originalStandardTypes = Type::getStandardTypes(); + self::$originalStandardTypes = Type::builtInScalars(); } public function tearDown(): void From d6ea872e122d3c0e2d0d4c2a24dee6a75dcaf5bf Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sat, 14 Mar 2026 19:05:39 +0100 Subject: [PATCH 4/6] Improve benchmark consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lower retry_threshold from 5 to 2, increase iterations for BuildSchemaBench (2→5) and HugeSchemaBench (3→5), and add 500ms sleep between iterations for those two to allow CPU frequency/cache settling. This cut per-benchmark rstdev from ~1.5–3.8% down to ~0.6–1.3%, eliminating false signals like the spurious +9% regression on benchManyNestedDeferreds that appeared in branch comparisons. --- benchmarks/BuildSchemaBench.php | 4 +++- benchmarks/HugeSchemaBench.php | 4 +++- phpbench.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/benchmarks/BuildSchemaBench.php b/benchmarks/BuildSchemaBench.php index 46069373a..856cca298 100644 --- a/benchmarks/BuildSchemaBench.php +++ b/benchmarks/BuildSchemaBench.php @@ -11,9 +11,11 @@ * * @Warmup(2) * + * @Sleep(500000) + * * @Revs(10) * - * @Iterations(2) + * @Iterations(5) */ class BuildSchemaBench { diff --git a/benchmarks/HugeSchemaBench.php b/benchmarks/HugeSchemaBench.php index 1f58835a5..dfb3610cb 100644 --- a/benchmarks/HugeSchemaBench.php +++ b/benchmarks/HugeSchemaBench.php @@ -16,9 +16,11 @@ * * @Warmup(2) * + * @Sleep(500000) + * * @Revs(10) * - * @Iterations(3) + * @Iterations(5) */ class HugeSchemaBench { diff --git a/phpbench.json b/phpbench.json index b47419350..de5b5c6f5 100644 --- a/phpbench.json +++ b/phpbench.json @@ -2,7 +2,7 @@ "runner.bootstrap": "vendor/autoload.php", "runner.path": "benchmarks", "runner.file_pattern": "*Bench.php", - "runner.retry_threshold": 5, + "runner.retry_threshold": 2, "runner.time_unit": "milliseconds", "runner.progress": "plain", "report.generators": { From 627b5ce2928ef76adb72bfbea75e0e36232fbf98 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sat, 14 Mar 2026 19:42:29 +0100 Subject: [PATCH 5/6] changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2509467e7..3f60ba656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,20 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +### Added + +- Support per-schema scalar overrides via `types` config or `typeLoader`, without global side effects https://github.com/webonyx/graphql-php/pull/1869 +- Add `Type::builtInScalars()` and `Type::BUILT_IN_SCALAR_NAMES` aligning with GraphQL spec terminology https://github.com/webonyx/graphql-php/pull/1869 +- Add `Directive::builtInDirectives()` and `Directive::isBuiltInDirective()` aligning with GraphQL spec terminology https://github.com/webonyx/graphql-php/pull/1869 + +### Deprecated + +- Deprecate `Type::overrideStandardTypes()` in favor of per-schema scalar overrides https://github.com/webonyx/graphql-php/pull/1869 +- Deprecate `Type::getStandardTypes()` in favor of `Type::builtInScalars()` https://github.com/webonyx/graphql-php/pull/1869 +- Deprecate `Type::STANDARD_TYPE_NAMES` in favor of `Type::BUILT_IN_SCALAR_NAMES` https://github.com/webonyx/graphql-php/pull/1869 +- Deprecate `Directive::getInternalDirectives()` in favor of `Directive::builtInDirectives()` https://github.com/webonyx/graphql-php/pull/1869 +- Deprecate `Directive::isSpecifiedDirective()` in favor of `Directive::isBuiltInDirective()` https://github.com/webonyx/graphql-php/pull/1869 + ## v15.30.2 ### Fixed From a71ad691494f13883021c1ffd6b7dcf59919d11b Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sat, 14 Mar 2026 19:42:52 +0100 Subject: [PATCH 6/6] v15.31.0 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f60ba656..b5746dc9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +## v15.31.0 + ### Added - Support per-schema scalar overrides via `types` config or `typeLoader`, without global side effects https://github.com/webonyx/graphql-php/pull/1869