diff --git a/examples/01-blog/Blog/Type/Scalar/UrlType.php b/examples/01-blog/Blog/Type/Scalar/UrlType.php index 361e27f..c539121 100644 --- a/examples/01-blog/Blog/Type/Scalar/UrlType.php +++ b/examples/01-blog/Blog/Type/Scalar/UrlType.php @@ -42,20 +42,21 @@ class UrlType extends ScalarType /** * Parses an externally provided literal value to use as an input (e.g. in Query AST) * - * @param $ast Node + * @param Node $valueNode + * @param array|null $variables * @return null|string * @throws Error */ - public function parseLiteral($ast) + public function parseLiteral($valueNode, array $variables = null) { // Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL // error location in query: - if (!($ast instanceof StringValueNode)) { - throw new Error('Query error: Can only parse strings got: ' . $ast->kind, [$ast]); + if (!($valueNode instanceof StringValueNode)) { + throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]); } - if (!is_string($ast->value) || !filter_var($ast->value, FILTER_VALIDATE_URL)) { - throw new Error('Query error: Not a valid URL', [$ast]); + if (!is_string($valueNode->value) || !filter_var($valueNode->value, FILTER_VALIDATE_URL)) { + throw new Error('Query error: Not a valid URL', [$valueNode]); } - return $ast->value; + return $valueNode->value; } } diff --git a/src/Executor/Values.php b/src/Executor/Values.php index 06c9532..49bcc3a 100644 --- a/src/Executor/Values.php +++ b/src/Executor/Values.php @@ -1,9 +1,7 @@ parseValue($value); - if (null === $parseResult && !$type->isValidValue($value)) { - $v = Utils::printSafeJson($value); - return [ - "Expected type \"{$type->name}\", found $v." - ]; - } - return []; - } catch (\Exception $e) { + Utils::invariant($type instanceof EnumType || $type instanceof ScalarType, 'Must be input type'); + + + try { + // Scalar/Enum input checks to ensure the type can parse the value to + // a non-null value. + + if (!$type->isValidValue($value)) { + $v = Utils::printSafeJson($value); return [ - "Expected type \"{$type->name}\", found " . Utils::printSafeJson($value) . ': ' . - $e->getMessage() - ]; - } catch (\Throwable $e) { - return [ - "Expected type \"{$type->name}\", found " . Utils::printSafeJson($value) . ': ' . - $e->getMessage() + "Expected type \"{$type->name}\", found $v." ]; } + } catch (\Exception $e) { + return [ + "Expected type \"{$type->name}\", found " . Utils::printSafeJson($value) . ': ' . + $e->getMessage() + ]; + } catch (\Throwable $e) { + return [ + "Expected type \"{$type->name}\", found " . Utils::printSafeJson($value) . ': ' . + $e->getMessage() + ]; } - throw new InvariantViolation('Must be input type'); + + return []; } /** @@ -370,16 +370,12 @@ class Values return $coercedObj; } - if ($type instanceof LeafType) { - $parsed = $type->parseValue($value); - if (null === $parsed) { - // null or invalid values represent a failure to parse correctly, - // in which case no value is returned. - return $undefined; - } - return $parsed; + Utils::invariant($type instanceof EnumType || $type instanceof ScalarType, 'Must be input type'); + + if ($type->isValidValue($value)) { + return $type->parseValue($value); } - throw new InvariantViolation('Must be input type'); + return $undefined; } } diff --git a/src/Language/AST/ListValueNode.php b/src/Language/AST/ListValueNode.php index 1a43512..bcd56db 100644 --- a/src/Language/AST/ListValueNode.php +++ b/src/Language/AST/ListValueNode.php @@ -7,7 +7,7 @@ class ListValueNode extends Node implements ValueNode public $kind = NodeKind::LST; /** - * @var ValueNode[] + * @var ValueNode[]|NodeList */ public $values; } diff --git a/src/Language/AST/ObjectValueNode.php b/src/Language/AST/ObjectValueNode.php index 2dd38ca..cc763e9 100644 --- a/src/Language/AST/ObjectValueNode.php +++ b/src/Language/AST/ObjectValueNode.php @@ -6,7 +6,7 @@ class ObjectValueNode extends Node implements ValueNode public $kind = NodeKind::OBJECT; /** - * @var ObjectFieldNode[] + * @var ObjectFieldNode[]|NodeList */ public $fields; } diff --git a/src/Type/Definition/BooleanType.php b/src/Type/Definition/BooleanType.php index 64746a9..2a1adc7 100644 --- a/src/Type/Definition/BooleanType.php +++ b/src/Type/Definition/BooleanType.php @@ -2,6 +2,7 @@ namespace GraphQL\Type\Definition; use GraphQL\Language\AST\BooleanValueNode; +use GraphQL\Utils\Utils; /** * Class BooleanType @@ -34,18 +35,19 @@ class BooleanType extends ScalarType */ public function parseValue($value) { - return is_bool($value) ? $value : null; + return is_bool($value) ? $value : Utils::undefined(); } /** - * @param $ast + * @param $valueNode + * @param array|null $variables * @return bool|null */ - public function parseLiteral($ast) + public function parseLiteral($valueNode, array $variables = null) { - if ($ast instanceof BooleanValueNode) { - return (bool) $ast->value; + if ($valueNode instanceof BooleanValueNode) { + return (bool) $valueNode->value; } - return null; + return Utils::undefined(); } } diff --git a/src/Type/Definition/CustomScalarType.php b/src/Type/Definition/CustomScalarType.php index 49bc8be..14e1d53 100644 --- a/src/Type/Definition/CustomScalarType.php +++ b/src/Type/Definition/CustomScalarType.php @@ -1,6 +1,7 @@ config['parseValue'])) { return call_user_func($this->config['parseValue'], $value); } else { - return null; + return $value; } } /** * @param $valueNode + * @param array|null $variables * @return mixed */ - public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode) + public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode, array $variables = null) { if (isset($this->config['parseLiteral'])) { - return call_user_func($this->config['parseLiteral'], $valueNode); + return call_user_func($this->config['parseLiteral'], $valueNode, $variables); } else { - return null; + return AST::valueFromASTUntyped($valueNode, $variables); } } diff --git a/src/Type/Definition/EnumType.php b/src/Type/Definition/EnumType.php index 120bfee..035c86f 100644 --- a/src/Type/Definition/EnumType.php +++ b/src/Type/Definition/EnumType.php @@ -122,9 +122,10 @@ class EnumType extends Type implements InputType, OutputType, LeafType /** * @param $valueNode + * @param array|null $variables * @return bool */ - public function isValidLiteral($valueNode) + public function isValidLiteral($valueNode, array $variables = null) { return $valueNode instanceof EnumValueNode && $this->getNameLookup()->offsetExists($valueNode->value); } @@ -136,14 +137,15 @@ class EnumType extends Type implements InputType, OutputType, LeafType public function parseValue($value) { $lookup = $this->getNameLookup(); - return isset($lookup[$value]) ? $lookup[$value]->value : null; + return isset($lookup[$value]) ? $lookup[$value]->value : Utils::undefined(); } /** * @param $value + * @param array|null $variables * @return null */ - public function parseLiteral($value) + public function parseLiteral($value, array $variables = null) { if ($value instanceof EnumValueNode) { $lookup = $this->getNameLookup(); diff --git a/src/Type/Definition/FloatType.php b/src/Type/Definition/FloatType.php index 092f258..826b017 100644 --- a/src/Type/Definition/FloatType.php +++ b/src/Type/Definition/FloatType.php @@ -1,7 +1,6 @@ value; + if ($valueNode instanceof FloatValueNode || $valueNode instanceof IntValueNode) { + return (float) $valueNode->value; } - return null; + return Utils::undefined(); } } diff --git a/src/Type/Definition/IDType.php b/src/Type/Definition/IDType.php index 5912973..47ed897 100644 --- a/src/Type/Definition/IDType.php +++ b/src/Type/Definition/IDType.php @@ -1,7 +1,6 @@ value; + if ($valueNode instanceof StringValueNode || $valueNode instanceof IntValueNode) { + return $valueNode->value; } - return null; + return Utils::undefined(); } } diff --git a/src/Type/Definition/IntType.php b/src/Type/Definition/IntType.php index 4473ce5..5444e2e 100644 --- a/src/Type/Definition/IntType.php +++ b/src/Type/Definition/IntType.php @@ -1,7 +1,6 @@ = self::MIN_INT ? $value : null; + return $isInt && $value <= self::MAX_INT && $value >= self::MIN_INT ? $value : Utils::undefined(); } /** - * @param $ast + * @param $valueNode + * @param array|null $variables * @return int|null */ - public function parseLiteral($ast) + public function parseLiteral($valueNode, array $variables = null) { - if ($ast instanceof IntValueNode) { - $val = (int) $ast->value; - if ($ast->value === (string) $val && self::MIN_INT <= $val && $val <= self::MAX_INT) { + if ($valueNode instanceof IntValueNode) { + $val = (int) $valueNode->value; + if ($valueNode->value === (string) $val && self::MIN_INT <= $val && $val <= self::MAX_INT) { return $val; } } - return null; + return Utils::undefined(); } } diff --git a/src/Type/Definition/LeafType.php b/src/Type/Definition/LeafType.php index a0bd30f..2ec8efc 100644 --- a/src/Type/Definition/LeafType.php +++ b/src/Type/Definition/LeafType.php @@ -1,6 +1,8 @@ parseValue($value); + return !Utils::isInvalid($this->parseValue($value)); } /** @@ -56,10 +55,11 @@ abstract class ScalarType extends Type implements OutputType, InputType, LeafTyp * Equivalent to checking for if the parsedLiteral is nullish. * * @param $valueNode + * @param array|null $variables * @return bool */ - public function isValidLiteral($valueNode) + public function isValidLiteral($valueNode, array $variables = null) { - return null !== $this->parseLiteral($valueNode); + return !Utils::isInvalid($this->parseLiteral($valueNode, $variables)); } } diff --git a/src/Type/Definition/StringType.php b/src/Type/Definition/StringType.php index 0e0784b..98dab82 100644 --- a/src/Type/Definition/StringType.php +++ b/src/Type/Definition/StringType.php @@ -1,7 +1,6 @@ value; + if ($valueNode instanceof StringValueNode) { + return $valueNode->value; } - return null; + return Utils::undefined(); } } diff --git a/src/Utils/AST.php b/src/Utils/AST.php index bc3a0e4..6fd6a9b 100644 --- a/src/Utils/AST.php +++ b/src/Utils/AST.php @@ -30,9 +30,9 @@ use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\LeafType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; +use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; -use GraphQL\Utils\Utils; /** * Various utilities dealing with AST @@ -383,19 +383,77 @@ class AST return $coercedObj; } - if ($type instanceof LeafType) { - $parsed = $type->parseLiteral($valueNode); - - if (null === $parsed && !$type->isValidLiteral($valueNode)) { - // Invalid values represent a failure to parse correctly, in which case - // no value is returned. - return $undefined; - } - - return $parsed; + if (!$type instanceof ScalarType && !$type instanceof EnumType) { + throw new InvariantViolation('Must be input type'); } - throw new InvariantViolation('Must be input type'); + if ($type->isValidLiteral($valueNode, $variables)) { + return $type->parseLiteral($valueNode, $variables); + } + + return $undefined; + } + + /** + * Produces a PHP value given a GraphQL Value AST. + * + * Unlike `valueFromAST()`, no type is provided. The resulting JavaScript value + * will reflect the provided GraphQL value AST. + * + * | GraphQL Value | PHP Value | + * | -------------------- | ------------- | + * | Input Object | Assoc Array | + * | List | Array | + * | Boolean | Boolean | + * | String | String | + * | Int / Float | Int / Float | + * | Enum | Mixed | + * | Null | null | + * + * @api + * @param Node $valueNode + * @param array|null $variables + * @return mixed + * @throws \Exception + */ + public static function valueFromASTUntyped($valueNode, array $variables = null) { + switch (true) { + case $valueNode instanceof NullValueNode: + return null; + case $valueNode instanceof IntValueNode: + return intval($valueNode->value, 10); + case $valueNode instanceof FloatValueNode: + return floatval($valueNode->value); + case $valueNode instanceof StringValueNode: + case $valueNode instanceof EnumValueNode: + case $valueNode instanceof BooleanValueNode: + return $valueNode->value; + case $valueNode instanceof ListValueNode: + return array_map( + function($node) use ($variables) { + return self::valueFromASTUntyped($node, $variables); + }, + iterator_to_array($valueNode->values) + ); + case $valueNode instanceof ObjectValueNode: + return array_combine( + array_map( + function($field) { return $field->name->value; }, + iterator_to_array($valueNode->fields) + ), + array_map( + function($field) use ($variables) { return self::valueFromASTUntyped($field->value, $variables); }, + iterator_to_array($valueNode->fields) + ) + ); + case $valueNode instanceof VariableNode: + $variableName = $valueNode->name->value; + return ($variables && isset($variables[$variableName]) && !Utils::isInvalid($variables[$variableName])) + ? $variables[$variableName] + : null; + default: + throw new InvariantViolation('Unexpected value kind: ' . $valueNode->kind); + } } /** diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index 6faf6e3..b04ef40 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -539,19 +539,9 @@ class BuildSchema 'name' => $def->name->value, 'description' => $this->getDescription($def), 'astNode' => $def, - 'serialize' => function () { - return false; + 'serialize' => function($value) { + return $value; }, - // Note: validation calls the parse functions to determine if a - // literal value is correct. Returning null would cause use of custom - // scalars to always fail validation. Returning false causes them to - // always pass validation. - 'parseValue' => function () { - return false; - }, - 'parseLiteral' => function () { - return false; - } ]; } diff --git a/src/Utils/Utils.php b/src/Utils/Utils.php index fa1183a..cb019ef 100644 --- a/src/Utils/Utils.php +++ b/src/Utils/Utils.php @@ -15,12 +15,23 @@ class Utils return $undefined ?: $undefined = new \stdClass(); } + /** + * Check if the value is invalid + * + * @param mixed $value + * @return bool + */ + public static function isInvalid($value) + { + return self::undefined() === $value; + } + /** * @param object $obj * @param array $vars * @param array $requiredKeys * - * @return array + * @return object */ public static function assign($obj, array $vars, array $requiredKeys = []) { diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 31b7649..1a1e83c 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -2,7 +2,6 @@ namespace GraphQL\Validator; use GraphQL\Error\Error; -use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\ListValueNode; use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\NodeKind; @@ -11,11 +10,12 @@ use GraphQL\Language\AST\VariableNode; use GraphQL\Language\Printer; use GraphQL\Language\Visitor; use GraphQL\Type\Schema; +use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; -use GraphQL\Type\Definition\LeafType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\ScalarType; use GraphQL\Utils\Utils; use GraphQL\Utils\TypeInfo; use GraphQL\Validator\Rules\AbstractValidationRule; @@ -306,17 +306,15 @@ class DocumentValidator return $errors; } - if ($type instanceof LeafType) { - // Scalars must parse to a non-null value - if (!$type->isValidLiteral($valueNode)) { - $printed = Printer::doPrint($valueNode); - return [ "Expected type \"{$type->name}\", found $printed." ]; - } + Utils::invariant($type instanceof ScalarType || $type instanceof EnumType, 'Must be input type'); - return []; + // Scalars determine if a literal values is valid. + if (!$type->isValidLiteral($valueNode)) { + $printed = Printer::doPrint($valueNode); + return [ "Expected type \"{$type->name}\", found $printed." ]; } - throw new InvariantViolation('Must be input type'); + return []; } /** diff --git a/tests/Executor/TestClasses.php b/tests/Executor/TestClasses.php index 6e53b50..ef86938 100644 --- a/tests/Executor/TestClasses.php +++ b/tests/Executor/TestClasses.php @@ -2,6 +2,7 @@ namespace GraphQL\Tests\Executor; use GraphQL\Type\Definition\ScalarType; +use GraphQL\Utils\Utils; class Dog { @@ -65,15 +66,15 @@ class ComplexScalar extends ScalarType if ($value === 'SerializedValue') { return 'DeserializedValue'; } - return null; + return Utils::undefined(); } - public function parseLiteral($valueNode) + public function parseLiteral($valueNode, array $variables = null) { if ($valueNode->value === 'SerializedValue') { return 'DeserializedValue'; } - return null; + return Utils::undefined(); } } diff --git a/tests/Utils/AstFromValueUntypedTest.php b/tests/Utils/AstFromValueUntypedTest.php new file mode 100644 index 0000000..53c81da --- /dev/null +++ b/tests/Utils/AstFromValueUntypedTest.php @@ -0,0 +1,110 @@ +assertEquals( + $expected, + AST::valueFromASTUntyped(Parser::parseValue($valueText), $variables) + ); + } + + /** + * @it parses simple values + */ + public function testParsesSimpleValues() + { + $this->assertTestCase('null', null); + $this->assertTestCase('true', true); + $this->assertTestCase('false', false); + $this->assertTestCase('123', 123); + $this->assertTestCase('123.456', 123.456); + $this->assertTestCase('abc123', 'abc123'); + } + + /** + * @it parses lists of values + */ + public function testParsesListsOfValues() + { + $this->assertTestCase('[true, false]', [true, false]); + $this->assertTestCase('[true, 123.45]', [true, 123.45]); + $this->assertTestCase('[true, null]', [true, null]); + $this->assertTestCase('[true, ["foo", 1.2]]', [true, ['foo', 1.2]]); + } + + /** + * @it parses input objects + */ + public function testParsesInputObjects() + { + $this->assertTestCase( + '{ int: 123, bool: false }', + ['int' => 123, 'bool' => false] + ); + + $this->assertTestCase( + '{ foo: [ { bar: "baz"} ] }', + ['foo' => [['bar' => 'baz']]] + ); + } + + /** + * @it parses enum values as plain strings + */ + public function testParsesEnumValuesAsPlainStrings() + { + $this->assertTestCase( + 'TEST_ENUM_VALUE', + 'TEST_ENUM_VALUE' + ); + + $this->assertTestCase( + '[TEST_ENUM_VALUE]', + ['TEST_ENUM_VALUE'] + ); + } + + /** + * @it parses enum values as plain strings + */ + public function testParsesVariables() + { + $this->assertTestCase( + '$testVariable', + 'foo', + ['testVariable' => 'foo'] + ); + $this->assertTestCase( + '[$testVariable]', + ['foo'], + ['testVariable' => 'foo'] + ); + $this->assertTestCase( + '{a:[$testVariable]}', + ['a' => ['foo']], + ['testVariable' => 'foo'] + ); + $this->assertTestCase( + '$testVariable', + null, + ['testVariable' => null] + ); + $this->assertTestCase( + '$testVariable', + null, + [] + ); + $this->assertTestCase( + '$testVariable', + null, + null + ); + } +} diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index 7f0c47e..11054fd 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -636,6 +636,26 @@ type Hello { $this->assertEquals($output, $body); } + /** + * @it Custom scalar argument field with default + */ + public function testCustomScalarArgumentFieldWithDefault() + { + $body = ' +schema { + query: Hello +} + +scalar CustomScalar + +type Hello { + str(int: CustomScalar = 2): String +} +'; + $output = $this->cycleOutput($body); + $this->assertEquals($output, $body); + } + /** * @it Simple type with mutation */