2018-02-13 16:51:44 +01:00
|
|
|
<?php
|
2018-08-22 01:20:49 +02:00
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
2018-02-13 16:51:44 +01:00
|
|
|
namespace GraphQL\Utils;
|
|
|
|
|
|
|
|
use GraphQL\Error\Error;
|
2018-02-16 16:19:25 +01:00
|
|
|
use GraphQL\Language\AST\Node;
|
2018-02-13 16:51:44 +01:00
|
|
|
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;
|
2018-08-22 01:20:49 +02:00
|
|
|
use function array_key_exists;
|
|
|
|
use function array_keys;
|
|
|
|
use function array_map;
|
|
|
|
use function array_merge;
|
|
|
|
use function is_array;
|
|
|
|
use function is_object;
|
|
|
|
use function is_string;
|
|
|
|
use function sprintf;
|
2018-02-13 16:51:44 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
2018-08-22 01:20:49 +02:00
|
|
|
*
|
|
|
|
* @param mixed[] $path
|
2018-02-13 16:51:44 +01:00
|
|
|
*/
|
2018-08-22 01:20:49 +02:00
|
|
|
public static function coerceValue($value, InputType $type, $blameNode = null, ?array $path = null)
|
2018-02-13 16:51:44 +01:00
|
|
|
{
|
|
|
|
if ($type instanceof NonNull) {
|
|
|
|
if ($value === null) {
|
|
|
|
return self::ofErrors([
|
|
|
|
self::coercionError(
|
2018-08-22 01:20:49 +02:00
|
|
|
sprintf('Expected non-nullable type %s not to be null', $type),
|
2018-02-13 16:51:44 +01:00
|
|
|
$blameNode,
|
|
|
|
$path
|
|
|
|
),
|
|
|
|
]);
|
|
|
|
}
|
2018-08-22 01:20:49 +02:00
|
|
|
|
2018-02-13 16:51:44 +01:00
|
|
|
return self::coerceValue($value, $type->getWrappedType(), $blameNode, $path);
|
|
|
|
}
|
|
|
|
|
2018-08-22 01:20:49 +02:00
|
|
|
if ($value === null) {
|
2018-02-13 16:51:44 +01:00
|
|
|
// 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 {
|
2018-04-24 15:14:31 +02:00
|
|
|
return self::ofValue($type->parseValue($value));
|
2018-02-13 16:51:44 +01:00
|
|
|
} catch (\Exception $error) {
|
|
|
|
return self::ofErrors([
|
2018-02-16 16:19:25 +01:00
|
|
|
self::coercionError(
|
2018-08-22 01:20:49 +02:00
|
|
|
sprintf('Expected type %s', $type->name),
|
2018-02-16 16:19:25 +01:00
|
|
|
$blameNode,
|
|
|
|
$path,
|
|
|
|
$error->getMessage(),
|
|
|
|
$error
|
|
|
|
),
|
2018-02-13 16:51:44 +01:00
|
|
|
]);
|
|
|
|
} catch (\Throwable $error) {
|
|
|
|
return self::ofErrors([
|
2018-02-16 16:19:25 +01:00
|
|
|
self::coercionError(
|
2018-08-22 01:20:49 +02:00
|
|
|
sprintf('Expected type %s', $type->name),
|
2018-02-16 16:19:25 +01:00
|
|
|
$blameNode,
|
|
|
|
$path,
|
|
|
|
$error->getMessage(),
|
|
|
|
$error
|
|
|
|
),
|
2018-02-13 16:51:44 +01:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($type instanceof EnumType) {
|
|
|
|
if (is_string($value)) {
|
|
|
|
$enumValue = $type->getValue($value);
|
|
|
|
if ($enumValue) {
|
|
|
|
return self::ofValue($enumValue->value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-16 16:19:25 +01:00
|
|
|
$suggestions = Utils::suggestionList(
|
|
|
|
Utils::printSafe($value),
|
2018-08-22 01:20:49 +02:00
|
|
|
array_map(
|
|
|
|
function ($enumValue) {
|
|
|
|
return $enumValue->name;
|
|
|
|
},
|
|
|
|
$type->getValues()
|
|
|
|
)
|
2018-02-16 16:19:25 +01:00
|
|
|
);
|
2018-08-22 01:20:49 +02:00
|
|
|
|
2018-02-16 16:19:25 +01:00
|
|
|
$didYouMean = $suggestions
|
2018-08-22 01:20:49 +02:00
|
|
|
? 'did you mean ' . Utils::orList($suggestions) . '?'
|
2018-02-16 16:19:25 +01:00
|
|
|
: null;
|
|
|
|
|
2018-02-13 16:51:44 +01:00
|
|
|
return self::ofErrors([
|
2018-02-16 16:19:25 +01:00
|
|
|
self::coercionError(
|
2018-08-22 01:20:49 +02:00
|
|
|
sprintf('Expected type %s', $type->name),
|
2018-02-16 16:19:25 +01:00
|
|
|
$blameNode,
|
|
|
|
$path,
|
|
|
|
$didYouMean
|
|
|
|
),
|
2018-02-13 16:51:44 +01:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($type instanceof ListOfType) {
|
|
|
|
$itemType = $type->getWrappedType();
|
|
|
|
if (is_array($value) || $value instanceof \Traversable) {
|
2018-08-22 01:20:49 +02:00
|
|
|
$errors = [];
|
2018-02-13 16:51:44 +01:00
|
|
|
$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'];
|
|
|
|
}
|
|
|
|
}
|
2018-08-22 01:20:49 +02:00
|
|
|
|
2018-02-13 16:51:44 +01:00
|
|
|
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);
|
2018-08-22 01:20:49 +02:00
|
|
|
|
2018-02-13 16:51:44 +01:00
|
|
|
return $coercedItem['errors'] ? $coercedItem : self::ofValue([$coercedItem['value']]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($type instanceof InputObjectType) {
|
2018-08-22 01:20:49 +02:00
|
|
|
if (! is_object($value) && ! is_array($value) && ! $value instanceof \Traversable) {
|
2018-02-13 16:51:44 +01:00
|
|
|
return self::ofErrors([
|
2018-02-16 16:19:25 +01:00
|
|
|
self::coercionError(
|
2018-08-22 01:20:49 +02:00
|
|
|
sprintf('Expected type %s to be an object', $type->name),
|
2018-02-16 16:19:25 +01:00
|
|
|
$blameNode,
|
|
|
|
$path
|
|
|
|
),
|
2018-02-13 16:51:44 +01:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2018-08-22 01:20:49 +02:00
|
|
|
$errors = [];
|
2018-02-13 16:51:44 +01:00
|
|
|
$coercedValue = [];
|
2018-08-22 01:20:49 +02:00
|
|
|
$fields = $type->getFields();
|
2018-02-13 16:51:44 +01:00
|
|
|
foreach ($fields as $fieldName => $field) {
|
2018-08-22 01:20:49 +02:00
|
|
|
if (array_key_exists($fieldName, $value)) {
|
|
|
|
$fieldValue = $value[$fieldName];
|
2018-02-13 16:51:44 +01:00
|
|
|
$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'];
|
|
|
|
}
|
2018-08-22 01:20:49 +02:00
|
|
|
} elseif ($field->defaultValueExists()) {
|
|
|
|
$coercedValue[$fieldName] = $field->defaultValue;
|
|
|
|
} elseif ($field->getType() instanceof NonNull) {
|
|
|
|
$fieldPath = self::printPath(self::atPath($path, $fieldName));
|
|
|
|
$errors = self::add(
|
|
|
|
$errors,
|
|
|
|
self::coercionError(
|
|
|
|
sprintf(
|
|
|
|
'Field %s of required type %s was not provided',
|
|
|
|
$fieldPath,
|
|
|
|
$field->type->toString()
|
|
|
|
),
|
|
|
|
$blameNode
|
|
|
|
)
|
|
|
|
);
|
2018-02-13 16:51:44 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure every provided field is defined.
|
|
|
|
foreach ($value as $fieldName => $field) {
|
2018-08-22 01:20:49 +02:00
|
|
|
if (array_key_exists($fieldName, $fields)) {
|
|
|
|
continue;
|
2018-02-13 16:51:44 +01:00
|
|
|
}
|
2018-08-22 01:20:49 +02:00
|
|
|
|
|
|
|
$suggestions = Utils::suggestionList(
|
|
|
|
$fieldName,
|
|
|
|
array_keys($fields)
|
|
|
|
);
|
|
|
|
$didYouMean = $suggestions
|
|
|
|
? 'did you mean ' . Utils::orList($suggestions) . '?'
|
|
|
|
: null;
|
|
|
|
$errors = self::add(
|
|
|
|
$errors,
|
|
|
|
self::coercionError(
|
|
|
|
sprintf('Field "%s" is not defined by type %s', $fieldName, $type->name),
|
|
|
|
$blameNode,
|
|
|
|
$path,
|
|
|
|
$didYouMean
|
|
|
|
)
|
|
|
|
);
|
2018-02-13 16:51:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
|
|
|
|
}
|
|
|
|
|
2018-08-22 01:20:49 +02:00
|
|
|
throw new Error(sprintf('Unexpected type %s', $type->name));
|
2018-02-13 16:51:44 +01:00
|
|
|
}
|
|
|
|
|
2018-08-22 01:20:49 +02:00
|
|
|
private static function ofErrors($errors)
|
|
|
|
{
|
2018-02-13 16:51:44 +01:00
|
|
|
return ['errors' => $errors, 'value' => Utils::undefined()];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-08-22 01:20:49 +02:00
|
|
|
* @param string $message
|
|
|
|
* @param Node $blameNode
|
|
|
|
* @param mixed[]|null $path
|
|
|
|
* @param string $subMessage
|
2018-02-13 16:51:44 +01:00
|
|
|
* @param \Exception|\Throwable|null $originalError
|
|
|
|
* @return Error
|
|
|
|
*/
|
2018-08-22 01:20:49 +02:00
|
|
|
private static function coercionError(
|
|
|
|
$message,
|
|
|
|
$blameNode,
|
|
|
|
?array $path = null,
|
|
|
|
$subMessage = null,
|
|
|
|
$originalError = null
|
|
|
|
) {
|
2018-02-13 16:51:44 +01:00
|
|
|
$pathStr = self::printPath($path);
|
2018-08-22 01:20:49 +02:00
|
|
|
|
2018-02-13 16:51:44 +01:00
|
|
|
// Return a GraphQLError instance
|
|
|
|
return new Error(
|
|
|
|
$message .
|
|
|
|
($pathStr ? ' at ' . $pathStr : '') .
|
2018-02-16 16:19:25 +01:00
|
|
|
($subMessage ? '; ' . $subMessage : '.'),
|
2018-02-13 16:51:44 +01:00
|
|
|
$blameNode,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
$originalError
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Build a string describing the path into the value where the error was found
|
|
|
|
*
|
2018-08-22 01:20:49 +02:00
|
|
|
* @param mixed[]|null $path
|
2018-02-13 16:51:44 +01:00
|
|
|
* @return string
|
|
|
|
*/
|
2018-08-22 01:20:49 +02:00
|
|
|
private static function printPath(?array $path = null)
|
|
|
|
{
|
|
|
|
$pathStr = '';
|
2018-02-13 16:51:44 +01:00
|
|
|
$currentPath = $path;
|
2018-08-22 01:20:49 +02:00
|
|
|
while ($currentPath) {
|
|
|
|
$pathStr =
|
2018-02-13 16:51:44 +01:00
|
|
|
(is_string($currentPath['key'])
|
|
|
|
? '.' . $currentPath['key']
|
|
|
|
: '[' . $currentPath['key'] . ']') . $pathStr;
|
|
|
|
$currentPath = $currentPath['prev'];
|
|
|
|
}
|
2018-08-22 01:20:49 +02:00
|
|
|
|
2018-02-13 16:51:44 +01:00
|
|
|
return $pathStr ? 'value' . $pathStr : '';
|
|
|
|
}
|
2018-08-22 01:20:49 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param mixed $value
|
|
|
|
* @return (mixed|null)[]
|
|
|
|
*/
|
|
|
|
private static function ofValue($value)
|
|
|
|
{
|
|
|
|
return ['errors' => null, 'value' => $value];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param mixed|null $prev
|
|
|
|
* @param mixed|null $key
|
|
|
|
* @return (mixed|null)[]
|
|
|
|
*/
|
|
|
|
private static function atPath($prev, $key)
|
|
|
|
{
|
|
|
|
return ['prev' => $prev, 'key' => $key];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param Error[] $errors
|
|
|
|
* @param Error|Error[] $moreErrors
|
|
|
|
* @return Error[]
|
|
|
|
*/
|
|
|
|
private static function add($errors, $moreErrors)
|
|
|
|
{
|
|
|
|
return array_merge($errors, is_array($moreErrors) ? $moreErrors : [$moreErrors]);
|
|
|
|
}
|
2018-02-13 16:51:44 +01:00
|
|
|
}
|