Preserve original coercion errors, improve error quality.

This is a fairly major refactoring of coerceValue which returns an Either so it can return a complete collection of errors. This allows originalError to be preserved for scalar coercion errors and ensures *all* errors are represented in the response.

This had a minor change to the logic in execute / subscribe to allow for buildExecutionContext to abrupt complete with multiple errors.

ref: graphql/graphql-js#1133
This commit is contained in:
Daniel Tschinder 2018-02-13 16:51:44 +01:00
parent 6d45a22ba4
commit 60df83f47e
21 changed files with 844 additions and 718 deletions

View File

@ -277,6 +277,14 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
return $this->locations; return $this->locations;
} }
/**
* @return array|Node[]|null
*/
public function getNodes()
{
return $this->nodes;
}
/** /**
* Returns an array describing the path from the root value to the field which produced this error. * Returns an array describing the path from the root value to the field which produced this error.
* Only included for execution errors. * Only included for execution errors.

View File

@ -100,9 +100,16 @@ class Executor
{ {
// TODO: deprecate (just always use SyncAdapter here) and have `promiseToExecute()` for other cases // TODO: deprecate (just always use SyncAdapter here) and have `promiseToExecute()` for other cases
$promiseAdapter = self::getPromiseAdapter(); $promiseAdapter = self::getPromiseAdapter();
$result = self::promiseToExecute(
$result = self::promiseToExecute($promiseAdapter, $schema, $ast, $rootValue, $contextValue, $promiseAdapter,
$variableValues, $operationName, $fieldResolver); $schema,
$ast,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver
);
// Wait for promised results when using sync promises // Wait for promised results when using sync promises
if ($promiseAdapter instanceof SyncPromiseAdapter) { if ($promiseAdapter instanceof SyncPromiseAdapter) {
@ -140,11 +147,19 @@ class Executor
callable $fieldResolver = null callable $fieldResolver = null
) )
{ {
try { $exeContext = self::buildExecutionContext(
$exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $contextValue, $variableValues, $schema,
$operationName, $fieldResolver, $promiseAdapter); $ast,
} catch (Error $e) { $rootValue,
return $promiseAdapter->createFulfilled(new ExecutionResult(null, [$e])); $contextValue,
$variableValues,
$operationName,
$fieldResolver,
$promiseAdapter
);
if (is_array($exeContext)) {
return $promiseAdapter->createFulfilled(new ExecutionResult(null, $exeContext));
} }
$executor = new self($exeContext); $executor = new self($exeContext);
@ -159,13 +174,12 @@ class Executor
* @param DocumentNode $documentNode * @param DocumentNode $documentNode
* @param $rootValue * @param $rootValue
* @param $contextValue * @param $contextValue
* @param $rawVariableValues * @param array|\Traversable $rawVariableValues
* @param string $operationName * @param string $operationName
* @param callable $fieldResolver * @param callable $fieldResolver
* @param PromiseAdapter $promiseAdapter * @param PromiseAdapter $promiseAdapter
* *
* @return ExecutionContext * @return ExecutionContext|Error[]
* @throws Error
*/ */
private static function buildExecutionContext( private static function buildExecutionContext(
Schema $schema, Schema $schema,
@ -178,30 +192,17 @@ class Executor
PromiseAdapter $promiseAdapter = null PromiseAdapter $promiseAdapter = null
) )
{ {
if (null !== $rawVariableValues) {
Utils::invariant(
is_array($rawVariableValues) || $rawVariableValues instanceof \ArrayAccess,
"Variable values are expected to be array or instance of ArrayAccess, got " . Utils::getVariableType($rawVariableValues)
);
}
if (null !== $operationName) {
Utils::invariant(
is_string($operationName),
"Operation name is supposed to be string, got " . Utils::getVariableType($operationName)
);
}
$errors = []; $errors = [];
$fragments = []; $fragments = [];
/** @var OperationDefinitionNode $operation */
$operation = null; $operation = null;
$hasMultipleAssumedOperations = false;
foreach ($documentNode->definitions as $definition) { foreach ($documentNode->definitions as $definition) {
switch ($definition->kind) { switch ($definition->kind) {
case NodeKind::OPERATION_DEFINITION: case NodeKind::OPERATION_DEFINITION:
if (!$operationName && $operation) { if (!$operationName && $operation) {
throw new Error( $hasMultipleAssumedOperations = true;
'Must provide operation name if query contains multiple operations.'
);
} }
if (!$operationName || if (!$operationName ||
(isset($definition->name) && $definition->name->value === $operationName)) { (isset($definition->name) && $definition->name->value === $operationName)) {
@ -216,19 +217,40 @@ class Executor
if (!$operation) { if (!$operation) {
if ($operationName) { if ($operationName) {
throw new Error("Unknown operation named \"$operationName\"."); $errors[] = new Error("Unknown operation named \"$operationName\".");
} else { } else {
throw new Error('Must provide an operation.'); $errors[] = new Error('Must provide an operation.');
}
} else if ($hasMultipleAssumedOperations) {
$errors[] = new Error(
'Must provide operation name if query contains multiple operations.'
);
}
$variableValues = null;
if ($operation) {
$coercedVariableValues = Values::getVariableValues(
$schema,
$operation->variableDefinitions ?: [],
$rawVariableValues ?: []
);
if ($coercedVariableValues['errors']) {
$errors = array_merge($errors, $coercedVariableValues['errors']);
} else {
$variableValues = $coercedVariableValues['coerced'];
} }
} }
$variableValues = Values::getVariableValues( if ($errors) {
$schema, return $errors;
$operation->variableDefinitions ?: [], }
$rawVariableValues ?: []
);
$exeContext = new ExecutionContext( Utils::invariant($operation, 'Has operation if no errors.');
Utils::invariant($variableValues !== null, 'Has variables if no errors.');
return new ExecutionContext(
$schema, $schema,
$fragments, $fragments,
$rootValue, $rootValue,
@ -239,7 +261,6 @@ class Executor
$fieldResolver ?: self::$defaultFieldResolver, $fieldResolver ?: self::$defaultFieldResolver,
$promiseAdapter ?: self::getPromiseAdapter() $promiseAdapter ?: self::getPromiseAdapter()
); );
return $exeContext;
} }
/** /**

View File

@ -26,6 +26,7 @@ use GraphQL\Type\Definition\Type;
use GraphQL\Utils\AST; use GraphQL\Utils\AST;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use GraphQL\Utils\Value;
use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\DocumentValidator;
class Values class Values
@ -36,56 +37,62 @@ class Values
* to match the variable definitions, a Error will be thrown. * to match the variable definitions, a Error will be thrown.
* *
* @param Schema $schema * @param Schema $schema
* @param VariableDefinitionNode[] $definitionNodes * @param VariableDefinitionNode[] $varDefNodes
* @param array $inputs * @param array $inputs
* @return array * @return array
* @throws Error
*/ */
public static function getVariableValues(Schema $schema, $definitionNodes, array $inputs) public static function getVariableValues(Schema $schema, $varDefNodes, array $inputs)
{ {
$errors = [];
$coercedValues = []; $coercedValues = [];
foreach ($definitionNodes as $definitionNode) { foreach ($varDefNodes as $varDefNode) {
$varName = $definitionNode->variable->name->value; $varName = $varDefNode->variable->name->value;
$varType = TypeInfo::typeFromAST($schema, $definitionNode->type); /** @var InputType|Type $varType */
$varType = TypeInfo::typeFromAST($schema, $varDefNode->type);
if (!Type::isInputType($varType)) { if (!Type::isInputType($varType)) {
throw new Error( $errors[] = new Error(
'Variable "$'.$varName.'" expected value of type ' . "Variable \"\$$varName\" expected value of type " .
'"' . Printer::doPrint($definitionNode->type) . '" which cannot be used as an input type.', '"' . Printer::doPrint($varDefNode->type) . '" which cannot be used as an input type.',
[$definitionNode->type] [$varDefNode->type]
); );
}
if (!array_key_exists($varName, $inputs)) {
$defaultValue = $definitionNode->defaultValue;
if ($defaultValue) {
$coercedValues[$varName] = AST::valueFromAST($defaultValue, $varType);
}
if ($varType instanceof NonNull) {
throw new Error(
'Variable "$'.$varName .'" of required type ' .
'"'. Utils::printSafe($varType) . '" was not provided.',
[$definitionNode]
);
}
} else { } else {
$value = $inputs[$varName]; if (!array_key_exists($varName, $inputs)) {
$errors = self::isValidPHPValue($value, $varType); if ($varType instanceof NonNull) {
if (!empty($errors)) { $errors[] = new Error(
$message = "\n" . implode("\n", $errors); "Variable \"\$$varName\" of required type " .
throw new Error( "\"{$varType}\" was not provided.",
'Variable "$' . $varName . '" got invalid value ' . [$varDefNode]
json_encode($value) . '.' . $message, );
[$definitionNode] } else if ($varDefNode->defaultValue) {
); $coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType);
} }
} else {
$value = $inputs[$varName];
$coerced = Value::coerceValue($value, $varType, $varDefNode);
/** @var Error[] $coercionErrors */
$coercionErrors = $coerced['errors'];
if ($coercionErrors) {
$messagePrelude = "Variable \"\$$varName\" got invalid value " . Utils::printSafeJson($value) . '; ';
$coercedValue = self::coerceValue($varType, $value); foreach($coercionErrors as $error) {
Utils::invariant($coercedValue !== Utils::undefined(), 'Should have reported error.'); $errors[] = new Error(
$coercedValues[$varName] = $coercedValue; $messagePrelude . $error->getMessage(),
$error->getNodes(),
$error->getSource(),
$error->getPositions(),
$error->getPath(),
$error,
$error->getExtensions()
);
}
} else {
$coercedValues[$varName] = $coerced['value'];
}
}
} }
} }
return $coercedValues; return ['errors' => $errors, 'coerced' => $errors ? null : $coercedValues];
} }
/** /**
@ -206,203 +213,16 @@ class Values
} }
/** /**
* Given a PHP value and a GraphQL type, determine if the value will be * @deprecated as of 0.12 (Use coerceValue() directly for richer information)
* accepted for that type. This is primarily useful for validating the
* runtime values of query variables.
*
* @param $value * @param $value
* @param InputType $type * @param InputType $type
* @return array * @return array
*/ */
public static function isValidPHPValue($value, InputType $type) public static function isValidPHPValue($value, InputType $type)
{ {
// A value must be provided if the type is non-null. $errors = Value::coerceValue($value, $type)['errors'];
if ($type instanceof NonNull) { return $errors
if (null === $value) { ? array_map(function(/*\Throwable */$error) { return $error->getMessage(); }, $errors)
return ['Expected "' . Utils::printSafe($type) . '", found null.']; : [];
}
return self::isValidPHPValue($value, $type->getWrappedType());
}
if (null === $value) {
return [];
}
// Lists accept a non-list value as a list of one.
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if (is_array($value)) {
$tmp = [];
foreach ($value as $index => $item) {
$errors = self::isValidPHPValue($item, $itemType);
$tmp = array_merge($tmp, Utils::map($errors, function ($error) use ($index) {
return "In element #$index: $error";
}));
}
return $tmp;
}
return self::isValidPHPValue($value, $itemType);
}
// Input objects check each defined field.
if ($type instanceof InputObjectType) {
if (!is_object($value) && !is_array($value)) {
return ["Expected \"{$type->name}\", found not an object."];
}
$fields = $type->getFields();
$errors = [];
// Ensure every provided field is defined.
$props = is_object($value) ? get_object_vars($value) : $value;
foreach ($props as $providedField => $tmp) {
if (!isset($fields[$providedField])) {
$errors[] = "In field \"{$providedField}\": Unknown field.";
}
}
// Ensure every defined field is valid.
foreach ($fields as $fieldName => $tmp) {
$newErrors = self::isValidPHPValue(isset($value[$fieldName]) ? $value[$fieldName] : null, $fields[$fieldName]->getType());
$errors = array_merge(
$errors,
Utils::map($newErrors, function ($error) use ($fieldName) {
return "In field \"{$fieldName}\": {$error}";
})
);
}
return $errors;
}
if ($type instanceof EnumType) {
if (!is_string($value) || !$type->getValue($value)) {
$printed = Utils::printSafeJson($value);
return ["Expected type \"{$type->name}\", found $printed."];
}
return [];
}
Utils::invariant($type instanceof ScalarType, 'Must be a scalar type');
/** @var ScalarType $type */
// Scalars determine if a value is valid via parseValue().
try {
$parseResult = $type->parseValue($value);
if (Utils::isInvalid($parseResult)) {
$printed = Utils::printSafeJson($value);
return [
"Expected type \"{$type->name}\", found $printed."
];
}
} catch (\Exception $error) {
$printed = Utils::printSafeJson($value);
$message = $error->getMessage();
return ["Expected type \"{$type->name}\", found $printed; $message"];
} catch (\Throwable $error) {
$printed = Utils::printSafeJson($value);
$message = $error->getMessage();
return ["Expected type \"{$type->name}\", found $printed; $message"];
}
return [];
}
/**
* Given a type and any value, return a runtime value coerced to match the type.
*/
private static function coerceValue(Type $type, $value)
{
$undefined = Utils::undefined();
if ($value === $undefined) {
return $undefined;
}
if ($type instanceof NonNull) {
if ($value === null) {
// Intentionally return no value.
return $undefined;
}
return self::coerceValue($type->getWrappedType(), $value);
}
if (null === $value) {
return null;
}
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if (is_array($value) || $value instanceof \Traversable) {
$coercedValues = [];
foreach ($value as $item) {
$itemValue = self::coerceValue($itemType, $item);
if ($undefined === $itemValue) {
// Intentionally return no value.
return $undefined;
}
$coercedValues[] = $itemValue;
}
return $coercedValues;
} else {
$coercedValue = self::coerceValue($itemType, $value);
if ($coercedValue === $undefined) {
// Intentionally return no value.
return $undefined;
}
return [$coercedValue];
}
}
if ($type instanceof InputObjectType) {
$coercedObj = [];
$fields = $type->getFields();
foreach ($fields as $fieldName => $field) {
if (!array_key_exists($fieldName, $value)) {
if ($field->defaultValueExists()) {
$coercedObj[$fieldName] = $field->defaultValue;
} else if ($field->getType() instanceof NonNull) {
// Intentionally return no value.
return $undefined;
}
continue;
}
$fieldValue = self::coerceValue($field->getType(), $value[$fieldName]);
if ($fieldValue === $undefined) {
// Intentionally return no value.
return $undefined;
}
$coercedObj[$fieldName] = $fieldValue;
}
return $coercedObj;
}
if ($type instanceof EnumType) {
if (!is_string($value) || !$type->getValue($value)) {
return $undefined;
}
$enumValue = $type->getValue($value);
if (!$enumValue) {
return $undefined;
}
return $enumValue->value;
}
Utils::invariant($type instanceof ScalarType, 'Must be a scalar type');
/** @var ScalarType $type */
// Scalars determine if a value is valid via parseValue().
try {
$parseResult = $type->parseValue($value);
if (Utils::isInvalid($parseResult)) {
return $undefined;
}
} catch (\Exception $error) {
return $undefined;
} catch (\Throwable $error) {
return $undefined;
}
return $parseResult;
} }
} }

View File

@ -1,7 +1,7 @@
<?php <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\Error;
use GraphQL\Language\AST\FloatValueNode; use GraphQL\Language\AST\FloatValueNode;
use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\IntValueNode;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -28,28 +28,21 @@ values as specified by
/** /**
* @param mixed $value * @param mixed $value
* @return float|null * @return float|null
* @throws Error
*/ */
public function serialize($value) public function serialize($value)
{ {
if (is_numeric($value) || $value === true || $value === false) { return $this->coerceFloat($value);
return (float) $value;
}
if ($value === '') {
$err = 'Float cannot represent non numeric value: (empty string)';
} else {
$err = sprintf('Float cannot represent non numeric value: %s', Utils::printSafe($value));
}
throw new InvariantViolation($err);
} }
/** /**
* @param mixed $value * @param mixed $value
* @return float|null * @return float|null
* @throws Error
*/ */
public function parseValue($value) public function parseValue($value)
{ {
return (is_numeric($value) && !is_string($value)) ? (float) $value : Utils::undefined(); return $this->coerceFloat($value);
} }
/** /**
@ -64,4 +57,21 @@ values as specified by
} }
return Utils::undefined(); return Utils::undefined();
} }
private function coerceFloat($value) {
if ($value === '') {
throw new Error(
'Float cannot represent non numeric value: (empty string)'
);
}
if (!is_numeric($value) && $value !== true && $value !== false) {
throw new Error(
'Float cannot represent non numeric value: ' .
Utils::printSafe($value)
);
}
return (float) $value;
}
} }

View File

@ -1,7 +1,7 @@
<?php <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\Error;
use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\IntValueNode;
use GraphQL\Language\AST\StringValueNode; use GraphQL\Language\AST\StringValueNode;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -43,7 +43,7 @@ When expected as an input type, any string (such as `"4"`) or integer
return 'null'; return 'null';
} }
if (!is_scalar($value) && (!is_object($value) || !method_exists($value, '__toString'))) { if (!is_scalar($value) && (!is_object($value) || !method_exists($value, '__toString'))) {
throw new InvariantViolation("ID type cannot represent non scalar value: " . Utils::printSafe($value)); throw new Error("ID type cannot represent non scalar value: " . Utils::printSafe($value));
} }
return (string) $value; return (string) $value;
} }

View File

@ -1,7 +1,7 @@
<?php <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\Error;
use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\IntValueNode;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -34,49 +34,21 @@ values. Int can represent values between -(2^31) and 2^31 - 1. ';
/** /**
* @param mixed $value * @param mixed $value
* @return int|null * @return int|null
* @throws Error
*/ */
public function serialize($value) public function serialize($value)
{ {
if ($value === '') { return $this->coerceInt($value);
throw new InvariantViolation('Int cannot represent non 32-bit signed integer value: (empty string)');
}
if (false === $value || true === $value) {
return (int) $value;
}
if (!is_numeric($value) || $value > self::MAX_INT || $value < self::MIN_INT) {
throw new InvariantViolation(sprintf(
'Int cannot represent non 32-bit signed integer value: %s',
Utils::printSafe($value)
));
}
$num = (float) $value;
// The GraphQL specification does not allow serializing non-integer values
// as Int to avoid accidental data loss.
// Examples: 1.0 == 1; 1.1 != 1, etc
if ($num != (int) $value) {
// Additionally account for scientific notation (i.e. 1e3), because (float)'1e3' is 1000, but (int)'1e3' is 1
$trimmed = floor($num);
if ($trimmed !== $num) {
throw new InvariantViolation(sprintf(
'Int cannot represent non-integer value: %s',
Utils::printSafe($value)
));
}
}
return (int) $value;
} }
/** /**
* @param mixed $value * @param mixed $value
* @return int|null * @return int|null
* @throws Error
*/ */
public function parseValue($value) public function parseValue($value)
{ {
// Below is a fix against PHP bug where (in some combinations of OSs and versions) return $this->coerceInt($value);
// boundary values are treated as "double" vs "integer" and failing is_int() check
$isInt = is_int($value) || $value === self::MIN_INT || $value === self::MAX_INT;
return $isInt && $value <= self::MAX_INT && $value >= self::MIN_INT ? $value : Utils::undefined();
} }
/** /**
@ -94,4 +66,28 @@ values. Int can represent values between -(2^31) and 2^31 - 1. ';
} }
return Utils::undefined(); return Utils::undefined();
} }
private function coerceInt($value) {
if ($value === '') {
throw new Error(
'Int cannot represent non 32-bit signed integer value: (empty string)'
);
}
$num = floatval($value);
if (!is_numeric($value) && !is_bool($value) || $num > self::MAX_INT || $num < self::MIN_INT) {
throw new Error(
'Int cannot represent non 32-bit signed integer value: ' .
Utils::printSafe($value)
);
}
$int = intval($num);
if ($int != $num) {
throw new Error(
'Int cannot represent non-integer value: ' .
Utils::printSafe($value)
);
}
return $int;
}
} }

View File

@ -1,7 +1,7 @@
<?php <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\Error;
use GraphQL\Language\AST\StringValueNode; use GraphQL\Language\AST\StringValueNode;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -27,6 +27,7 @@ represent free-form human-readable text.';
/** /**
* @param mixed $value * @param mixed $value
* @return mixed|string * @return mixed|string
* @throws Error
*/ */
public function serialize($value) public function serialize($value)
{ {
@ -40,18 +41,19 @@ represent free-form human-readable text.';
return 'null'; return 'null';
} }
if (!is_scalar($value)) { if (!is_scalar($value)) {
throw new InvariantViolation("String cannot represent non scalar value: " . Utils::printSafe($value)); throw new Error("String cannot represent non scalar value: " . Utils::printSafe($value));
} }
return (string) $value; return $this->coerceString($value);
} }
/** /**
* @param mixed $value * @param mixed $value
* @return string * @return string
* @throws Error
*/ */
public function parseValue($value) public function parseValue($value)
{ {
return is_string($value) ? $value : Utils::undefined(); return $this->coerceString($value);
} }
/** /**
@ -66,4 +68,15 @@ represent free-form human-readable text.';
} }
return Utils::undefined(); return Utils::undefined();
} }
private function coerceString($value) {
if (is_array($value)) {
throw new Error(
'String cannot represent an array value: ' .
Utils::printSafe($value)
);
}
return (string) $value;
}
} }

View File

@ -269,22 +269,7 @@ class Utils
$var = (array) $var; $var = (array) $var;
} }
if (is_array($var)) { if (is_array($var)) {
$count = count($var); return json_encode($var);
if (!isset($var[0]) && $count > 0) {
$keys = [];
$keyCount = 0;
foreach ($var as $key => $value) {
$keys[] = '"' . $key . '"';
if ($keyCount++ > 4) {
break;
}
}
$keysLabel = $keyCount === 1 ? 'key' : 'keys';
$msg = "object with first $keysLabel: " . implode(', ', $keys);
} else {
$msg = "array($count)";
}
return $msg;
} }
if ('' === $var) { if ('' === $var) {
return '(empty string)'; return '(empty string)';
@ -296,7 +281,7 @@ class Utils
return 'false'; return 'false';
} }
if (true === $var) { if (true === $var) {
return 'false'; return 'true';
} }
if (is_string($var)) { if (is_string($var)) {
return "\"$var\""; return "\"$var\"";
@ -320,22 +305,7 @@ class Utils
return 'instance of ' . get_class($var); return 'instance of ' . get_class($var);
} }
if (is_array($var)) { if (is_array($var)) {
$count = count($var); return json_encode($var);
if (!isset($var[0]) && $count > 0) {
$keys = [];
$keyCount = 0;
foreach ($var as $key => $value) {
$keys[] = '"' . $key . '"';
if ($keyCount++ > 4) {
break;
}
}
$keysLabel = $keyCount === 1 ? 'key' : 'keys';
$msg = "associative array($count) with first $keysLabel: " . implode(', ', $keys);
} else {
$msg = "array($count)";
}
return $msg;
} }
if ('' === $var) { if ('' === $var) {
return '(empty string)'; return '(empty string)';
@ -350,7 +320,7 @@ class Utils
return 'true'; return 'true';
} }
if (is_string($var)) { if (is_string($var)) {
return "\"$var\""; return $var;
} }
if (is_scalar($var)) { if (is_scalar($var)) {
return (string) $var; return (string) $var;

224
src/Utils/Value.php Normal file
View File

@ -0,0 +1,224 @@
<?php
namespace GraphQL\Utils;
use GraphQL\Error\Error;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ScalarType;
/**
* Coerces a PHP value given a GraphQL Type.
*
* Returns either a value which is valid for the provided type or a list of
* encountered coercion errors.
*/
class Value
{
/**
* Given a type and any value, return a runtime value coerced to match the type.
*/
public static function coerceValue($value, InputType $type, $blameNode = null, array $path = null)
{
if ($type instanceof NonNull) {
if ($value === null) {
return self::ofErrors([
self::coercionError(
"Expected non-nullable type $type",
$blameNode,
$path
),
]);
}
return self::coerceValue($value, $type->getWrappedType(), $blameNode, $path);
}
if (null === $value) {
// Explicitly return the value null.
return self::ofValue(null);
}
if ($type instanceof ScalarType) {
// Scalars determine if a value is valid via parseValue(), which can
// throw to indicate failure. If it throws, maintain a reference to
// the original error.
try {
$parseResult = $type->parseValue($value);
if (Utils::isInvalid($parseResult)) {
return self::ofErrors([
self::coercionError("Expected type {$type->name}", $blameNode, $path),
]);
}
return self::ofValue($parseResult);
} catch (\Exception $error) {
return self::ofErrors([
self::coercionError("Expected type {$type->name}", $blameNode, $path, $error),
]);
} catch (\Throwable $error) {
return self::ofErrors([
self::coercionError("Expected type {$type->name}", $blameNode, $path, $error),
]);
}
}
if ($type instanceof EnumType) {
if (is_string($value)) {
$enumValue = $type->getValue($value);
if ($enumValue) {
return self::ofValue($enumValue->value);
}
}
return self::ofErrors([
self::coercionError("Expected type {$type->name}", $blameNode, $path),
]);
}
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if (is_array($value) || $value instanceof \Traversable) {
$errors = [];
$coercedValue = [];
foreach ($value as $index => $itemValue) {
$coercedItem = self::coerceValue(
$itemValue,
$itemType,
$blameNode,
self::atPath($path, $index)
);
if ($coercedItem['errors']) {
$errors = self::add($errors, $coercedItem['errors']);
} else {
$coercedValue[] = $coercedItem['value'];
}
}
return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
}
// Lists accept a non-list value as a list of one.
$coercedItem = self::coerceValue($value, $itemType, $blameNode);
return $coercedItem['errors'] ? $coercedItem : self::ofValue([$coercedItem['value']]);
}
if ($type instanceof InputObjectType) {
if (!is_object($value) && !is_array($value) && !$value instanceof \Traversable) {
return self::ofErrors([
self::coercionError("Expected object type {$type->name}", $blameNode, $path),
]);
}
$errors = [];
$coercedValue = [];
$fields = $type->getFields();
foreach ($fields as $fieldName => $field) {
if (!array_key_exists($fieldName, $value)) {
if ($field->defaultValueExists()) {
$coercedValue[$fieldName] = $field->defaultValue;
} else if ($field->getType() instanceof NonNull) {
$fieldPath = self::printPath(self::atPath($path, $fieldName));
$errors = self::add(
$errors,
self::coercionError(
"Field {$fieldPath} of required " .
"type {$field->type} was not provided",
$blameNode
)
);
}
} else {
$fieldValue = $value[$fieldName];
$coercedField = self::coerceValue(
$fieldValue,
$field->getType(),
$blameNode,
self::atPath($path, $fieldName)
);
if ($coercedField['errors']) {
$errors = self::add($errors, $coercedField['errors']);
} else {
$coercedValue[$fieldName] = $coercedField['value'];
}
}
}
// Ensure every provided field is defined.
foreach ($value as $fieldName => $field) {
if (!array_key_exists($fieldName, $fields)) {
$errors = self::add(
$errors,
self::coercionError(
"Field \"{$fieldName}\" is not defined by type {$type->name}",
$blameNode,
$path
)
);
}
}
return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
}
throw new Error("Unexpected type {$type}");
}
private static function ofValue($value) {
return ['errors' => null, 'value' => $value];
}
private static function ofErrors($errors) {
return ['errors' => $errors, 'value' => Utils::undefined()];
}
private static function add($errors, $moreErrors) {
return array_merge($errors, is_array($moreErrors) ? $moreErrors : [$moreErrors]);
}
private static function atPath($prev, $key) {
return ['prev' => $prev, 'key' => $key];
}
/**
* @param string $message
* @param Node $blameNode
* @param array|null $path
* @param \Exception|\Throwable|null $originalError
* @return Error
*/
private static function coercionError($message, $blameNode, array $path = null, $originalError = null) {
$pathStr = self::printPath($path);
// Return a GraphQLError instance
return new Error(
$message .
($pathStr ? ' at ' . $pathStr : '') .
($originalError && $originalError->getMessage()
? '; ' . $originalError->getMessage()
: '.'),
$blameNode,
null,
null,
null,
$originalError
);
}
/**
* Build a string describing the path into the value where the error was found
*
* @param $path
* @return string
*/
private static function printPath(array $path = null) {
$pathStr = '';
$currentPath = $path;
while($currentPath) {
$pathStr =
(is_string($currentPath['key'])
? '.' . $currentPath['key']
: '[' . $currentPath['key'] . ']') . $pathStr;
$currentPath = $currentPath['prev'];
}
return $pathStr ? 'value' . $pathStr : '';
}
}

