Skip to content
Merged
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

### Changed

- Exclusively support `types` for per-schema scalar overrides, not `typeLoader` https://github.com/webonyx/graphql-php/pull/1884

## v15.31.1

### Fixed
Expand Down
27 changes: 0 additions & 27 deletions benchmarks/ScalarOverrideBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ class ScalarOverrideBench
{
private Schema $schemaBaseline;

private Schema $schemaTypeLoader;

private Schema $schemaTypes;

public function setUp(): void
Expand All @@ -47,21 +45,6 @@ public function setUp(): void
'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' => [
Expand All @@ -82,11 +65,6 @@ public function benchGetTypeWithoutOverride(): void
$this->schemaBaseline->getType('String');
}

public function benchGetTypeWithTypeLoaderOverride(): void
{
$this->schemaTypeLoader->getType('String');
}

public function benchGetTypeWithTypesOverride(): void
{
$this->schemaTypes->getType('String');
Expand All @@ -97,11 +75,6 @@ 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 }');
Expand Down
16 changes: 8 additions & 8 deletions docs/schema-definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ with complex input values (see [Mutations and Input Types](type-definitions/inpu
The schema constructor expects an instance of [`GraphQL\Type\SchemaConfig`](class-reference.md#graphqltypeschemaconfig)
or an array with the following options:

| Option | Type | Notes |
| ------------ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| query | `ObjectType` or `callable(): ?ObjectType` or `null` | **Required.** Object type (usually named `Query`) containing root-level fields of your read API |
| mutation | `ObjectType` or `callable(): ?ObjectType` or `null` | Object type (usually named `Mutation`) containing root-level fields of your write API |
| subscription | `ObjectType` or `callable(): ?ObjectType` or `null` | Reserved for future subscriptions implementation. Currently presented for compatibility with introspection query of **graphql-js**, used by various clients (like Relay or GraphiQL) |
| directives | `array<Directive>` | A full list of [directives](type-definitions/directives.md) supported by your schema. By default, contains built-in **@skip** and **@include** directives.<br><br> If you pass your own directives and still want to use built-in directives - add them explicitly. For example:<br><br> _array_merge(GraphQL::getStandardDirectives(), [$myCustomDirective]);_ |
| types | `array<ObjectType>` | List of object types which cannot be detected by **graphql-php** during static schema analysis.<br><br>Most often this happens when the object type is never referenced in fields directly but is still a part of a schema because it implements an interface which resolves to this object type in its **resolveType** callable. <br><br> Note that you are not required to pass all of your types here - it is simply a workaround for a concrete use-case. |
| typeLoader | `callable(string $name): Type` | Expected to return a type instance given the name. Must always return the same instance if called multiple times, see [lazy loading](#lazy-loading-of-types). See section below on lazy type loading. |
| Option | Type | Notes |
| ------------ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| query | `ObjectType` or `callable(): ?ObjectType` or `null` | **Required.** Object type (usually named `Query`) containing root-level fields of your read API |
| mutation | `ObjectType` or `callable(): ?ObjectType` or `null` | Object type (usually named `Mutation`) containing root-level fields of your write API |
| subscription | `ObjectType` or `callable(): ?ObjectType` or `null` | Reserved for future subscriptions implementation. Currently presented for compatibility with introspection query of **graphql-js**, used by various clients (like Relay or GraphiQL) |
| directives | `array<Directive>` | A full list of [directives](type-definitions/directives.md) supported by your schema. By default, contains built-in **@skip** and **@include** directives.<br><br> If you pass your own directives and still want to use built-in directives - add them explicitly. For example:<br><br> _array_merge(GraphQL::getStandardDirectives(), [$myCustomDirective]);_ |
| types | `iterable<Type&NamedType>` | Additional types to register in the schema.<br><br>Use this for object types that are not directly referenced in fields but implement an interface that resolves to them via **resolveType**.<br><br>Can also contain custom scalar types named like built-in scalars (`String`, `Int`, etc.) to [override them](type-definitions/scalars.md#overriding-built-in-scalars) on a per-schema basis. |
| typeLoader | `callable(string $name): Type` | Expected to return a type instance given the name. Must always return the same instance if called multiple times, see [lazy loading](#lazy-loading-of-types). See section below on lazy type loading. |

### Using config class

Expand Down
27 changes: 27 additions & 0 deletions docs/type-definitions/scalars.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,30 @@ $emailType = new CustomScalarType([

Keep in mind the passed functions will be called statically, so a passed in `callable`
such as `[Foo::class, 'bar']` should only reference static class methods.

## Overriding Built-in Scalars

You can override built-in scalars (`String`, `Int`, `Float`, `Boolean`, `ID`) on a per-schema basis by passing a custom scalar with the same name through the `types` option.
This works with or without a `typeLoader`:

```php
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;

$uppercaseString = new CustomScalarType([
'name' => 'String',
'serialize' => static fn ($value): string => strtoupper((string) $value),
]);

$schema = new Schema([
'query' => $queryType,
'typeLoader' => $myTypeLoader,
'types' => [$uppercaseString],
]);
```

The custom scalar replaces the built-in one throughout the entire schema, affecting both serialization of results and coercion of inputs.

> **Note:** The `typeLoader` is never called for built-in scalar names.
> Always use `types` to override them.
8 changes: 7 additions & 1 deletion src/Type/Definition/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ public static function overrideStandardTypes(array $types): void
throw new InvariantViolation("Expecting instance of {$typeClass}, got {$notType}");
}

if (! in_array($type->name, self::BUILT_IN_SCALAR_NAMES, true)) {
if (! self::isBuiltInScalarName($type->name)) {
$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}");
Expand Down Expand Up @@ -231,6 +231,12 @@ public static function isBuiltInScalar($type): bool
&& in_array($type->name, self::BUILT_IN_SCALAR_NAMES, true);
}

/** Checks if the given name is one of the built-in scalar type names (ID, String, Int, Float, Boolean). */
public static function isBuiltInScalarName(string $name): bool
{
return in_array($name, self::BUILT_IN_SCALAR_NAMES, true);
}

/**
* Determines if the given type is an input type.
*
Expand Down
27 changes: 9 additions & 18 deletions src/Type/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,22 +188,6 @@ public function getTypeMap(): array
$allReferencedTypes[$name] = $override;
}

if (isset($this->config->typeLoader)) {
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 !== $builtInScalars[$scalarName]
) {
$allReferencedTypes[$scalarName] = $type;
}
}
}

$this->resolvedTypes = $allReferencedTypes;
$this->fullyLoaded = true;
}
Expand Down Expand Up @@ -362,11 +346,18 @@ public function hasType(string $name): bool
*/
private function loadType(string $typeName): ?Type
{
if (! isset($this->config->typeLoader)) {
$typeLoader = $this->config->typeLoader;

if (! isset($typeLoader)) {
return $this->getTypeMap()[$typeName] ?? null;
}

$type = ($this->config->typeLoader)($typeName);
// TODO https://github.com/webonyx/graphql-php/issues/1874 - reconsider supporting typeLoader-based scalar overrides in the next major version
if (Type::isBuiltInScalarName($typeName)) {
return null;
}

$type = $typeLoader($typeName);
if ($type === null) {
return null;
}
Expand Down
3 changes: 0 additions & 3 deletions tests/Executor/ExecutorLazySchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ public function testSimpleQuery(): void
'Query.fields',
'SomeObject',
'SomeObject.fields',
'String',
];
self::assertSame($expected, $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE));
self::assertSame($expectedExecutorCalls, $this->calls);
Expand Down Expand Up @@ -380,7 +379,6 @@ public function testDeepQuery(): void
'Query' => true,
'SomeObject' => true,
'OtherObject' => true,
'String' => true,
],
$this->loadedTypes
);
Expand All @@ -389,7 +387,6 @@ public function testDeepQuery(): void
'Query.fields',
'SomeObject',
'SomeObject.fields',
'String',
],
$this->calls
);
Expand Down
Loading
Loading