Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.3

### Fixed
Expand Down
8 changes: 6 additions & 2 deletions docs/class-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2790,6 +2790,7 @@ See [schema definition language docs](schema-definition-language.md) for details
*
* @param DocumentNode|Source|string $source
* @param array<string, bool> $options
* @param iterable<Type&NamedType> $types
*
* @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
* @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator
Expand All @@ -2807,7 +2808,8 @@ static function build(
$source,
?callable $typeConfigDecorator = null,
array $options = [],
?callable $fieldConfigDecorator = null
?callable $fieldConfigDecorator = null,
iterable $types = []
): GraphQL\Type\Schema
```

Expand All @@ -2821,6 +2823,7 @@ static function build(
* has no resolve methods, so execution will use default resolvers.
*
* @param array<string, bool> $options
* @param iterable<Type&NamedType> $types
*
* @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
* @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator
Expand All @@ -2837,7 +2840,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
```

Expand Down
24 changes: 24 additions & 0 deletions docs/schema-definition-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 11 additions & 1 deletion src/Utils/ASTDefinitionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,13 @@ class ASTDefinitionBuilder
/** @var array<string, array<int, Node&TypeExtensionNode>> */
private array $typeExtensionsMap;

/** @var array<string, Type&NamedType> */
private array $typeOverrides;

/**
* @param array<string, Node&TypeDefinitionNode> $typeDefinitionsMap
* @param array<string, array<int, Node&TypeExtensionNode>> $typeExtensionsMap
* @param array<string, Type&NamedType> $typeOverrides
*
* @phpstan-param ResolveType $resolveType
* @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
Expand All @@ -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();
}
Expand Down Expand Up @@ -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]);

Expand Down
48 changes: 38 additions & 10 deletions src/Utils/BuildSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -68,8 +69,12 @@ class BuildSchema
*/
private array $options;

/** @var iterable<Type&NamedType> */
private iterable $types;

/**
* @param array<string, bool> $options
* @param iterable<Type&NamedType> $types
*
* @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
* @phpstan-param BuildSchemaOptions $options
Expand All @@ -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;
}

/**
Expand All @@ -92,6 +99,7 @@ public function __construct(
*
* @param DocumentNode|Source|string $source
* @param array<string, bool> $options
* @param iterable<Type&NamedType> $types
*
* @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
* @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator
Expand All @@ -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);
}

/**
Expand All @@ -127,6 +136,7 @@ public static function build(
* has no resolve methods, so execution will use default resolvers.
*
* @param array<string, bool> $options
* @param iterable<Type&NamedType> $types
*
* @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
* @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator
Expand All @@ -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();
}

/**
Expand Down Expand Up @@ -201,14 +212,27 @@ public function buildSchema(): Schema
'subscription' => 'Subscription',
];

/** @var array<string, Type&NamedType> $typeOverrides */
$typeOverrides = [];
/** @var array<string, Type&NamedType> $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,
static function (string $typeName): Type {
throw self::unknownType($typeName);
},
$this->typeConfigDecorator,
$this->fieldConfigDecorator
$this->fieldConfigDecorator,
$typeOverrides
);

$directives = array_map(
Expand Down Expand Up @@ -253,12 +277,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),
))
);
}
Expand Down
66 changes: 66 additions & 0 deletions tests/Utils/BuildSchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = '
Expand Down
Loading