View File

@ -203,11 +203,21 @@ class QueryComplexity extends AbstractQuerySecurity
$args = []; $args = [];
if ($fieldDef instanceof FieldDefinition) { if ($fieldDef instanceof FieldDefinition) {
$variableValues = Values::getVariableValues( $variableValuesResult = Values::getVariableValues(
$this->context->getSchema(), $this->context->getSchema(),
$this->variableDefs, $this->variableDefs,
$rawVariableValues $rawVariableValues
); );
if ($variableValuesResult['errors']) {
throw new Error(implode("\n\n", array_map(
function ($error) {
return $error->getMessage();
}
, $variableValuesResult['errors'])));
}
$variableValues = $variableValuesResult['coerced'];
$args = Values::getArgumentValues($fieldDef, $node, $variableValues); $args = Values::getArgumentValues($fieldDef, $node, $variableValues);
} }
@ -220,12 +230,21 @@ class QueryComplexity extends AbstractQuerySecurity
return false; return false;
} }
$variableValues = Values::getVariableValues( $variableValuesResult = Values::getVariableValues(
$this->context->getSchema(), $this->context->getSchema(),
$this->variableDefs, $this->variableDefs,
$this->getRawVariableValues() $this->getRawVariableValues()
); );
if ($variableValuesResult['errors']) {
throw new Error(implode("\n\n", array_map(
function ($error) {
return $error->getMessage();
}
, $variableValuesResult['errors'])));
}
$variableValues = $variableValuesResult['coerced'];
if ($directiveNode->name->value === 'include') { if ($directiveNode->name->value === 'include') {
$directive = Directive::includeDirective(); $directive = Directive::includeDirective();
$directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues); $directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);

