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.37.2

### Changed
Expand Down
8 changes: 6 additions & 2 deletions docs/class-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2832,6 +2832,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 @@ -2849,7 +2850,8 @@ static function build(
$source,
?callable $typeConfigDecorator = null,
array $options = [],
?callable $fieldConfigDecorator = null
?callable $fieldConfigDecorator = null,
iterable $types = []
): GraphQL\Type\Schema
```

Expand All @@ -2863,6 +2865,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 @@ -2879,7 +2882,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
28 changes: 28 additions & 0 deletions docs/schema-definition-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,34 @@ $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'),
null,
[],
null,
[$dateType],
);
Comment thread
spawnia marked this conversation as resolved.
```

Types whose names match SDL definitions replace them entirely, including their extensions.
This works for any kind of type, so SDL can reference code-first types by declaring a stub such as `type User`.
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 @@ -87,9 +87,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 @@ -101,13 +105,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 @@ -264,6 +270,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];
}
Comment thread
spawnia marked this conversation as resolved.

if (isset($this->typeDefinitionsMap[$typeName])) {
$type = $this->makeSchemaDef($this->typeDefinitionsMap[$typeName]);

Expand Down
57 changes: 47 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,36 @@ 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) {
// @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue (unnecessary according to types, but can happen during runtime)
assert($type instanceof NamedType, 'Types passed to BuildSchema must be named types, got: ' . Utils::printSafe($type) . '.');
$typeName = $type->name;
$knownType = $typeOverrides[$typeName] ?? $extraTypesMap[$typeName] ?? $type;
assert(
$knownType === $type,
"Schema must contain unique named types but contains multiple types named \"{$typeName}\" (see https://webonyx.github.io/graphql-php/type-definitions/#type-registry).",
);

if (isset($typeDefinitionsMap[$typeName])) {
$typeOverrides[$typeName] = $type;
} else {
$extraTypesMap[$typeName] = $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 @@ -256,12 +289,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
Loading
Loading