Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src/Executor/Values.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public static function getVariableValues(Schema $schema, NodeList $varDefNodes,
} else {
// Otherwise, a non-null value was provided, coerce it to the expected
// type or report an error if coercion fails.
$coerced = Value::coerceInputValue($value, $varType);
$coerced = Value::coerceInputValue($value, $varType, null, $schema);

$coercionErrors = $coerced['errors'];
if ($coercionErrors !== null) {
Expand Down
17 changes: 17 additions & 0 deletions src/Utils/TypeComparators.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use GraphQL\Error\InvariantViolation;
use GraphQL\Type\Definition\ImplementingType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
Expand All @@ -19,6 +20,14 @@ public static function isEqualType(Type $typeA, Type $typeB): bool
return true;
}

// Named types with the same name are equal, even if they are different
// instances (e.g. a type loader override vs the built-in singleton).
if ($typeA instanceof NamedType && $typeB instanceof NamedType
&& $typeA->name() === $typeB->name()
) {
return true;
}
Comment on lines +23 to +29
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should only apply to scalar types.


// If either type is non-null, the other must also be non-null.
if ($typeA instanceof NonNull && $typeB instanceof NonNull) {
return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
Expand Down Expand Up @@ -46,6 +55,14 @@ public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type
return true;
}

// Named types with the same name are equivalent, even if they are different
// instances (e.g. a type loader override vs the built-in singleton).
if ($maybeSubType instanceof NamedType && $superType instanceof NamedType
&& $maybeSubType->name() === $superType->name()
) {
return true;
}

// If superType is non-null, maybeSubType must also be nullable.
if ($superType instanceof NonNull) {
if ($maybeSubType instanceof NonNull) {
Expand Down
21 changes: 17 additions & 4 deletions src/Utils/Value.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;

/**
* @phpstan-type CoercedValue array{errors: null, value: mixed}
Expand All @@ -37,7 +38,7 @@ class Value
*
* @phpstan-return CoercedValue|CoercedErrors
*/
public static function coerceInputValue($value, InputType $type, ?array $path = null): array
public static function coerceInputValue($value, InputType $type, ?array $path = null, ?Schema $schema = null): array
{
if ($type instanceof NonNull) {
if ($value === null) {
Expand All @@ -47,7 +48,7 @@ public static function coerceInputValue($value, InputType $type, ?array $path =
}

// @phpstan-ignore-next-line wrapped type is known to be input type after schema validation
return self::coerceInputValue($value, $type->getWrappedType(), $path);
return self::coerceInputValue($value, $type->getWrappedType(), $path, $schema);
}

if ($value === null) {
Expand All @@ -56,6 +57,16 @@ public static function coerceInputValue($value, InputType $type, ?array $path =
}

if ($type instanceof ScalarType || $type instanceof EnumType) {
// Account for type loader returning a different instance than the
// built-in singleton used in field definitions. Resolve the actual
// type from the schema to ensure the correct parseValue() is called.
if ($schema !== null && $type instanceof ScalarType) {
$schemaType = $schema->getType($type->name);
if ($schemaType instanceof ScalarType) {
$type = $schemaType;
}
}

// Scalars and Enums determine if a input value is valid via parseValue(), which can
// throw to indicate failure. If it throws, maintain a reference to
// the original error.
Expand Down Expand Up @@ -88,7 +99,8 @@ public static function coerceInputValue($value, InputType $type, ?array $path =
$coercedItem = self::coerceInputValue(
$itemValue,
$itemType,
[...$path ?? [], $index]
[...$path ?? [], $index],
$schema,
);

if (isset($coercedItem['errors'])) {
Expand All @@ -104,7 +116,7 @@ public static function coerceInputValue($value, InputType $type, ?array $path =
}

// Lists accept a non-list value as a list of one.
$coercedItem = self::coerceInputValue($value, $itemType);
$coercedItem = self::coerceInputValue($value, $itemType, null, $schema);

return isset($coercedItem['errors'])
? $coercedItem
Expand Down Expand Up @@ -133,6 +145,7 @@ public static function coerceInputValue($value, InputType $type, ?array $path =
$fieldValue,
$field->getType(),
[...$path ?? [], $fieldName],
$schema,
);

if (isset($coercedField['errors'])) {
Expand Down
137 changes: 137 additions & 0 deletions tests/Type/ScalarOverridesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use GraphQL\Error\InvariantViolation;
use GraphQL\GraphQL;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
Expand Down Expand Up @@ -204,12 +206,147 @@ public function testNonOverriddenScalarsAreUnaffected(): void
self::assertSame('abc-123', $data['identifier']);
}

public function testTypeLoaderOverrideWithVariableOfOverriddenBuiltInScalarType(): void
{
$customID = self::createCustomID(static fn ($value): string => (string) $value);

$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'node' => [
'type' => Type::string(),
'args' => [
'id' => Type::nonNull(Type::id()),
],
'resolve' => static fn ($root, array $args): string => 'node-' . $args['id'],
],
],
]);

$types = ['Query' => $queryType, 'ID' => $customID];

$schema = new Schema([
'query' => $queryType,
'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null,
]);

$schema->assertValid();

$result = GraphQL::executeQuery($schema, 'query ($id: ID!) { node(id: $id) }', null, null, ['id' => 'abc-123']);

self::assertEmpty($result->errors, isset($result->errors[0]) ? $result->errors[0]->getMessage() : '');
self::assertSame(['data' => ['node' => 'node-abc-123']], $result->toArray());
}

public function testTypeLoaderOverrideWithNullableVariableOfOverriddenBuiltInScalarType(): void
{
$customString = self::createUppercaseString();

$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'echo' => [
'type' => Type::string(),
'args' => [
'text' => Type::string(),
],
'resolve' => static fn ($root, array $args): ?string => $args['text'] ?? null,
],
],
]);

$types = ['Query' => $queryType, 'String' => $customString];

$schema = new Schema([
'query' => $queryType,
'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null,
]);

$schema->assertValid();

$result = GraphQL::executeQuery($schema, 'query ($text: String) { echo(text: $text) }', null, null, ['text' => 'hello']);

self::assertEmpty($result->errors, isset($result->errors[0]) ? $result->errors[0]->getMessage() : '');
self::assertSame(['data' => ['echo' => 'HELLO']], $result->toArray());
}

public function testTypeLoaderOverrideWithInputObjectFieldOfOverriddenBuiltInScalarType(): void
{
$customID = self::createCustomID(static fn ($value): string => 'custom-' . $value);

$inputType = new InputObjectType([
'name' => 'NodeInput',
'fields' => [
'id' => Type::nonNull(Type::id()),
'label' => Type::string(),
],
]);

$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'node' => [
'type' => Type::string(),
'args' => [
'input' => Type::nonNull($inputType),
],
'resolve' => static fn ($root, array $args): string => $args['input']['id'] . ':' . ($args['input']['label'] ?? ''),
],
],
]);

$types = ['Query' => $queryType, 'ID' => $customID, 'NodeInput' => $inputType];

$schema = new Schema([
'query' => $queryType,
'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null,
]);

$schema->assertValid();

$result = GraphQL::executeQuery(
$schema,
'query ($input: NodeInput!) { node(input: $input) }',
null,
null,
['input' => ['id' => 'abc-123', 'label' => 'test']],
);

self::assertEmpty($result->errors, isset($result->errors[0]) ? $result->errors[0]->getMessage() : '');
self::assertSame(['data' => ['node' => 'custom-abc-123:test']], $result->toArray());
}

