graphql-php/src/Utils/Value.php

315 lines
9.9 KiB
PHP
Raw Normal View History

<?php
2018-08-22 01:20:49 +02:00
declare(strict_types=1);
namespace GraphQL\Utils;
2018-09-26 10:55:09 +02:00
use Exception;
use GraphQL\Error\Error;
use GraphQL\Language\AST\Node;
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-09-26 10:55:09 +02:00
use Throwable;
use Traversable;
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;
/**
* 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-08-22 01:20:49 +02:00
public static function coerceValue($value, InputType $type, $blameNode = null, ?array $path = null)
{
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),
$blameNode,
$path
),
]);
}
2018-08-22 01:20:49 +02:00
return self::coerceValue($value, $type->getWrappedType(), $blameNode, $path);
}
2018-08-22 01:20:49 +02:00
if ($value === null) {
// 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 {
return self::ofValue($type->parseValue($value));
2018-09-26 10:55:09 +02:00
} catch (Exception $error) {
return self::ofErrors([
self::coercionError(
2018-08-22 01:20:49 +02:00
sprintf('Expected type %s', $type->name),
$blameNode,
$path,
$error->getMessage(),
$error
),
]);
2018-09-26 10:55:09 +02:00
} catch (Throwable $error) {
return self::ofErrors([
self::coercionError(
2018-08-22 01:20:49 +02:00
sprintf('Expected type %s', $type->name),
$blameNode,
$path,
$error->getMessage(),
$error
),
]);
}
}
if ($type instanceof EnumType) {
if (is_string($value)) {
$enumValue = $type->getValue($value);
if ($enumValue) {
return self::ofValue($enumValue->value);
}
}
$suggestions = Utils::suggestionList(
Utils::printSafe($value),
2018-08-22 01:20:49 +02:00
array_map(
2018-09-26 10:55:09 +02:00
static function ($enumValue) {
2018-08-22 01:20:49 +02:00
return $enumValue->name;
},
$type->getValues()
)
);
2018-08-22 01:20:49 +02:00
$didYouMean = $suggestions
2018-08-22 01:20:49 +02:00
? 'did you mean ' . Utils::orList($suggestions) . '?'
: null;
return self::ofErrors([
self::coercionError(
2018-08-22 01:20:49 +02:00
sprintf('Expected type %s', $type->name),
$blameNode,
$path,
$didYouMean
),
]);
}
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
2018-09-26 10:55:09 +02:00
if (is_array($value) || $value instanceof Traversable) {
2018-08-22 01:20:49 +02:00
$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'];
}
}
2018-08-22 01:20:49 +02: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
return $coercedItem['errors'] ? $coercedItem : self::ofValue([$coercedItem['value']]);
}
if ($type instanceof InputObjectType) {
2018-09-26 10:55:09 +02:00
if (! is_object($value) && ! is_array($value) && ! $value instanceof Traversable) {
return self::ofErrors([
self::coercionError(
2018-08-22 01:20:49 +02:00
sprintf('Expected type %s to be an object', $type->name),
$blameNode,
$path
),
]);
}
2018-08-22 01:20:49 +02:00
$errors = [];
$coercedValue = [];
2018-08-22 01:20:49 +02:00
$fields = $type->getFields();
foreach ($fields as $fieldName => $field) {
2018-08-22 01:20:49 +02:00
if (array_key_exists($fieldName, $value)) {
$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'];
}
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
)
);
}
}
// 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-08-22 01:20:49 +02:00
$suggestions = Utils::suggestionList(
(string) $fieldName,
2018-08-22 01:20:49 +02:00
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
)
);
}
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-08-22 01:20:49 +02:00
private static function ofErrors($errors)
{
return ['errors' => $errors, 'value' => Utils::undefined()];
}
/**
2018-09-26 10:55:09 +02:00
* @param string $message
* @param Node $blameNode
* @param mixed[]|null $path
* @param string $subMessage
* @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
) {
$pathStr = self::printPath($path);
2018-08-22 01:20:49 +02:00
// Return a GraphQLError instance
return new Error(
$message .
($pathStr ? ' at ' . $pathStr : '') .
($subMessage ? '; ' . $subMessage : '.'),
$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-09-26 10:55:09 +02:00
*
* @return string
*/
2018-08-22 01:20:49 +02:00
private static function printPath(?array $path = null)
{
$pathStr = '';
$currentPath = $path;
2018-08-22 01:20:49 +02:00
while ($currentPath) {
$pathStr =
(is_string($currentPath['key'])
? '.' . $currentPath['key']
: '[' . $currentPath['key'] . ']') . $pathStr;
$currentPath = $currentPath['prev'];
}
2018-08-22 01:20:49 +02:00
return $pathStr ? 'value' . $pathStr : '';
}
2018-08-22 01:20:49 +02:00
/**
* @param mixed $value
2018-09-26 10:55:09 +02:00
*
2018-08-22 01:20:49 +02:00
* @return (mixed|null)[]
*/
private static function ofValue($value)
{
return ['errors' => null, 'value' => $value];
}
/**
* @param mixed|null $prev
* @param mixed|null $key
2018-09-26 10:55:09 +02:00
*
2018-08-22 01:20:49 +02:00
* @return (mixed|null)[]
*/
private static function atPath($prev, $key)
{
return ['prev' => $prev, 'key' => $key];
}
/**
* @param Error[] $errors
* @param Error|Error[] $moreErrors
2018-09-26 10:55:09 +02:00
*
2018-08-22 01:20:49 +02:00
* @return Error[]
*/
private static function add($errors, $moreErrors)
{
return array_merge($errors, is_array($moreErrors) ? $moreErrors : [$moreErrors]);
}
}