diff --git a/docs/class-reference.md b/docs/class-reference.md index 21b10dc2c..326c88d8d 100644 --- a/docs/class-reference.md +++ b/docs/class-reference.md @@ -2955,7 +2955,8 @@ static function astFromValue($value, GraphQL\Type\Definition\InputType $type): ? static function valueFromAST( ?GraphQL\Language\AST\ValueNode $valueNode, GraphQL\Type\Definition\Type $type, - ?array $variables = null + ?array $variables = null, + ?GraphQL\Type\Schema $schema = null ) ``` diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index 55a865e99..f87bd06c0 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -466,10 +466,13 @@ protected function shouldIncludeNode(SelectionNode $node): bool { $variableValues = $this->exeContext->variableValues; + $schema = $this->exeContext->schema; + $skip = Values::getDirectiveValues( Directive::skipDirective(), $node, - $variableValues + $variableValues, + $schema, ); if (isset($skip['if']) && $skip['if'] === true) { return false; @@ -478,7 +481,8 @@ protected function shouldIncludeNode(SelectionNode $node): bool $include = Values::getDirectiveValues( Directive::includeDirective(), $node, - $variableValues + $variableValues, + $schema, ); return ! isset($include['if']) || $include['if'] !== false; @@ -738,7 +742,8 @@ protected function resolveFieldValueOrError( $args = $this->fieldArgsCache[$fieldDef][$fieldNode] ??= $argsMapper(Values::getArgumentValues( $fieldDef, $fieldNode, - $this->exeContext->variableValues + $this->exeContext->variableValues, + $this->exeContext->schema, ), $fieldDef, $fieldNode, $contextValue); return $resolveFn($rootValue, $args, $contextValue, $info); diff --git a/src/Executor/Values.php b/src/Executor/Values.php index e63737e79..185851fad 100644 --- a/src/Executor/Values.php +++ b/src/Executor/Values.php @@ -154,13 +154,13 @@ public static function getVariableValues(Schema $schema, NodeList $varDefNodes, * * @return array|null */ - public static function getDirectiveValues(Directive $directiveDef, Node $node, ?array $variableValues = null): ?array + public static function getDirectiveValues(Directive $directiveDef, Node $node, ?array $variableValues = null, ?Schema $schema = null): ?array { $directiveDefName = $directiveDef->name; foreach ($node->directives as $directive) { if ($directive->name->value === $directiveDefName) { - return self::getArgumentValues($directiveDef, $directive, $variableValues); + return self::getArgumentValues($directiveDef, $directive, $variableValues, $schema); } } @@ -180,7 +180,7 @@ public static function getDirectiveValues(Directive $directiveDef, Node $node, ? * * @return array */ - public static function getArgumentValues($def, Node $node, ?array $variableValues = null): array + public static function getArgumentValues($def, Node $node, ?array $variableValues = null, ?Schema $schema = null): array { if ($def->args === []) { return []; @@ -196,7 +196,7 @@ public static function getArgumentValues($def, Node $node, ?array $variableValue } } - return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node); + return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node, $schema); } /** @@ -209,7 +209,7 @@ public static function getArgumentValues($def, Node $node, ?array $variableValue * * @return array */ - public static function getArgumentValuesForMap($def, array $argumentValueMap, ?array $variableValues = null, ?Node $referenceNode = null): array + public static function getArgumentValuesForMap($def, array $argumentValueMap, ?array $variableValues = null, ?Node $referenceNode = null, ?Schema $schema = null): array { /** @var array $coercedValues */ $coercedValues = []; @@ -260,7 +260,7 @@ public static function getArgumentValuesForMap($def, array $argumentValueMap, ?a // usage here is of the correct type. $coercedValues[$name] = $variableValues[$variableName] ?? null; } else { - $coercedValue = AST::valueFromAST($argumentValueNode, $argType, $variableValues); + $coercedValue = AST::valueFromAST($argumentValueNode, $argType, $variableValues, $schema); if (Utils::undefined() === $coercedValue) { // Note: ValuesOfCorrectType validation should catch this before // execution. This is a runtime check to ensure execution does not diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index 681799dbc..805c4dca6 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -228,7 +228,7 @@ public static function overrideStandardTypes(array $types): void public static function isBuiltInScalar($type): bool { return $type instanceof ScalarType - && in_array($type->name, self::BUILT_IN_SCALAR_NAMES, true); + && self::isBuiltInScalarName($type->name); } /** Checks if the given name is one of the built-in scalar type names (ID, String, Int, Float, Boolean). */ diff --git a/src/Utils/AST.php b/src/Utils/AST.php index 891ccfd63..c21229fcb 100644 --- a/src/Utils/AST.php +++ b/src/Utils/AST.php @@ -37,6 +37,7 @@ use GraphQL\Type\Definition\NullableType; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Schema; /** * Various utilities dealing with AST. @@ -307,7 +308,7 @@ public static function astFromValue($value, InputType $type): ?ValueNode * * @api */ - public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $variables = null) + public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $variables = null, ?Schema $schema = null) { $undefined = Utils::undefined(); @@ -323,7 +324,7 @@ public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $v return $undefined; } - return self::valueFromAST($valueNode, $type->getWrappedType(), $variables); + return self::valueFromAST($valueNode, $type->getWrappedType(), $variables, $schema); } if ($valueNode instanceof NullValueNode) { @@ -362,7 +363,7 @@ public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $v $coercedValues[] = null; } else { - $itemValue = self::valueFromAST($itemNode, $itemType, $variables); + $itemValue = self::valueFromAST($itemNode, $itemType, $variables, $schema); if ($undefined === $itemValue) { // Invalid: intentionally return no value. return $undefined; @@ -375,7 +376,7 @@ public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $v return $coercedValues; } - $coercedValue = self::valueFromAST($valueNode, $itemType, $variables); + $coercedValue = self::valueFromAST($valueNode, $itemType, $variables, $schema); if ($undefined === $coercedValue) { // Invalid: intentionally return no value. return $undefined; @@ -416,7 +417,8 @@ public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $v $fieldValue = self::valueFromAST( $fieldNode->value, $field->getType(), - $variables + $variables, + $schema, ); if ($undefined === $fieldValue) { @@ -439,6 +441,16 @@ public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $v } assert($type instanceof ScalarType, 'only remaining option'); + $typeName = $type->name; + + // Account for type loader returning a different scalar instance than + // the built-in singleton used in field definitions. Resolve the actual + // type from the schema to ensure the correct parseLiteral() is called. + if ($schema !== null && Type::isBuiltInScalarName($typeName)) { + $schemaType = $schema->getType($typeName); + assert($schemaType instanceof ScalarType, "Schema must provide a ScalarType for built-in scalar \"{$typeName}\"."); + $type = $schemaType; + } // Scalars fulfill parsing a literal value via parseLiteral(). // Invalid values represent a failure to parse correctly, in which case diff --git a/tests/Type/Definition/TypeTest.php b/tests/Type/Definition/TypeTest.php index 45f1a75f1..3bc2d1789 100644 --- a/tests/Type/Definition/TypeTest.php +++ b/tests/Type/Definition/TypeTest.php @@ -48,4 +48,20 @@ public function testIsBuiltInScalarReturnsFalseForCustomScalarWithNonBuiltInName { self::assertFalse(Type::isBuiltInScalar(new CustomScalarType(['name' => 'MyScalar']))); // @phpstan-ignore staticMethod.alreadyNarrowedType } + + public function testIsBuiltInScalarNameReturnsTrueForBuiltInNames(): void + { + self::assertTrue(Type::isBuiltInScalarName(Type::STRING)); + self::assertTrue(Type::isBuiltInScalarName(Type::INT)); + self::assertTrue(Type::isBuiltInScalarName(Type::FLOAT)); + self::assertTrue(Type::isBuiltInScalarName(Type::BOOLEAN)); + self::assertTrue(Type::isBuiltInScalarName(Type::ID)); + } + + public function testIsBuiltInScalarNameReturnsFalseForNonBuiltInNames(): void + { + self::assertFalse(Type::isBuiltInScalarName('MyScalar')); + self::assertFalse(Type::isBuiltInScalarName('Query')); + self::assertFalse(Type::isBuiltInScalarName('')); + } } diff --git a/tests/Type/ScalarOverridesTest.php b/tests/Type/ScalarOverridesTest.php index 3e7a78512..74877eebd 100644 --- a/tests/Type/ScalarOverridesTest.php +++ b/tests/Type/ScalarOverridesTest.php @@ -4,6 +4,8 @@ use GraphQL\Error\InvariantViolation; use GraphQL\GraphQL; +use GraphQL\Language\AST\BooleanValueNode; +use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\InputObjectType; @@ -298,6 +300,87 @@ public function testTypesOverrideWithInputObjectFieldOfOverriddenBuiltInScalarTy self::assertSame(['data' => ['node' => 'custom-abc-123:test']], $result->toArray()); } + public function testTypesOverrideCallsParseLiteralForInlineArgument(): void + { + $parseLiteralCalled = false; + + $customID = new CustomScalarType([ + 'name' => Type::ID, + 'serialize' => static fn ($value): string => (string) $value, + 'parseValue' => static fn ($value): string => 'parsed-' . $value, + 'parseLiteral' => static function ($node) use (&$parseLiteralCalled): string { + $parseLiteralCalled = true; + + assert($node instanceof IntValueNode || $node instanceof StringValueNode); + + return 'literal-' . $node->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'], + ], + ], + ]); + + $schema = new Schema([ + 'query' => $queryType, + 'types' => [$customID], + ]); + + $result = GraphQL::executeQuery($schema, '{ node(id: 123) }'); + + self::assertEmpty($result->errors, isset($result->errors[0]) ? $result->errors[0]->getMessage() : ''); + self::assertTrue($parseLiteralCalled, 'Expected custom parseLiteral to be called for inline literal argument'); + self::assertSame(['data' => ['node' => 'node-literal-123']], $result->toArray()); + } + + public function testTypesOverrideCallsParseLiteralForDirectiveArgument(): void + { + $parseLiteralCalled = false; + + $customBoolean = new CustomScalarType([ + 'name' => Type::BOOLEAN, + 'serialize' => static fn ($value): bool => (bool) $value, + 'parseValue' => static fn ($value): bool => (bool) $value, + 'parseLiteral' => static function ($node) use (&$parseLiteralCalled): bool { + $parseLiteralCalled = true; + + self::assertInstanceOf(BooleanValueNode::class, $node); + + return $node->value; + }, + ]); + + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => Type::string(), + 'resolve' => static fn (): string => 'hello', + ], + ], + ]); + + $schema = new Schema([ + 'query' => $queryType, + 'types' => [$customBoolean], + ]); + + $result = GraphQL::executeQuery($schema, '{ greeting @include(if: true) }'); + + self::assertEmpty($result->errors, isset($result->errors[0]) ? $result->errors[0]->getMessage() : ''); + self::assertTrue($parseLiteralCalled, 'Expected custom parseLiteral to be called for directive argument'); + self::assertSame(['data' => ['greeting' => 'hello']], $result->toArray()); + } + public function testTypesOverrideWorksWithTypeLoader(): void { $uppercaseString = self::createUppercaseString();