View File

@ -12,197 +12,162 @@ use GraphQL\Type\Schema;
class ValuesTest extends \PHPUnit_Framework_TestCase { class ValuesTest extends \PHPUnit_Framework_TestCase {
public function testGetIDVariableValues() public function testGetIDVariableValues()
{ {
$this->expectInputVariablesMatchOutputVariables(['idInput' => '123456789']); $this->expectInputVariablesMatchOutputVariables(['idInput' => '123456789']);
$this->assertEquals( $this->assertEquals(
['idInput' => '123456789'], ['errors'=> [], 'coerced' => ['idInput' => '123456789']],
self::runTestCase(['idInput' => 123456789]), self::runTestCase(['idInput' => 123456789]),
'Integer ID was not converted to string' 'Integer ID was not converted to string'
); );
} }
public function testGetBooleanVariableValues() public function testGetBooleanVariableValues()
{ {
$this->expectInputVariablesMatchOutputVariables(['boolInput' => true]); $this->expectInputVariablesMatchOutputVariables(['boolInput' => true]);
$this->expectInputVariablesMatchOutputVariables(['boolInput' => false]); $this->expectInputVariablesMatchOutputVariables(['boolInput' => false]);
} }
public function testGetIntVariableValues() public function testGetIntVariableValues()
{ {
$this->expectInputVariablesMatchOutputVariables(['intInput' => -1]); $this->expectInputVariablesMatchOutputVariables(['intInput' => -1]);
$this->expectInputVariablesMatchOutputVariables(['intInput' => 0]); $this->expectInputVariablesMatchOutputVariables(['intInput' => 0]);
$this->expectInputVariablesMatchOutputVariables(['intInput' => 1]); $this->expectInputVariablesMatchOutputVariables(['intInput' => 1]);
// Test the int size limit // Test the int size limit
$this->expectInputVariablesMatchOutputVariables(['intInput' => 2147483647]); $this->expectInputVariablesMatchOutputVariables(['intInput' => 2147483647]);
$this->expectInputVariablesMatchOutputVariables(['intInput' => -2147483648]); $this->expectInputVariablesMatchOutputVariables(['intInput' => -2147483648]);
} }
public function testGetStringVariableValues() public function testGetStringVariableValues()
{ {
$this->expectInputVariablesMatchOutputVariables(['stringInput' => 'meow']); $this->expectInputVariablesMatchOutputVariables(['stringInput' => 'meow']);
$this->expectInputVariablesMatchOutputVariables(['stringInput' => '']); $this->expectInputVariablesMatchOutputVariables(['stringInput' => '']);
$this->expectInputVariablesMatchOutputVariables(['stringInput' => '1']); $this->expectInputVariablesMatchOutputVariables(['stringInput' => '1']);
$this->expectInputVariablesMatchOutputVariables(['stringInput' => '0']); $this->expectInputVariablesMatchOutputVariables(['stringInput' => '0']);
$this->expectInputVariablesMatchOutputVariables(['stringInput' => 'false']); $this->expectInputVariablesMatchOutputVariables(['stringInput' => 'false']);
$this->expectInputVariablesMatchOutputVariables(['stringInput' => '1.2']); $this->expectInputVariablesMatchOutputVariables(['stringInput' => '1.2']);
} }
public function testGetFloatVariableValues() public function testGetFloatVariableValues()
{ {
$this->expectInputVariablesMatchOutputVariables(['floatInput' => 1.2]); $this->expectInputVariablesMatchOutputVariables(['floatInput' => 1.2]);
$this->expectInputVariablesMatchOutputVariables(['floatInput' => 1.0]); $this->expectInputVariablesMatchOutputVariables(['floatInput' => 1.0]);
$this->expectInputVariablesMatchOutputVariables(['floatInput' => 1]); $this->expectInputVariablesMatchOutputVariables(['floatInput' => 1]);
$this->expectInputVariablesMatchOutputVariables(['floatInput' => 0]); $this->expectInputVariablesMatchOutputVariables(['floatInput' => 0]);
$this->expectInputVariablesMatchOutputVariables(['floatInput' => 1e3]); $this->expectInputVariablesMatchOutputVariables(['floatInput' => 1e3]);
} }
public function testBooleanForIDVariableThrowsError() public function testBooleanForIDVariableThrowsError()
{ {
$this->expectGraphQLError(['idInput' => true]); $this->expectGraphQLError(['idInput' => true]);
} }
public function testFloatForIDVariableThrowsError() public function testFloatForIDVariableThrowsError()
{ {
$this->expectGraphQLError(['idInput' => 1.0]); $this->expectGraphQLError(['idInput' => 1.0]);
} }
public function testStringForBooleanVariableThrowsError() public function testStringForBooleanVariableThrowsError()
{ {
$this->expectGraphQLError(['boolInput' => 'true']); $this->expectGraphQLError(['boolInput' => 'true']);
} }
public function testIntForBooleanVariableThrowsError() public function testIntForBooleanVariableThrowsError()
{ {
$this->expectGraphQLError(['boolInput' => 1]); $this->expectGraphQLError(['boolInput' => 1]);
} }
public function testFloatForBooleanVariableThrowsError() public function testFloatForBooleanVariableThrowsError()
{ {
$this->expectGraphQLError(['boolInput' => 1.0]); $this->expectGraphQLError(['boolInput' => 1.0]);
} }
public function testBooleanForIntVariableThrowsError() public function testStringForIntVariableThrowsError()
{ {
$this->expectGraphQLError(['intInput' => true]); $this->expectGraphQLError(['intInput' => 'true']);
} }
public function testStringForIntVariableThrowsError() public function testPositiveBigIntForIntVariableThrowsError()
{ {
$this->expectGraphQLError(['intInput' => 'true']); $this->expectGraphQLError(['intInput' => 2147483648]);
} }
public function testFloatForIntVariableThrowsError() public function testNegativeBigIntForIntVariableThrowsError()
{ {
$this->expectGraphQLError(['intInput' => 1.0]); $this->expectGraphQLError(['intInput' => -2147483649]);
} }
public function testPositiveBigIntForIntVariableThrowsError() // Helpers for running test cases and making assertions
{
$this->expectGraphQLError(['intInput' => 2147483648]);
}
public function testNegativeBigIntForIntVariableThrowsError() private function expectInputVariablesMatchOutputVariables($variables)
{ {
$this->expectGraphQLError(['intInput' => -2147483649]); $this->assertEquals(
} $variables,
self::runTestCase($variables)['coerced'],
'Output variables did not match input variables' . PHP_EOL . var_export($variables, true) . PHP_EOL
);
}
public function testBooleanForStringVariableThrowsError() private function expectGraphQLError($variables)
{ {
$this->expectGraphQLError(['stringInput' => true]); $result = self::runTestCase($variables);
} $this->assertGreaterThan(0, count($result['errors']));
}
public function testIntForStringVariableThrowsError() private static $schema;
{
$this->expectGraphQLError(['stringInput' => 1]);
}
public function testFloatForStringVariableThrowsError() private static function getSchema()
{ {
$this->expectGraphQLError(['stringInput' => 1.0]); if (!self::$schema) {
} self::$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'test' => [
'type' => Type::boolean(),
'args' => [
'idInput' => Type::id(),
'boolInput' => Type::boolean(),
'intInput' => Type::int(),
'stringInput' => Type::string(),
'floatInput' => Type::float()
]
],
]
])
]);
}
return self::$schema;
}
public function testBooleanForFloatVariableThrowsError() private static function getVariableDefinitionNodes()
{ {
$this->expectGraphQLError(['floatInput' => true]); $idInputDefinition = new VariableDefinitionNode([
} 'variable' => new VariableNode(['name' => new NameNode(['value' => 'idInput'])]),
'type' => new NamedTypeNode(['name' => new NameNode(['value' => 'ID'])])
]);
$boolInputDefinition = new VariableDefinitionNode([
'variable' => new VariableNode(['name' => new NameNode(['value' => 'boolInput'])]),
'type' => new NamedTypeNode(['name' => new NameNode(['value' => 'Boolean'])])
]);
$intInputDefinition = new VariableDefinitionNode([
'variable' => new VariableNode(['name' => new NameNode(['value' => 'intInput'])]),
'type' => new NamedTypeNode(['name' => new NameNode(['value' => 'Int'])])
]);
$stringInputDefintion = new VariableDefinitionNode([
'variable' => new VariableNode(['name' => new NameNode(['value' => 'stringInput'])]),
'type' => new NamedTypeNode(['name' => new NameNode(['value' => 'String'])])
]);
$floatInputDefinition = new VariableDefinitionNode([
'variable' => new VariableNode(['name' => new NameNode(['value' => 'floatInput'])]),
'type' => new NamedTypeNode(['name' => new NameNode(['value' => 'Float'])])
]);
return [$idInputDefinition, $boolInputDefinition, $intInputDefinition, $stringInputDefintion, $floatInputDefinition];
}
public function testStringForFloatVariableThrowsError() private function runTestCase($variables)
{ {
$this->expectGraphQLError(['floatInput' => '1.0']); return Values::getVariableValues(self::getSchema(), self::getVariableDefinitionNodes(), $variables);
} }
// Helpers for running test cases and making assertions
private function expectInputVariablesMatchOutputVariables($variables)
{
$this->assertEquals(
$variables,
self::runTestCase($variables),
'Output variables did not match input variables' . PHP_EOL . var_export($variables, true) . PHP_EOL
);
}
private function expectGraphQLError($variables)
{
$this->setExpectedException(\GraphQL\Error\Error::class);
self::runTestCase($variables);
}
private static $schema;
private static function getSchema()
{
if (!self::$schema) {
self::$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'test' => [
'type' => Type::boolean(),
'args' => [
'idInput' => Type::id(),
'boolInput' => Type::boolean(),
'intInput' => Type::int(),
'stringInput' => Type::string(),
'floatInput' => Type::float()
]
],
]
])
]);
}
return self::$schema;
}
private static function getVariableDefinitionNodes()
{
$idInputDefinition = new VariableDefinitionNode([
'variable' => new VariableNode(['name' => new NameNode(['value' => 'idInput'])]),
'type' => new NamedTypeNode(['name' => new NameNode(['value' => 'ID'])])
]);
$boolInputDefinition = new VariableDefinitionNode([
'variable' => new VariableNode(['name' => new NameNode(['value' => 'boolInput'])]),
'type' => new NamedTypeNode(['name' => new NameNode(['value' => 'Boolean'])])
]);
$intInputDefinition = new VariableDefinitionNode([
'variable' => new VariableNode(['name' => new NameNode(['value' => 'intInput'])]),
'type' => new NamedTypeNode(['name' => new NameNode(['value' => 'Int'])])
]);
$stringInputDefintion = new VariableDefinitionNode([
'variable' => new VariableNode(['name' => new NameNode(['value' => 'stringInput'])]),
'type' => new NamedTypeNode(['name' => new NameNode(['value' => 'String'])])
]);
$floatInputDefinition = new VariableDefinitionNode([
'variable' => new VariableNode(['name' => new NameNode(['value' => 'floatInput'])]),
'type' => new NamedTypeNode(['name' => new NameNode(['value' => 'Float'])])
]);
return [$idInputDefinition, $boolInputDefinition, $intInputDefinition, $stringInputDefintion, $floatInputDefinition];
}
private function runTestCase($variables)
{
return Values::getVariableValues(self::getSchema(), self::getVariableDefinitionNodes(), $variables);
}
} }