/** @throws InvariantViolation */
private static function createCustomID(\Closure $parseValue): CustomScalarType
{
return new CustomScalarType([
'name' => Type::ID,
'serialize' => static fn ($value): string => (string) $value,
'parseValue' => $parseValue,
'parseLiteral' => static function ($node): string {
if (! $node instanceof StringValueNode) {
throw new \Exception('Expected a string literal for ID.');
}

return $node->value;
},
Comment on lines +326 to +332
]);
}

/** @throws InvariantViolation */
private static function createUppercaseString(): CustomScalarType
{
return new CustomScalarType([
'name' => Type::STRING,
'serialize' => static fn ($value): string => strtoupper((string) $value),
'parseValue' => static fn ($value): string => (string) $value,
'parseLiteral' => static function ($node): string {
if (! $node instanceof StringValueNode) {
throw new \Exception('Expected a string literal for String.');
}

return $node->value;
},
Comment on lines +343 to +349
]);
}

Expand Down
76 changes: 76 additions & 0 deletions tests/Utils/TypeComparatorsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php declare(strict_types=1);

namespace GraphQL\Tests\Utils;

use GraphQL\Error\InvariantViolation;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Utils\TypeComparators;
use PHPUnit\Framework\TestCase;

final class TypeComparatorsTest extends TestCase
{
public function testIsEqualTypeWithDifferentInstancesOfSameNamedType(): void
{
$customString = new CustomScalarType(['name' => Type::STRING]);

self::assertTrue(TypeComparators::isEqualType(Type::string(), $customString));
self::assertTrue(TypeComparators::isEqualType($customString, Type::string()));
}

public function testIsEqualTypeWithWrappedDifferentInstances(): void
{
$customString = new CustomScalarType(['name' => Type::STRING]);

self::assertTrue(TypeComparators::isEqualType(Type::nonNull(Type::string()), Type::nonNull($customString)));
self::assertTrue(TypeComparators::isEqualType(Type::listOf(Type::string()), Type::listOf($customString)));
self::assertTrue(TypeComparators::isEqualType(
Type::nonNull(Type::listOf(Type::string())),
Type::nonNull(Type::listOf($customString)),
));
}

public function testIsTypeSubTypeOfWithDifferentInstancesOfSameNamedType(): void
{
$schema = $this->createSchemaWithCustomString();
$customString = new CustomScalarType(['name' => Type::STRING]);

self::assertTrue(TypeComparators::isTypeSubTypeOf($schema, $customString, Type::string()));
self::assertTrue(TypeComparators::isTypeSubTypeOf($schema, Type::string(), $customString));
}

public function testIsTypeSubTypeOfWithWrappedDifferentInstances(): void
{
$schema = $this->createSchemaWithCustomString();
$customString = new CustomScalarType(['name' => Type::STRING]);

self::assertTrue(TypeComparators::isTypeSubTypeOf($schema, Type::nonNull($customString), Type::string()));
self::assertTrue(TypeComparators::isTypeSubTypeOf($schema, Type::nonNull($customString), Type::nonNull(Type::string())));
self::assertTrue(TypeComparators::isTypeSubTypeOf(
$schema,
Type::nonNull(Type::listOf(Type::nonNull($customString))),
Type::listOf(Type::nonNull(Type::string())),
));
}

/** @throws InvariantViolation */
private function createSchemaWithCustomString(): Schema
{
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'greeting' => [
'type' => Type::string(),
'resolve' => static fn (): string => 'hello',
],
],
]);

return new Schema([
'query' => $queryType,
'typeLoader' => static fn (string $name): ?Type => ['Query' => $queryType][$name] ?? null,
]);
}
}
Loading