From 2426c01966457db1e8dd4501fbfd469b5335820b Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sat, 14 Mar 2026 21:30:17 +0100 Subject: [PATCH 1/3] Add \`types\` parameter to BuildSchema for injecting pre-built type instances from SDL Enables users to supply custom type instances (e.g., scalars with serialize/parseValue, enums with PHP values) when building a schema from SDL, without having to use the more awkward typeConfigDecorator callback pattern. Types whose names match SDL definitions replace the SDL-built instances. Types whose names are absent from the SDL are registered as extra types. Closes https://github.com/webonyx/graphql-php/issues/681 --- CHANGELOG.md | 4 ++ src/Utils/ASTDefinitionBuilder.php | 12 +++++- src/Utils/BuildSchema.php | 48 +++++++++++++++++----- tests/Utils/BuildSchemaTest.php | 66 ++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5746dc9d..8778ad1e2 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 +### Added + +- Allow injecting pre-built type instances when building a schema from SDL https://github.com/webonyx/graphql-php/issues/681 + ## v15.31.0 ### Added diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index 07764bb9d..d5660879d 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -86,9 +86,13 @@ class ASTDefinitionBuilder /** @var array> */ private array $typeExtensionsMap; + /** @var array */ + private array $typeOverrides; + /** * @param array $typeDefinitionsMap * @param array> $typeExtensionsMap + * @param array $typeOverrides * * @phpstan-param ResolveType $resolveType * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator @@ -100,13 +104,15 @@ public function __construct( array $typeExtensionsMap, callable $resolveType, ?callable $typeConfigDecorator = null, - ?callable $fieldConfigDecorator = null + ?callable $fieldConfigDecorator = null, + array $typeOverrides = [] ) { $this->typeDefinitionsMap = $typeDefinitionsMap; $this->typeExtensionsMap = $typeExtensionsMap; $this->resolveType = $resolveType; $this->typeConfigDecorator = $typeConfigDecorator; $this->fieldConfigDecorator = $fieldConfigDecorator; + $this->typeOverrides = $typeOverrides; $this->cache = Type::builtInTypes(); } @@ -263,6 +269,10 @@ private function internalBuildType(string $typeName, ?Node $typeNode = null): Ty return $this->cache[$typeName]; } + if (isset($this->typeOverrides[$typeName])) { + return $this->cache[$typeName] = $this->typeOverrides[$typeName]; + } + if (isset($this->typeDefinitionsMap[$typeName])) { $type = $this->makeSchemaDef($this->typeDefinitionsMap[$typeName]); diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index 4fdade26e..cbf7531f3 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -14,6 +14,7 @@ use GraphQL\Language\Parser; use GraphQL\Language\Source; use GraphQL\Type\Definition\Directive; +use GraphQL\Type\Definition\NamedType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; use GraphQL\Type\SchemaConfig; @@ -68,8 +69,12 @@ class BuildSchema */ private array $options; + /** @var iterable */ + private iterable $types; + /** * @param array $options + * @param iterable $types * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator * @phpstan-param BuildSchemaOptions $options @@ -78,12 +83,14 @@ public function __construct( DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = [], - ?callable $fieldConfigDecorator = null + ?callable $fieldConfigDecorator = null, + iterable $types = [] ) { $this->ast = $ast; $this->typeConfigDecorator = $typeConfigDecorator; $this->options = $options; $this->fieldConfigDecorator = $fieldConfigDecorator; + $this->types = $types; } /** @@ -92,6 +99,7 @@ public function __construct( * * @param DocumentNode|Source|string $source * @param array $options + * @param iterable $types * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator @@ -109,13 +117,14 @@ public static function build( $source, ?callable $typeConfigDecorator = null, array $options = [], - ?callable $fieldConfigDecorator = null + ?callable $fieldConfigDecorator = null, + iterable $types = [] ): Schema { $doc = $source instanceof DocumentNode ? $source : Parser::parse($source); - return self::buildAST($doc, $typeConfigDecorator, $options, $fieldConfigDecorator); + return self::buildAST($doc, $typeConfigDecorator, $options, $fieldConfigDecorator, $types); } /** @@ -127,6 +136,7 @@ public static function build( * has no resolve methods, so execution will use default resolvers. * * @param array $options + * @param iterable $types * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator @@ -143,9 +153,10 @@ public static function buildAST( DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = [], - ?callable $fieldConfigDecorator = null + ?callable $fieldConfigDecorator = null, + iterable $types = [] ): Schema { - return (new self($ast, $typeConfigDecorator, $options, $fieldConfigDecorator))->buildSchema(); + return (new self($ast, $typeConfigDecorator, $options, $fieldConfigDecorator, $types))->buildSchema(); } /** @@ -201,6 +212,18 @@ public function buildSchema(): Schema 'subscription' => 'Subscription', ]; + /** @var array $typeOverrides */ + $typeOverrides = []; + /** @var array $extraTypesMap */ + $extraTypesMap = []; + foreach ($this->types as $type) { + if (isset($typeDefinitionsMap[$type->name])) { + $typeOverrides[$type->name] = $type; + } else { + $extraTypesMap[$type->name] = $type; + } + } + $definitionBuilder = new ASTDefinitionBuilder( $typeDefinitionsMap, $typeExtensionsMap, @@ -209,7 +232,8 @@ static function (string $typeName): Type { throw self::unknownType($typeName); }, $this->typeConfigDecorator, - $this->fieldConfigDecorator + $this->fieldConfigDecorator, + $typeOverrides ); $directives = array_map( @@ -254,12 +278,16 @@ static function (string $typeName): Type { ->setSubscription(isset($operationTypes['subscription']) ? $definitionBuilder->maybeBuildType($operationTypes['subscription']) : null) - ->setTypeLoader(static fn (string $name): ?Type => $definitionBuilder->maybeBuildType($name)) + ->setTypeLoader(static fn (string $name): ?Type => $definitionBuilder->maybeBuildType($name) + ?? ($extraTypesMap[$name] ?? null)) ->setDirectives($directives) ->setAstNode($schemaDef) - ->setTypes(fn (): array => array_map( - static fn (TypeDefinitionNode $def): Type => $definitionBuilder->buildType($def->getName()->value), - $typeDefinitionsMap, + ->setTypes(fn (): array => array_merge( + array_map( + static fn (TypeDefinitionNode $def): Type => $definitionBuilder->buildType($def->getName()->value), + $typeDefinitionsMap, + ), + array_values($extraTypesMap), )) ); } diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index 877fdb3ff..9073e2b95 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -24,6 +24,7 @@ use GraphQL\Language\Parser; use GraphQL\Language\Printer; use GraphQL\Tests\TestCaseBase; +use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumValueDefinition; @@ -1452,6 +1453,71 @@ interface Hello { self::assertSame('My description of Hello', $hello->description); } + public function testBuildSchemaWithTypeOverrides(): void + { + $sdl = ' + schema { + query: Query + } + + type Query { + value: MyScalar + status: Status + } + + scalar MyScalar + + enum Status { + ACTIVE + INACTIVE + } + '; + + $myScalar = new CustomScalarType([ + 'name' => 'MyScalar', + 'serialize' => static fn ($value) => 'serialized:' . $value, + 'parseValue' => static fn ($value) => 'parsed:' . $value, + ]); + + $myEnum = new EnumType([ + 'name' => 'Status', + 'values' => [ + 'ACTIVE' => [ + 'value' => 1, + ], + 'INACTIVE' => [ + 'value' => 0, + ], + ], + ]); + + $extraType = new ObjectType([ + 'name' => 'ExtraType', + 'fields' => [ + 'id' => \GraphQL\Type\Definition\Type::string(), + ], + ]); + + $schema = BuildSchema::build($sdl, null, [], null, [$myScalar, $myEnum, $extraType]); + + $scalar = $schema->getType('MyScalar'); + self::assertSame($myScalar, $scalar); + + $enum = $schema->getType('Status'); + self::assertSame($myEnum, $enum); + + $extra = $schema->getType('ExtraType'); + self::assertSame($extraType, $extra); + + // Verify the custom scalar serialize/parseValue are actually used + $result = GraphQL::executeQuery( + $schema, + '{ value }', + ['value' => 'hello'] + ); + self::assertSame(['value' => 'serialized:hello'], $result->data); + } + public function testCreatesTypesLazily(): void { $sdl = ' From 4beacfcce813145aa5c6477d075ca70ea5ee9319 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 20:34:59 +0000 Subject: [PATCH 2/3] Autofix --- docs/class-reference.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/class-reference.md b/docs/class-reference.md index f6e627c43..5eb7da3e7 100644 --- a/docs/class-reference.md +++ b/docs/class-reference.md @@ -2774,6 +2774,7 @@ See [schema definition language docs](schema-definition-language.md) for details * * @param DocumentNode|Source|string $source * @param array $options + * @param iterable $types * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator @@ -2791,7 +2792,8 @@ static function build( $source, ?callable $typeConfigDecorator = null, array $options = [], - ?callable $fieldConfigDecorator = null + ?callable $fieldConfigDecorator = null, + iterable $types = [] ): GraphQL\Type\Schema ``` @@ -2805,6 +2807,7 @@ static function build( * has no resolve methods, so execution will use default resolvers. * * @param array $options + * @param iterable $types * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator @@ -2821,7 +2824,8 @@ static function buildAST( GraphQL\Language\AST\DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = [], - ?callable $fieldConfigDecorator = null + ?callable $fieldConfigDecorator = null, + iterable $types = [] ): GraphQL\Type\Schema ``` From 2efe4b193316027bf8c27f85b3f4c76c14e897ea Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sat, 14 Mar 2026 22:05:45 +0100 Subject: [PATCH 3/3] Document types parameter in schema-definition-language.md --- docs/schema-definition-language.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/schema-definition-language.md b/docs/schema-definition-language.md index b0caa4efc..619c3fe8b 100644 --- a/docs/schema-definition-language.md +++ b/docs/schema-definition-language.md @@ -55,6 +55,30 @@ $schema = BuildSchema::build($contents, $typeConfigDecorator); You can learn more about using `$typeConfigDecorator` in [examples/05-type-config-decorator](https://github.com/webonyx/graphql-php/blob/master/examples/05-type-config-decorator). +## Custom scalar and enum types + +When building a schema from SDL, scalar types are stubs — they serialize and parse values as-is. +To attach real behavior (validation, coercion, PHP-backed enum values), pass pre-built type instances via the `types` parameter: + +```php +use GraphQL\Type\Definition\CustomScalarType; +use GraphQL\Utils\BuildSchema; + +$dateType = new CustomScalarType([ + 'name' => 'Date', + 'serialize' => static fn ($value) => $value->format('Y-m-d'), + 'parseValue' => static fn ($value) => new DateTimeImmutable($value), +]); + +$schema = BuildSchema::build( + file_get_contents('schema.graphql'), + types: [$dateType], +); +``` + +Types whose names match SDL definitions replace the SDL-built stubs. +Types whose names are absent from the SDL are registered as extras and remain reachable via `$schema->getType()`. + ## Performance considerations Method **BuildSchema::build()** produces a [lazy schema](schema-definition.md#lazy-loading-of-types) automatically,