View File

@ -3,6 +3,7 @@ namespace GraphQL\Tests\Executor;
require_once __DIR__ . '/TestClasses.php'; require_once __DIR__ . '/TestClasses.php';
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\Executor; use GraphQL\Executor\Executor;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
@ -134,7 +135,7 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
]; ];
$this->assertEquals($expected, $result); $this->assertEquals($expected, $result);
// properly parses single value to array: // properly parses single value to list:
$params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']]; $params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']];
$this->assertEquals( $this->assertEquals(
['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']], ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']],
@ -158,13 +159,15 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
'errors' => [ 'errors' => [
[ [
'message' => 'message' =>
'Variable "$input" got invalid value {"a":"foo","b":"bar","c":null}.'. "\n". 'Variable "$input" got invalid value ' .
'In field "c": Expected "String!", found null.', '{"a":"foo","b":"bar","c":null}; ' .
'Expected non-nullable type String! at value.c.',
'locations' => [['line' => 2, 'column' => 17]], 'locations' => [['line' => 2, 'column' => 17]],
'category' => 'graphql' 'category' => 'graphql'
] ]
] ]
]; ];
$this->assertEquals($expected, $result->toArray()); $this->assertEquals($expected, $result->toArray());
// errors on incorrect type: // errors on incorrect type:
@ -174,8 +177,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
'errors' => [ 'errors' => [
[ [
'message' => 'message' =>
'Variable "$input" got invalid value "foo bar".' . "\n" . 'Variable "$input" got invalid value "foo bar"; ' .
'Expected "TestInputObject", found not an object.', 'Expected object type TestInputObject.',
'locations' => [ [ 'line' => 2, 'column' => 17 ] ], 'locations' => [ [ 'line' => 2, 'column' => 17 ] ],
'category' => 'graphql', 'category' => 'graphql',
] ]
@ -191,8 +194,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
'errors' => [ 'errors' => [
[ [
'message' => 'message' =>
'Variable "$input" got invalid value {"a":"foo","b":"bar"}.'. "\n". 'Variable "$input" got invalid value {"a":"foo","b":"bar"}; '.
'In field "c": Expected "String!", found null.', 'Field value.c of required type String! was not provided.',
'locations' => [['line' => 2, 'column' => 17]], 'locations' => [['line' => 2, 'column' => 17]],
'category' => 'graphql', 'category' => 'graphql',
] ]
@ -214,9 +217,15 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
'errors' => [ 'errors' => [
[ [
'message' => 'message' =>
'Variable "$input" got invalid value {"na":{"a":"foo"}}.' . "\n" . 'Variable "$input" got invalid value {"na":{"a":"foo"}}; ' .
'In field "na": In field "c": Expected "String!", found null.' . "\n" . 'Field value.na.c of required type String! was not provided.',
'In field "nb": Expected "String!", found null.', 'locations' => [['line' => 2, 'column' => 19]],
'category' => 'graphql',
],
[
'message' =>
'Variable "$input" got invalid value {"na":{"a":"foo"}}; ' .
'Field value.nb of required type String! was not provided.',
'locations' => [['line' => 2, 'column' => 19]], 'locations' => [['line' => 2, 'column' => 19]],
'category' => 'graphql', 'category' => 'graphql',
] ]
@ -226,14 +235,15 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
// errors on addition of unknown input field // errors on addition of unknown input field
$params = ['input' => [ 'a' => 'foo', 'b' => 'bar', 'c' => 'baz', 'd' => 'dog' ]]; $params = ['input' => [ 'a' => 'foo', 'b' => 'bar', 'c' => 'baz', 'extra' => 'dog' ]];
$result = Executor::execute($schema, $ast, null, null, $params); $result = Executor::execute($schema, $ast, null, null, $params);
$expected = [ $expected = [
'errors' => [ 'errors' => [
[ [
'message' => 'message' =>
'Variable "$input" got invalid value {"a":"foo","b":"bar","c":"baz","d":"dog"}.'."\n". 'Variable "$input" got invalid value ' .
'In field "d": Expected type "ComplexScalar", found "dog".', '{"a":"foo","b":"bar","c":"baz","extra":"dog"}; ' .
'Field "extra" is not defined by type TestInputObject.',
'locations' => [['line' => 2, 'column' => 17]], 'locations' => [['line' => 2, 'column' => 17]],
'category' => 'graphql', 'category' => 'graphql',
] ]
@ -401,8 +411,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
'errors' => [ 'errors' => [
[ [
'message' => 'message' =>
'Variable "$value" got invalid value null.' . "\n". 'Variable "$value" got invalid value null; ' .
'Expected "String!", found null.', 'Expected non-nullable type String!.',
'locations' => [['line' => 2, 'column' => 31]], 'locations' => [['line' => 2, 'column' => 31]],
'category' => 'graphql', 'category' => 'graphql',
] ]
@ -482,8 +492,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$expected = [ $expected = [
'errors' => [[ 'errors' => [[
'message' => 'message' =>
'Variable "$value" got invalid value [1,2,3].' . "\n" . 'Variable "$value" got invalid value [1,2,3]; Expected type ' .
'Expected type "String", found array(3).', 'String; String cannot represent an array value: [1,2,3]',
'category' => 'graphql', 'category' => 'graphql',
'locations' => [ 'locations' => [
['line' => 2, 'column' => 31] ['line' => 2, 'column' => 31]
@ -491,7 +501,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
]] ]]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, $variables)->toArray()); $result = Executor::execute($this->schema(), $ast, null, null, $variables)->toArray(true);
$this->assertEquals($expected, $result);
} }
/** /**
@ -500,8 +512,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
public function testSerializingAnArrayViaGraphQLStringThrowsTypeError() public function testSerializingAnArrayViaGraphQLStringThrowsTypeError()
{ {
$this->setExpectedException( $this->setExpectedException(
InvariantViolation::class, Error::class,
'String cannot represent non scalar value: array(3)' 'String cannot represent non scalar value: [1,2,3]'
); );
Type::string()->serialize([1, 2, 3]); Type::string()->serialize([1, 2, 3]);
} }
@ -601,8 +613,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
'errors' => [ 'errors' => [
[ [
'message' => 'message' =>
'Variable "$input" got invalid value null.' . "\n" . 'Variable "$input" got invalid value null; ' .
'Expected "[String]!", found null.', 'Expected non-nullable type [String]!.',
'locations' => [['line' => 2, 'column' => 17]], 'locations' => [['line' => 2, 'column' => 17]],
'category' => 'graphql', 'category' => 'graphql',
] ]
@ -623,7 +635,7 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['nnList' => '["A"]']]; $expected = ['data' => ['nnList' => '["A"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => 'A'])->toArray()); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A']])->toArray());
} }
/** /**
@ -670,7 +682,7 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['listNN' => '["A"]']]; $expected = ['data' => ['listNN' => '["A"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => 'A'])->toArray()); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A']])->toArray());
} }
/** /**
@ -689,8 +701,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
'errors' => [ 'errors' => [
[ [
'message' => 'message' =>
'Variable "$input" got invalid value ["A",null,"B"].' . "\n" . 'Variable "$input" got invalid value ["A",null,"B"]; ' .
'In element #1: Expected "String!", found null.', 'Expected non-nullable type String! at value[1].',
'locations' => [ ['line' => 2, 'column' => 17] ], 'locations' => [ ['line' => 2, 'column' => 17] ],
'category' => 'graphql', 'category' => 'graphql',
] ]
@ -715,8 +727,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
'errors' => [ 'errors' => [
[ [
'message' => 'message' =>
'Variable "$input" got invalid value null.' . "\n" . 'Variable "$input" got invalid value null; ' .
'Expected "[String!]!", found null.', 'Expected non-nullable type [String!]!.',
'locations' => [ ['line' => 2, 'column' => 17] ], 'locations' => [ ['line' => 2, 'column' => 17] ],
'category' => 'graphql', 'category' => 'graphql',
] ]
@ -756,8 +768,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
'errors' => [ 'errors' => [
[ [
'message' => 'message' =>
'Variable "$input" got invalid value ["A",null,"B"].'."\n". 'Variable "$input" got invalid value ["A",null,"B"]; ' .
'In element #1: Expected "String!", found null.', 'Expected non-nullable type String! at value[1].',
'locations' => [ ['line' => 2, 'column' => 17] ], 'locations' => [ ['line' => 2, 'column' => 17] ],
'category' => 'graphql', 'category' => 'graphql',
] ]

View File

@ -322,7 +322,7 @@ class QueryExecutionTest extends TestCase
$this->setExpectedException( $this->setExpectedException(
InvariantViolation::class, InvariantViolation::class,
'Persistent query loader must return query string or instance of GraphQL\Language\AST\DocumentNode '. 'Persistent query loader must return query string or instance of GraphQL\Language\AST\DocumentNode '.
'but got: associative array(1) with first key: "err"' 'but got: {"err":"err"}'
); );
$this->config->setPersistentQueryLoader(function($queryId, OperationParams $params) use (&$called) { $this->config->setPersistentQueryLoader(function($queryId, OperationParams $params) use (&$called) {
return ['err' => 'err']; return ['err' => 'err'];

View File

@ -70,7 +70,7 @@ class RequestValidationTest extends \PHPUnit_Framework_TestCase
$this->assertInputError( $this->assertInputError(
$parsedBody, $parsedBody,
'GraphQL Request parameter "query" must be string, but got object with first key: "t"' 'GraphQL Request parameter "query" must be string, but got {"t":"{my query}"}'
); );
} }
@ -82,7 +82,7 @@ class RequestValidationTest extends \PHPUnit_Framework_TestCase
$this->assertInputError( $this->assertInputError(
$parsedBody, $parsedBody,
'GraphQL Request parameter "queryId" must be string, but got object with first key: "t"' 'GraphQL Request parameter "queryId" must be string, but got {"t":"{my query}"}'
); );
} }
@ -95,7 +95,7 @@ class RequestValidationTest extends \PHPUnit_Framework_TestCase
$this->assertInputError( $this->assertInputError(
$parsedBody, $parsedBody,
'GraphQL Request parameter "operation" must be string, but got array(0)' 'GraphQL Request parameter "operation" must be string, but got []'
); );
} }

View File

@ -235,7 +235,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
'{ colorEnum(fromString: "GREEN") }', '{ colorEnum(fromString: "GREEN") }',
null, null,
[ [
'message' => 'Expected a value of type "Color" but received: "GREEN"', 'message' => 'Expected a value of type "Color" but received: GREEN',
'locations' => [new SourceLocation(1, 3)] 'locations' => [new SourceLocation(1, 3)]
] ]
); );
@ -325,7 +325,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
$this->expectFailure( $this->expectFailure(
'query test($color: Color!) { colorEnum(fromEnum: $color) }', 'query test($color: Color!) { colorEnum(fromEnum: $color) }',
['color' => 2], ['color' => 2],
"Variable \"\$color\" got invalid value 2.\nExpected type \"Color\", found 2." 'Variable "$color" got invalid value 2; Expected type Color.'
); );
} }
@ -459,7 +459,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
[ [
'data' => ['first' => 'ONE', 'second' => 'TWO', 'third' => null], 'data' => ['first' => 'ONE', 'second' => 'TWO', 'third' => null],
'errors' => [[ 'errors' => [[
'debugMessage' => 'Expected a value of type "SimpleEnum" but received: "WRONG"', 'debugMessage' => 'Expected a value of type "SimpleEnum" but received: WRONG',
'locations' => [['line' => 4, 'column' => 13]] 'locations' => [['line' => 4, 'column' => 13]]
]] ]]
], ],

View File

@ -1,8 +1,7 @@
<?php <?php
namespace GraphQL\Tests\Type; namespace GraphQL\Tests\Type;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\Error;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
class ScalarSerializationTest extends \PHPUnit_Framework_TestCase class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
@ -31,14 +30,14 @@ class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
// The GraphQL specification does not allow serializing non-integer values // The GraphQL specification does not allow serializing non-integer values
// as Int to avoid accidental data loss. // as Int to avoid accidental data loss.
$intType = Type::int(); $intType = Type::int();
$this->setExpectedException(InvariantViolation::class, 'Int cannot represent non-integer value: 0.1'); $this->setExpectedException(Error::class, 'Int cannot represent non-integer value: 0.1');
$intType->serialize(0.1); $intType->serialize(0.1);
} }
public function testSerializesOutputIntCannotRepresentFloat2() public function testSerializesOutputIntCannotRepresentFloat2()
{ {
$intType = Type::int(); $intType = Type::int();
$this->setExpectedException(InvariantViolation::class, 'Int cannot represent non-integer value: 1.1'); $this->setExpectedException(Error::class, 'Int cannot represent non-integer value: 1.1');
$intType->serialize(1.1); $intType->serialize(1.1);
} }
@ -46,7 +45,7 @@ class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
public function testSerializesOutputIntCannotRepresentNegativeFloat() public function testSerializesOutputIntCannotRepresentNegativeFloat()
{ {
$intType = Type::int(); $intType = Type::int();
$this->setExpectedException(InvariantViolation::class, 'Int cannot represent non-integer value: -1.1'); $this->setExpectedException(Error::class, 'Int cannot represent non-integer value: -1.1');
$intType->serialize(-1.1); $intType->serialize(-1.1);
} }
@ -54,7 +53,7 @@ class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
public function testSerializesOutputIntCannotRepresentNumericString() public function testSerializesOutputIntCannotRepresentNumericString()
{ {
$intType = Type::int(); $intType = Type::int();
$this->setExpectedException(InvariantViolation::class, ''); $this->setExpectedException(Error::class, '');
$intType->serialize('Int cannot represent non-integer value: "-1.1"'); $intType->serialize('Int cannot represent non-integer value: "-1.1"');
} }
@ -64,7 +63,7 @@ class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
// Maybe a safe PHP int, but bigger than 2^32, so not // Maybe a safe PHP int, but bigger than 2^32, so not
// representable as a GraphQL Int // representable as a GraphQL Int
$intType = Type::int(); $intType = Type::int();
$this->setExpectedException(InvariantViolation::class, 'Int cannot represent non 32-bit signed integer value: 9876504321'); $this->setExpectedException(Error::class, 'Int cannot represent non 32-bit signed integer value: 9876504321');
$intType->serialize(9876504321); $intType->serialize(9876504321);
} }
@ -72,28 +71,28 @@ class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
public function testSerializesOutputIntCannotRepresentLowerThan32Bits() public function testSerializesOutputIntCannotRepresentLowerThan32Bits()
{ {
$intType = Type::int(); $intType = Type::int();
$this->setExpectedException(InvariantViolation::class, 'Int cannot represent non 32-bit signed integer value: -9876504321'); $this->setExpectedException(Error::class, 'Int cannot represent non 32-bit signed integer value: -9876504321');
$intType->serialize(-9876504321); $intType->serialize(-9876504321);
} }
public function testSerializesOutputIntCannotRepresentBiggerThanSigned32Bits() public function testSerializesOutputIntCannotRepresentBiggerThanSigned32Bits()
{ {
$intType = Type::int(); $intType = Type::int();
$this->setExpectedException(InvariantViolation::class, 'Int cannot represent non 32-bit signed integer value: 1.0E+100'); $this->setExpectedException(Error::class, 'Int cannot represent non 32-bit signed integer value: 1.0E+100');
$intType->serialize(1e100); $intType->serialize(1e100);
} }
public function testSerializesOutputIntCannotRepresentLowerThanSigned32Bits() public function testSerializesOutputIntCannotRepresentLowerThanSigned32Bits()
{ {
$intType = Type::int(); $intType = Type::int();
$this->setExpectedException(InvariantViolation::class, 'Int cannot represent non 32-bit signed integer value: -1.0E+100'); $this->setExpectedException(Error::class, 'Int cannot represent non 32-bit signed integer value: -1.0E+100');
$intType->serialize(-1e100); $intType->serialize(-1e100);
} }
public function testSerializesOutputIntCannotRepresentString() public function testSerializesOutputIntCannotRepresentString()
{ {
$intType = Type::int(); $intType = Type::int();
$this->setExpectedException(InvariantViolation::class, 'Int cannot represent non 32-bit signed integer value: "one"'); $this->setExpectedException(Error::class, 'Int cannot represent non 32-bit signed integer value: one');
$intType->serialize('one'); $intType->serialize('one');
} }
@ -101,7 +100,7 @@ class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
public function testSerializesOutputIntCannotRepresentEmptyString() public function testSerializesOutputIntCannotRepresentEmptyString()
{ {
$intType = Type::int(); $intType = Type::int();
$this->setExpectedException(InvariantViolation::class, 'Int cannot represent non 32-bit signed integer value: (empty string)'); $this->setExpectedException(Error::class, 'Int cannot represent non 32-bit signed integer value: (empty string)');
$intType->serialize(''); $intType->serialize('');
} }
@ -127,14 +126,14 @@ class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
public function testSerializesOutputFloatCannotRepresentString() public function testSerializesOutputFloatCannotRepresentString()
{ {
$floatType = Type::float(); $floatType = Type::float();
$this->setExpectedException(InvariantViolation::class, 'Float cannot represent non numeric value: "one"'); $this->setExpectedException(Error::class, 'Float cannot represent non numeric value: one');
$floatType->serialize('one'); $floatType->serialize('one');
} }
public function testSerializesOutputFloatCannotRepresentEmptyString() public function testSerializesOutputFloatCannotRepresentEmptyString()
{ {
$floatType = Type::float(); $floatType = Type::float();
$this->setExpectedException(InvariantViolation::class, 'Float cannot represent non numeric value: (empty string)'); $this->setExpectedException(Error::class, 'Float cannot represent non numeric value: (empty string)');
$floatType->serialize(''); $floatType->serialize('');
} }
@ -156,14 +155,14 @@ class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
public function testSerializesOutputStringsCannotRepresentArray() public function testSerializesOutputStringsCannotRepresentArray()
{ {
$stringType = Type::string(); $stringType = Type::string();
$this->setExpectedException(InvariantViolation::class, 'String cannot represent non scalar value: array(0)'); $this->setExpectedException(Error::class, 'String cannot represent non scalar value: []');
$stringType->serialize([]); $stringType->serialize([]);
} }
public function testSerializesOutputStringsCannotRepresentObject() public function testSerializesOutputStringsCannotRepresentObject()
{ {
$stringType = Type::string(); $stringType = Type::string();
$this->setExpectedException(InvariantViolation::class, 'String cannot represent non scalar value: instance of stdClass'); $this->setExpectedException(Error::class, 'String cannot represent non scalar value: instance of stdClass');
$stringType->serialize(new \stdClass()); $stringType->serialize(new \stdClass());
} }
@ -202,7 +201,7 @@ class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
public function testSerializesOutputIDCannotRepresentObject() public function testSerializesOutputIDCannotRepresentObject()
{ {
$idType = Type::id(); $idType = Type::id();
$this->setExpectedException(InvariantViolation::class, 'ID type cannot represent non scalar value: instance of stdClass'); $this->setExpectedException(Error::class, 'ID type cannot represent non scalar value: instance of stdClass');
$idType->serialize(new \stdClass()); $idType->serialize(new \stdClass());
} }
} }

View File

@ -165,7 +165,7 @@ class TypeLoaderTest extends \PHPUnit_Framework_TestCase
{ {
$this->setExpectedException( $this->setExpectedException(
InvariantViolation::class, InvariantViolation::class,
'Schema type loader must be callable if provided but got: array(0)' 'Schema type loader must be callable if provided but got: []'
); );
new Schema([ new Schema([

View File

@ -1662,7 +1662,7 @@ class ValidationTest extends \PHPUnit_Framework_TestCase
public function invalidEnumValueName() public function invalidEnumValueName()
{ {
return [ return [
['#value', 'SomeEnum has value with invalid name: "#value" (Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "#value" does not.)'], ['#value', 'SomeEnum has value with invalid name: #value (Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "#value" does not.)'],
['true', 'SomeEnum: "true" can not be used as an Enum value.'], ['true', 'SomeEnum: "true" can not be used as an Enum value.'],
['false', 'SomeEnum: "false" can not be used as an Enum value.'], ['false', 'SomeEnum: "false" can not be used as an Enum value.'],
['null', 'SomeEnum: "null" can not be used as an Enum value.'], ['null', 'SomeEnum: "null" can not be used as an Enum value.'],
@ -1776,7 +1776,7 @@ class ValidationTest extends \PHPUnit_Framework_TestCase
$this->setExpectedException( $this->setExpectedException(
InvariantViolation::class, InvariantViolation::class,
'BadResolver.badField field resolver must be a function if provided, but got: array(0)' 'BadResolver.badField field resolver must be a function if provided, but got: []'
); );
$schema->assertValid(); $schema->assertValid();

View File

@ -0,0 +1,201 @@
<?php
namespace GraphQL\Tests\Utils;
use GraphQL\Executor\Values;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\Value;
class CoerceValueTest extends \PHPUnit_Framework_TestCase
{
// Describe: coerceValue
/**
* @it coercing an array to GraphQLString produces an error
*/
public function testCoercingAnArrayToGraphQLStringProducesAnError()
{
$result = Value::coerceValue([1, 2, 3], Type::string());
$this->expectError(
$result,
'Expected type String; String cannot represent an array value: [1,2,3]'
);
$this->assertEquals(
'String cannot represent an array value: [1,2,3]',
$result['errors'][0]->getPrevious()->getMessage()
);
}
// Describe: for GraphQLInt
/**
* @it returns no error for int input
*/
public function testIntReturnsNoErrorForIntInput()
{
$result = Value::coerceValue('1', Type::int());
$this->expectNoErrors($result);
}
/**
* @it returns no error for negative int input
*/
public function testIntReturnsNoErrorForNegativeIntInput()
{
$result = Value::coerceValue('-1', Type::int());
$this->expectNoErrors($result);
}
/**
* @it returns no error for exponent input
*/
public function testIntReturnsNoErrorForExponentInput()
{
$result = Value::coerceValue('1e3', Type::int());
$this->expectNoErrors($result);
}
/**
* @it returns no error for null
*/
public function testIntReturnsASingleErrorNull()
{
$result = Value::coerceValue(null, Type::int());
$this->expectNoErrors($result);
}
/**
* @it returns a single error for empty value
*/
public function testIntReturnsASingleErrorForEmptyValue()
{
$result = Value::coerceValue('', Type::int());
$this->expectError(
$result,
'Expected type Int; Int cannot represent non 32-bit signed integer value: (empty string)'
);
}
/**
* @it returns error for float input as int
*/
public function testIntReturnsErrorForFloatInputAsInt()
{
$result = Value::coerceValue('1.5', Type::int());
$this->expectError(
$result,
'Expected type Int; Int cannot represent non-integer value: 1.5'
);
}
/**
* @it returns a single error for char input
*/
public function testIntReturnsASingleErrorForCharInput()
{
$result = Value::coerceValue('a', Type::int());
$this->expectError(
$result,
'Expected type Int; Int cannot represent non 32-bit signed integer value: a'
);
}
/**
* @it returns a single error for multi char input
*/
public function testIntReturnsASingleErrorForMultiCharInput()
{
$result = Value::coerceValue('meow', Type::int());
$this->expectError(
$result,
'Expected type Int; Int cannot represent non 32-bit signed integer value: meow'
);
}
// Describe: for GraphQLFloat
/**
* @it returns no error for int input
*/
public function testFloatReturnsNoErrorForIntInput()
{
$result = Value::coerceValue('1', Type::float());
$this->expectNoErrors($result);
}
/**
* @it returns no error for exponent input
*/
public function testFloatReturnsNoErrorForExponentInput()
{
$result = Value::coerceValue('1e3', Type::float());
$this->expectNoErrors($result);
}
/**
* @it returns no error for float input
*/
public function testFloatReturnsNoErrorForFloatInput()
{
$result = Value::coerceValue('1.5', Type::float());
$this->expectNoErrors($result);
}
/**
* @it returns no error for null
*/
public function testFloatReturnsASingleErrorNull()
{
$result = Value::coerceValue(null, Type::float());
$this->expectNoErrors($result);
}
/**
* @it returns a single error for empty value
*/
public function testFloatReturnsASingleErrorForEmptyValue()
{
$result = Value::coerceValue('', Type::float());
$this->expectError(
$result,
'Expected type Float; Float cannot represent non numeric value: (empty string)'
);
}
/**
* @it returns a single error for char input
*/
public function testFloatReturnsASingleErrorForCharInput()
{
$result = Value::coerceValue('a', Type::float());
$this->expectError(
$result,
'Expected type Float; Float cannot represent non numeric value: a'
);
}
/**
* @it returns a single error for multi char input
*/
public function testFloatReturnsASingleErrorForMultiCharInput()
{
$result = Value::coerceValue('meow', Type::float());
$this->expectError(
$result,
'Expected type Float; Float cannot represent non numeric value: meow'
);
}
private function expectNoErrors($result)
{
$this->assertInternalType('array', $result);
$this->assertNull($result['errors']);
}
private function expectError($result, $expected) {
$this->assertInternalType('array', $result);
$this->assertInternalType('array', $result['errors']);
$this->assertCount(1, $result['errors']);
$this->assertEquals($expected, $result['errors'][0]->getMessage());
}
}

View File

@ -1,132 +0,0 @@
<?php
namespace GraphQL\Tests\Utils;
use GraphQL\Executor\Values;
use GraphQL\Type\Definition\Type;
class IsValidPHPValueTest extends \PHPUnit_Framework_TestCase
{
public function testValidIntValue()
{
// returns no error for positive int value
$result = Values::isValidPHPValue(1, Type::int());
$this->expectNoErrors($result);
// returns no error for negative int value
$result = Values::isValidPHPValue(-1, Type::int());
$this->expectNoErrors($result);
// returns no error for null value
$result = Values::isValidPHPValue(null, Type::int());
$this->expectNoErrors($result);
// returns a single error for positive int string value
$result = Values::isValidPHPValue('1', Type::int());
$this->expectErrorResult($result, 1);
// returns a single error for negative int string value
$result = Values::isValidPHPValue('-1', Type::int());
$this->expectErrorResult($result, 1);
// returns errors for exponential int string value
$result = Values::isValidPHPValue('1e3', Type::int());
$this->expectErrorResult($result, 1);
$result = Values::isValidPHPValue('0e3', Type::int());
$this->expectErrorResult($result, 1);
// returns a single error for empty value
$result = Values::isValidPHPValue('', Type::int());
$this->expectErrorResult($result, 1);
// returns error for float value
$result = Values::isValidPHPValue(1.5, Type::int());
$this->expectErrorResult($result, 1);
$result = Values::isValidPHPValue(1e3, Type::int());
$this->expectErrorResult($result, 1);
// returns error for float string value
$result = Values::isValidPHPValue('1.5', Type::int());
$this->expectErrorResult($result, 1);
// returns a single error for char input
$result = Values::isValidPHPValue('a', Type::int());
$this->expectErrorResult($result, 1);
// returns a single error for char input
$result = Values::isValidPHPValue('meow', Type::int());
$this->expectErrorResult($result, 1);
}
public function testValidFloatValue()
{
// returns no error for positive float value
$result = Values::isValidPHPValue(1.2, Type::float());
$this->expectNoErrors($result);
// returns no error for exponential float value
$result = Values::isValidPHPValue(1e3, Type::float());
$this->expectNoErrors($result);
// returns no error for negative float value
$result = Values::isValidPHPValue(-1.2, Type::float());
$this->expectNoErrors($result);
// returns no error for a positive int value
$result = Values::isValidPHPValue(1, Type::float());
$this->expectNoErrors($result);
// returns no errors for a negative int value
$result = Values::isValidPHPValue(-1, Type::float());
$this->expectNoErrors($result);
// returns no error for null value:
$result = Values::isValidPHPValue(null, Type::float());
$this->expectNoErrors($result);
// returns error for positive float string value
$result = Values::isValidPHPValue('1.2', Type::float());
$this->expectErrorResult($result, 1);
// returns error for negative float string value
$result = Values::isValidPHPValue('-1.2', Type::float());
$this->expectErrorResult($result, 1);
// returns error for a positive int string value
$result = Values::isValidPHPValue('1', Type::float());
$this->expectErrorResult($result, 1);
// returns errors for a negative int string value
$result = Values::isValidPHPValue('-1', Type::float());
$this->expectErrorResult($result, 1);
// returns error for exponent input
$result = Values::isValidPHPValue('1e3', Type::float());
$this->expectErrorResult($result, 1);
$result = Values::isValidPHPValue('0e3', Type::float());
$this->expectErrorResult($result, 1);
// returns a single error for empty value
$result = Values::isValidPHPValue('', Type::float());
$this->expectErrorResult($result, 1);
// returns a single error for char input
$result = Values::isValidPHPValue('a', Type::float());
$this->expectErrorResult($result, 1);
// returns a single error for char input
$result = Values::isValidPHPValue('meow', Type::float());
$this->expectErrorResult($result, 1);
}
private function expectNoErrors($result)
{
$this->assertInternalType('array', $result);
$this->assertEquals([], $result);
}
private function expectErrorResult($result, $size) {
$this->assertInternalType('array', $result);
$this->assertEquals($size, count($result));
}
}

View File

@ -41,7 +41,7 @@ function renderClassMethod(ReflectionMethod $method) {
if ($p->isDefaultValueAvailable()) { if ($p->isDefaultValueAvailable()) {
$val = $p->isDefaultValueConstant() ? $p->getDefaultValueConstantName() : $p->getDefaultValue(); $val = $p->isDefaultValueConstant() ? $p->getDefaultValueConstantName() : $p->getDefaultValue();
$def .= " = " . (is_array($val) ? '[]' : Utils::printSafe($val)); $def .= " = " . Utils::printSafeJson($val);
} }
return $def; return $def;
@ -63,7 +63,7 @@ TEMPLATE;
} }
function renderConstant($value, $name) { function renderConstant($value, $name) {
return "const $name = " . Utils::printSafe($value) . ";"; return "const $name = " . Utils::printSafeJson($value) . ";";
} }
function renderProp(ReflectionProperty $prop) { function renderProp(ReflectionProperty $prop) {