diff --git a/src/Type/Definition/IDType.php b/src/Type/Definition/IDType.php index 524d686..d881209 100644 --- a/src/Type/Definition/IDType.php +++ b/src/Type/Definition/IDType.php @@ -31,7 +31,7 @@ When expected as an input type, any string (such as `"4"`) or integer */ public function serialize($value) { - return (string) $value; + return $this->parseValue($value); } /** @@ -40,6 +40,12 @@ When expected as an input type, any string (such as `"4"`) or integer */ public function parseValue($value) { + if ($value === true) { + return 'true'; + } + if ($value === false) { + return 'false'; + } return (string) $value; } diff --git a/src/Utils/AST.php b/src/Utils/AST.php index 88ea3aa..b100a5b 100644 --- a/src/Utils/AST.php +++ b/src/Utils/AST.php @@ -1,11 +1,11 @@ getWrappedType() : null; - return new ListValue([ - 'values' => Utils::map($value, function ($item) use ($itemType) { - $itemValue = self::astFromValue($item, $itemType); - Utils::invariant($itemValue, 'Could not create AST item.'); - return $itemValue; - }) - ]); - } else if ($type instanceof ListOfType) { - // Because GraphQL will accept single values as a "list of one" when - // expecting a list, if there's a non-array value and an expected list type, - // create an AST using the list's item type. - return self::astFromValue($value, $type->getWrappedType()); - } - - if (is_bool($value)) { - return new BooleanValue(['value' => $value]); - } - - if (is_int($value)) { - if ($type instanceof FloatType) { - return new FloatValue(['value' => (string)(float)$value]); - } - return new IntValue(['value' => $value]); - } else if (is_float($value)) { - $tmp = (int) $value; - if ($tmp == $value && (!$type instanceof FloatType)) { - return new IntValue(['value' => (string)$tmp]); - } else { - return new FloatValue(['value' => (string)$value]); + if ($type instanceof ListOfType) { + $itemType = $type->getWrappedType(); + if (is_array($value) || ($value instanceof \Traversable)) { + $valuesASTs = []; + foreach ($value as $item) { + $itemAST = self::astFromValue($item, $itemType); + if ($itemAST) { + $valuesASTs[] = $itemAST; + } + } + return new ListValue(['values' => $valuesASTs]); } + return self::astFromValue($value, $itemType); } - // PHP strings can be Enum values or String values. Use the - // GraphQLType to differentiate if possible. - if (is_string($value)) { - if ($type instanceof EnumType && preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $value)) { - return new EnumValue(['value' => $value]); + // Populate the fields of the input object by creating ASTs from each value + // in the PHP object according to the fields in the input type. + if ($type instanceof InputObjectType) { + $isArrayLike = is_array($value) || $value instanceof \ArrayAccess; + if ($value === null || (!$isArrayLike && !is_object($value))) { + return null; + } + $fields = $type->getFields(); + $fieldASTs = []; + foreach ($fields as $fieldName => $field) { + if ($isArrayLike) { + $fieldValue = isset($value[$fieldName]) ? $value[$fieldName] : null; + } else { + $fieldValue = isset($value->{$fieldName}) ? $value->{$fieldName} : null; + } + $fieldValue = self::astFromValue($fieldValue, $field->getType()); + + if ($fieldValue) { + $fieldASTs[] = new ObjectField([ + 'name' => new Name(['value' => $fieldName]), + 'value' => $fieldValue + ]); + } + } + return new ObjectValue(['fields' => $fieldASTs]); + } + + // Since value is an internally represented value, it must be serialized + // to an externally represented value before converting into an AST. + if ($type instanceof LeafType) { + $serialized = $type->serialize($value); + } else { + throw new InvariantViolation("Must provide Input Type, cannot use: " . Utils::printSafe($type)); + } + + if (null === $serialized) { + return null; + } + + // Others serialize based on their corresponding PHP scalar types. + if (is_bool($serialized)) { + return new BooleanValue(['value' => $serialized]); + } + if (is_int($serialized)) { + return new IntValue(['value' => $serialized]); + } + if (is_float($serialized)) { + if ((int) $serialized == $serialized) { + return new IntValue(['value' => $serialized]); + } + return new FloatValue(['value' => $serialized]); + } + if (is_string($serialized)) { + // Enum types use Enum literals. + if ($type instanceof EnumType) { + return new EnumValue(['value' => $serialized]); + } + + // ID types can use Int literals. + $asInt = (int) $serialized; + if ($type instanceof IDType && (string) $asInt === $serialized) { + return new IntValue(['value' => $serialized]); } // Use json_encode, which uses the same string encoding as GraphQL, // then remove the quotes. return new StringValue([ - 'value' => mb_substr(json_encode($value), 1, -1) + 'value' => substr(json_encode($serialized), 1, -1) ]); } - // last remaining possible type - Utils::invariant(is_object($value) || $isAssoc); - - // Populate the fields of the input object by creating ASTs from each value - // in the PHP object. - $fields = []; - $tmp = $isAssoc ? $value : get_object_vars($value); - foreach ($tmp as $fieldName => $objValue) { - $fieldType = null; - if ($type instanceof InputObjectType) { - $tmp = $type->getFields(); - $fieldDef = isset($tmp[$fieldName]) ? $tmp[$fieldName] : null; - $fieldType = $fieldDef ? $fieldDef->getType() : null; - } - - $fieldValue = self::astFromValue($objValue, $fieldType); - - if ($fieldValue) { - $fields[] = new ObjectField([ - 'name' => new Name(['value' => $fieldName]), - 'value' => $fieldValue - ]); - } - } - return new ObjectValue([ - 'fields' => $fields - ]); + throw new InvariantViolation('Cannot convert value to AST: ' . Utils::printSafe($serialized)); } /** + * Produces a PHP value given a GraphQL Value AST. + * + * A GraphQL type must be provided, which will be used to interpret different + * GraphQL Value literals. + * + * | GraphQL Value | PHP Value | + * | -------------------- | ------------- | + * | Input Object | Assoc Array | + * | List | Array | + * | Boolean | Boolean | + * | String | String | + * | Int / Float | Int / Float | + * | Enum Value | Mixed | + * * @param $valueAST * @param InputType $type * @param null $variables @@ -157,6 +175,9 @@ class AST public static function valueFromAST($valueAST, InputType $type, $variables = null) { if ($type instanceof NonNull) { + // Note: we're not checking that the result of valueFromAST is non-null. + // We're assuming that this query has been validated and the value used + // here is of the correct type. return self::valueFromAST($valueAST, $type->getWrappedType(), $variables); } @@ -170,6 +191,9 @@ class AST if (!$variables || !isset($variables[$variableName])) { return null; } + // Note: we're not doing any checking that this variable is correct. We're + // assuming that this query has been validated and the variable usage here + // is of the correct type. return $variables[$variableName]; } @@ -205,7 +229,10 @@ class AST return $values; } - Utils::invariant($type instanceof ScalarType || $type instanceof EnumType, 'Must be input type'); - return $type->parseLiteral($valueAST); + if ($type instanceof LeafType) { + return $type->parseLiteral($valueAST); + } + + throw new InvariantViolation('Must be input type'); } } diff --git a/tests/Utils/AstFromValueTest.php b/tests/Utils/AstFromValueTest.php index 355811d..a65762a 100644 --- a/tests/Utils/AstFromValueTest.php +++ b/tests/Utils/AstFromValueTest.php @@ -11,7 +11,6 @@ use GraphQL\Language\AST\ObjectField; use GraphQL\Language\AST\ObjectValue; use GraphQL\Language\AST\StringValue; use GraphQL\Type\Definition\EnumType; -use GraphQL\Type\Definition\InputObjectField; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Utils\AST; @@ -25,43 +24,66 @@ class ASTFromValueTest extends \PHPUnit_Framework_TestCase */ public function testConvertsBooleanValueToASTs() { - $this->assertEquals(new BooleanValue(['value' => true]), AST::astFromValue(true)); - $this->assertEquals(new BooleanValue(['value' => false]), AST::astFromValue(false)); + $this->assertEquals(new BooleanValue(['value' => true]), AST::astFromValue(true, Type::boolean())); + $this->assertEquals(new BooleanValue(['value' => false]), AST::astFromValue(false, Type::boolean())); + $this->assertEquals(null, AST::astFromValue(null, Type::boolean())); + $this->assertEquals(new BooleanValue(['value' => false]), AST::astFromValue(0, Type::boolean())); + $this->assertEquals(new BooleanValue(['value' => true]), AST::astFromValue(1, Type::boolean())); } /** - * @it converts numeric values to ASTs + * @it converts Int values to Int ASTs */ - public function testConvertsNumericValuesToASTs() + public function testConvertsIntValuesToASTs() { - $this->assertEquals(new IntValue(['value' => '123']), AST::astFromValue(123)); - // $this->assertEquals(new IntValue(['value' => 123]), AST::astFromValue(123.0)); // doesn't make sense for PHP because it has float and int natively unlike JS - $this->assertEquals(new FloatValue(['value' => '123.5']), AST::astFromValue(123.5)); - $this->assertEquals(new IntValue(['value' => '10000']), AST::astFromValue(1e4)); - $this->assertEquals(new FloatValue(['value' => '1.0E+40']), AST::astFromValue(1e40)); // Note: js version will produce 1e+40, both values are valid GraphQL floats + $this->assertEquals(new IntValue(['value' => '123']), AST::astFromValue(123.0, Type::int())); + $this->assertEquals(new IntValue(['value' => '123']), AST::astFromValue(123.5, Type::int())); + $this->assertEquals(new IntValue(['value' => '10000']), AST::astFromValue(1e4, Type::int())); + + try { + AST::astFromValue(1e40, Type::int()); // Note: js version will produce 1e+40, both values are valid GraphQL floats + $this->fail('Expected exception is not thrown'); + } catch (\Exception $e) { + $this->assertSame('Int cannot represent non 32-bit signed integer value: 1.0E+40', $e->getMessage()); + } } /** - * @it converts numeric values to Float ASTs + * @it converts Float values to Int/Float ASTs */ - public function testConvertsNumericValuesToFloatASTs() + public function testConvertsFloatValuesToIntOrFloatASTs() { - $this->assertEquals(new FloatValue(['value' => '123.0']), AST::astFromValue(123, Type::float())); - $this->assertEquals(new FloatValue(['value' => '123.0']), AST::astFromValue(123.0, Type::float())); + $this->assertEquals(new IntValue(['value' => '123']), AST::astFromValue(123, Type::float())); + $this->assertEquals(new IntValue(['value' => '123']), AST::astFromValue(123.0, Type::float())); $this->assertEquals(new FloatValue(['value' => '123.5']), AST::astFromValue(123.5, Type::float())); - $this->assertEquals(new FloatValue(['value' => '10000.0']), AST::astFromValue(1e4, Type::float())); + $this->assertEquals(new IntValue(['value' => '10000']), AST::astFromValue(1e4, Type::float())); $this->assertEquals(new FloatValue(['value' => '1e+40']), AST::astFromValue(1e40, Type::float())); } /** - * @it converts string values to ASTs + * @it converts String values to String ASTs */ public function testConvertsStringValuesToASTs() { - $this->assertEquals(new StringValue(['value' => 'hello']), AST::astFromValue('hello')); - $this->assertEquals(new StringValue(['value' => 'VALUE']), AST::astFromValue('VALUE')); - $this->assertEquals(new StringValue(['value' => 'VA\\nLUE']), AST::astFromValue("VA\nLUE")); - $this->assertEquals(new StringValue(['value' => '123']), AST::astFromValue('123')); + $this->assertEquals(new StringValue(['value' => 'hello']), AST::astFromValue('hello', Type::string())); + $this->assertEquals(new StringValue(['value' => 'VALUE']), AST::astFromValue('VALUE', Type::string())); + $this->assertEquals(new StringValue(['value' => 'VA\\nLUE']), AST::astFromValue("VA\nLUE", Type::string())); + $this->assertEquals(new StringValue(['value' => '123']), AST::astFromValue(123, Type::string())); + $this->assertEquals(new StringValue(['value' => 'false']), AST::astFromValue(false, Type::string())); + $this->assertEquals(null, AST::astFromValue(null, Type::string())); + } + + /** + * @it converts ID values to Int/String ASTs + */ + public function testConvertIdValuesToIntOrStringASTs() + { + $this->assertEquals(new StringValue(['value' => 'hello']), AST::astFromValue('hello', Type::id())); + $this->assertEquals(new StringValue(['value' => 'VALUE']), AST::astFromValue('VALUE', Type::id())); + $this->assertEquals(new StringValue(['value' => 'VA\\nLUE']), AST::astFromValue("VA\nLUE", Type::id())); + $this->assertEquals(new IntValue(['value' => '123']), AST::astFromValue(123, Type::id())); + $this->assertEquals(new StringValue(['value' => 'false']), AST::astFromValue(false, Type::id())); + $this->assertEquals(null, AST::astFromValue(null, Type::id())); } /** @@ -69,11 +91,14 @@ class ASTFromValueTest extends \PHPUnit_Framework_TestCase */ public function testConvertsStringValuesToEnumASTsIfPossible() { - $this->assertEquals(new EnumValue(['value' => 'hello']), AST::astFromValue('hello', $this->myEnum())); $this->assertEquals(new EnumValue(['value' => 'HELLO']), AST::astFromValue('HELLO', $this->myEnum())); - $this->assertEquals(new EnumValue(['value' => 'VALUE']), AST::astFromValue('VALUE', $this->myEnum())); - $this->assertEquals(new StringValue(['value' => 'VA\\nLUE']), AST::astFromValue("VA\nLUE", $this->myEnum())); - $this->assertEquals(new StringValue(['value' => '123']), AST::astFromValue("123", $this->myEnum())); + $this->assertEquals(new EnumValue(['value' => 'COMPLEX']), AST::astFromValue($this->complexValue(), $this->myEnum())); + + // Note: case sensitive + $this->assertEquals(null, AST::astFromValue('hello', $this->myEnum())); + + // Note: Not a valid enum value + $this->assertEquals(null, AST::astFromValue('VALUE', $this->myEnum())); } /** @@ -87,15 +112,15 @@ class ASTFromValueTest extends \PHPUnit_Framework_TestCase new StringValue(['value' => 'BAR']) ] ]); - $this->assertEquals($value1, AST::astFromValue(['FOO', 'BAR'])); + $this->assertEquals($value1, AST::astFromValue(['FOO', 'BAR'], Type::listOf(Type::string()))); $value2 = new ListValue([ 'values' => [ - new EnumValue(['value' => 'FOO']), - new EnumValue(['value' => 'BAR']), + new EnumValue(['value' => 'HELLO']), + new EnumValue(['value' => 'GOODBYE']), ] ]); - $this->assertEquals($value2, AST::astFromValue(['FOO', 'BAR'], Type::listOf($this->myEnum()))); + $this->assertEquals($value2, AST::astFromValue(['HELLO', 'GOODBYE'], Type::listOf($this->myEnum()))); } /** @@ -103,7 +128,7 @@ class ASTFromValueTest extends \PHPUnit_Framework_TestCase */ public function testConvertsListSingletons() { - $this->assertEquals(new EnumValue(['value' => 'FOO']), AST::astFromValue('FOO', Type::listOf($this->myEnum()))); + $this->assertEquals(new StringValue(['value' => 'FOO']), AST::astFromValue('FOO', Type::listOf(Type::string()))); } /** @@ -111,25 +136,35 @@ class ASTFromValueTest extends \PHPUnit_Framework_TestCase */ public function testConvertsInputObjects() { + $inputObj = new InputObjectType([ + 'name' => 'MyInputObj', + 'fields' => [ + 'foo' => Type::float(), + 'bar' => $this->myEnum() + ] + ]); + $expected = new ObjectValue([ 'fields' => [ - $this->objectField('foo', new IntValue(['value' => 3])), - $this->objectField('bar', new StringValue(['value' => 'HELLO'])) + $this->objectField('foo', new IntValue(['value' => '3'])), + $this->objectField('bar', new EnumValue(['value' => 'HELLO'])) ] ]); $data = ['foo' => 3, 'bar' => 'HELLO']; - $this->assertEquals($expected, AST::astFromValue($data)); - $this->assertEquals($expected, AST::astFromValue((object) $data)); + $this->assertEquals($expected, AST::astFromValue($data, $inputObj)); + $this->assertEquals($expected, AST::astFromValue((object) $data, $inputObj)); + } - $expected = new ObjectValue([ - 'fields' => [ - $this->objectField('foo', new FloatValue(['value' => '3.0'])), - $this->objectField('bar', new EnumValue(['value' => 'HELLO'])), - ] - ]); - $this->assertEquals($expected, AST::astFromValue($data, $this->inputObj())); - $this->assertEquals($expected, AST::astFromValue((object) $data, $this->inputObj())); + private $complexValue; + + private function complexValue() + { + if (!$this->complexValue) { + $this->complexValue = new \stdClass(); + $this->complexValue->someArbitrary = 'complexValue'; + } + return $this->complexValue; } /** @@ -142,20 +177,7 @@ class ASTFromValueTest extends \PHPUnit_Framework_TestCase 'values' => [ 'HELLO' => [], 'GOODBYE' => [], - ] - ]); - } - - /** - * @return InputObjectField - */ - private function inputObj() - { - return new InputObjectType([ - 'name' => 'MyInputObj', - 'fields' => [ - 'foo' => ['type' => Type::float()], - 'bar' => ['type' => $this->myEnum()] + 'COMPLEX' => ['value' => $this->complexValue()] ] ]); }