mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-24 22:06:04 +03:00
Mapping Executor
This commit is contained in:
parent
aeb56d139a
commit
3edf6248b0
@ -278,11 +278,12 @@ Option | Type | Notes
|
|||||||
name | `string` | Required. Name of the field. If not set - GraphQL will look use `key` of fields array on type definition.
|
name | `string` | Required. Name of the field. If not set - GraphQL will look use `key` of fields array on type definition.
|
||||||
type | `Type` or `callback() => Type` | Required. One of internal or custom types. Alternatively - callback that returns `type`.
|
type | `Type` or `callback() => Type` | Required. One of internal or custom types. Alternatively - callback that returns `type`.
|
||||||
args | `array` | Array of possible type arguments. Each entry is expected to be an array with following keys: **name** (`string`), **type** (`Type` or `callback() => Type`), **defaultValue** (`any`)
|
args | `array` | Array of possible type arguments. Each entry is expected to be an array with following keys: **name** (`string`), **type** (`Type` or `callback() => Type`), **defaultValue** (`any`)
|
||||||
resolve | `callback($value, $args, ResolveInfo $info) => $data` | Function that receives `$value` describing parent type and returns `$data` for this field.
|
resolve | `callback($value, $args, ResolveInfo $info) => $fieldValue` | Function that receives `$value` of parent type and returns value for this field. Mutually exclusive with `map`
|
||||||
|
map | `callback($listOfValues, $args, ResolveInfo $info) => $fieldValues[]` | Function that receives list of parent type values and maps them to list of field values. Mutually exclusive with `resolve`
|
||||||
description | `string` | Field description for clients
|
description | `string` | Field description for clients
|
||||||
deprecationReason | `string` | Text describing why this field is deprecated. When not empty - field will not be returned by introspection queries (unless forced)
|
deprecationReason | `string` | Text describing why this field is deprecated. When not empty - field will not be returned by introspection queries (unless forced)
|
||||||
|
|
||||||
The `resolve` option is exactly the place where your custom fetching logic lives.
|
Use `map` or `resolve` for custom data fetching logic. `resolve` is easier to use, but `map` allows batching of queries to backend storage (for example you can use Redis MGET or IN(?) for SQL queries).
|
||||||
|
|
||||||
|
|
||||||
### Schema
|
### Schema
|
||||||
|
@ -147,10 +147,12 @@ class Executor
|
|||||||
$fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
|
$fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
|
||||||
|
|
||||||
if ($operation->operation === 'mutation') {
|
if ($operation->operation === 'mutation') {
|
||||||
return self::executeFieldsSerially($exeContext, $type, $rootValue, $fields->getArrayCopy());
|
$result = self::executeFieldsSerially($exeContext, $type, [$rootValue], $fields);
|
||||||
|
} else {
|
||||||
|
$result = self::executeFields($exeContext, $type, [$rootValue], $fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::executeFields($exeContext, $type, $rootValue, $fields);
|
return null === $result || $result === [] ? [] : $result[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -187,30 +189,40 @@ class Executor
|
|||||||
/**
|
/**
|
||||||
* Implements the "Evaluating selection sets" section of the spec
|
* Implements the "Evaluating selection sets" section of the spec
|
||||||
* for "write" mode.
|
* for "write" mode.
|
||||||
|
*
|
||||||
|
* @param ExecutionContext $exeContext
|
||||||
|
* @param ObjectType $parentType
|
||||||
|
* @param $sourceList
|
||||||
|
* @param $sourceIsList
|
||||||
|
* @param $fields
|
||||||
|
* @return array
|
||||||
|
* @throws Error
|
||||||
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceValue, $fields)
|
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields)
|
||||||
{
|
{
|
||||||
$results = [];
|
$results = [];
|
||||||
foreach ($fields as $responseName => $fieldASTs) {
|
foreach ($fields as $responseName => $fieldASTs) {
|
||||||
$result = self::resolveField($exeContext, $parentType, $sourceValue, $fieldASTs);
|
self::resolveField($exeContext, $parentType, $sourceList, $fieldASTs, $responseName, $results);
|
||||||
|
}
|
||||||
|
|
||||||
if ($result !== self::$UNDEFINED) {
|
|
||||||
// Undefined means that field is not defined in schema
|
|
||||||
$results[$responseName] = $result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the "Evaluating selection sets" section of the spec
|
* Implements the "Evaluating selection sets" section of the spec
|
||||||
* for "read" mode.
|
* for "read" mode.
|
||||||
|
* @param ExecutionContext $exeContext
|
||||||
|
* @param ObjectType $parentType
|
||||||
|
* @param $sourceList
|
||||||
|
* @param $fields
|
||||||
|
* @return array
|
||||||
*/
|
*/
|
||||||
private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $source, $fields)
|
private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields)
|
||||||
{
|
{
|
||||||
// Native PHP doesn't support promises.
|
// Native PHP doesn't support promises.
|
||||||
// Custom executor should be built for platforms like ReactPHP
|
// Custom executor should be built for platforms like ReactPHP
|
||||||
return self::executeFieldsSerially($exeContext, $parentType, $source, $fields);
|
return self::executeFieldsSerially($exeContext, $parentType, $sourceList, $fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -345,12 +357,21 @@ class Executor
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the field on the given source object. In particular, this
|
* Given list of parent type values returns corresponding list of field values
|
||||||
* figures out the value that the field returns by calling its resolve function,
|
*
|
||||||
* then calls completeValue to complete promises, serialize scalars, or execute
|
* In particular, this
|
||||||
* the sub-selection-set for objects.
|
* figures out the value that the field returns by calling its `resolve` or `map` function,
|
||||||
|
* then calls `completeValue` on each value to serialize scalars, or execute the sub-selection-set
|
||||||
|
* for objects.
|
||||||
|
*
|
||||||
|
* @param ExecutionContext $exeContext
|
||||||
|
* @param ObjectType $parentType
|
||||||
|
* @param $sourceValueList
|
||||||
|
* @param $fieldASTs
|
||||||
|
* @return array
|
||||||
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldASTs)
|
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $sourceValueList, $fieldASTs, $responseName, &$resolveResult)
|
||||||
{
|
{
|
||||||
$fieldAST = $fieldASTs[0];
|
$fieldAST = $fieldASTs[0];
|
||||||
$fieldName = $fieldAST->name->value;
|
$fieldName = $fieldAST->name->value;
|
||||||
@ -358,19 +379,11 @@ class Executor
|
|||||||
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName);
|
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName);
|
||||||
|
|
||||||
if (!$fieldDef) {
|
if (!$fieldDef) {
|
||||||
return self::$UNDEFINED;
|
return ;
|
||||||
}
|
}
|
||||||
|
|
||||||
$returnType = $fieldDef->getType();
|
$returnType = $fieldDef->getType();
|
||||||
|
|
||||||
if (isset($fieldDef->resolve)) {
|
|
||||||
$resolveFn = $fieldDef->resolve;
|
|
||||||
} else if (isset($parentType->resolveField)) {
|
|
||||||
$resolveFn = $parentType->resolveField;
|
|
||||||
} else {
|
|
||||||
$resolveFn = self::$defaultResolveFn;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build hash of arguments from the field.arguments AST, using the
|
// Build hash of arguments from the field.arguments AST, using the
|
||||||
// variables scope to fulfill any variable references.
|
// variables scope to fulfill any variable references.
|
||||||
// TODO: find a way to memoize, in case this field is within a List type.
|
// TODO: find a way to memoize, in case this field is within a List type.
|
||||||
@ -394,12 +407,22 @@ class Executor
|
|||||||
'variableValues' => $exeContext->variableValues,
|
'variableValues' => $exeContext->variableValues,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// If an error occurs while calling the field `resolve` function, ensure that
|
$mapFn = $fieldDef->mapFn;
|
||||||
|
|
||||||
|
// If an error occurs while calling the field `map` or `resolve` function, ensure that
|
||||||
// it is wrapped as a GraphQLError with locations. Log this error and return
|
// it is wrapped as a GraphQLError with locations. Log this error and return
|
||||||
// null if allowed, otherwise throw the error so the parent field can handle
|
// null if allowed, otherwise throw the error so the parent field can handle
|
||||||
// it.
|
// it.
|
||||||
|
if ($mapFn) {
|
||||||
try {
|
try {
|
||||||
$result = call_user_func($resolveFn, $source, $args, $info);
|
$mapped = call_user_func($mapFn, $sourceValueList, $args, $info);
|
||||||
|
|
||||||
|
Utils::invariant(
|
||||||
|
is_array($mapped) && count($mapped) === count($sourceValueList),
|
||||||
|
"Function `map` of $parentType.$fieldName is expected to return array " .
|
||||||
|
"with exact same number of items as list being mapped (first argument of `map`)"
|
||||||
|
);
|
||||||
|
|
||||||
} catch (\Exception $error) {
|
} catch (\Exception $error) {
|
||||||
$reportedError = Error::createLocatedError($error, $fieldASTs);
|
$reportedError = Error::createLocatedError($error, $fieldASTs);
|
||||||
|
|
||||||
@ -411,14 +434,48 @@ class Executor
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::completeValueCatchingError(
|
foreach ($mapped as $index => $value) {
|
||||||
|
$resolveResult[$index][$responseName] = self::completeValueCatchingError(
|
||||||
$exeContext,
|
$exeContext,
|
||||||
$returnType,
|
$returnType,
|
||||||
$fieldASTs,
|
$fieldASTs,
|
||||||
$info,
|
$info,
|
||||||
$result
|
$value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (isset($fieldDef->resolveFn)) {
|
||||||
|
$resolveFn = $fieldDef->resolveFn;
|
||||||
|
} else if (isset($parentType->resolveFieldFn)) {
|
||||||
|
$resolveFn = $parentType->resolveFieldFn;
|
||||||
|
} else {
|
||||||
|
$resolveFn = self::$defaultResolveFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($sourceValueList as $index => $value) {
|
||||||
|
try {
|
||||||
|
$resolved = call_user_func($resolveFn, $value, $args, $info);
|
||||||
|
} catch (\Exception $error) {
|
||||||
|
$reportedError = Error::createLocatedError($error, $fieldASTs);
|
||||||
|
|
||||||
|
if ($returnType instanceof NonNull) {
|
||||||
|
throw $reportedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exeContext->addError($reportedError);
|
||||||
|
$resolved = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolveResult[$index][$responseName] = self::completeValueCatchingError(
|
||||||
|
$exeContext,
|
||||||
|
$returnType,
|
||||||
|
$fieldASTs,
|
||||||
|
$info,
|
||||||
|
$resolved
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static function completeValueCatchingError(
|
public static function completeValueCatchingError(
|
||||||
@ -489,14 +546,100 @@ class Executor
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If field type is List, complete each item in the list with the inner type
|
// If field type is Scalar or Enum, serialize to a valid value, returning
|
||||||
|
// null if serialization is not possible.
|
||||||
|
if ($returnType instanceof ScalarType ||
|
||||||
|
$returnType instanceof EnumType) {
|
||||||
|
return $returnType->serialize($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If field type is List, and return type is Composite - complete by executing these fields with list value as parameter
|
||||||
if ($returnType instanceof ListOfType) {
|
if ($returnType instanceof ListOfType) {
|
||||||
$itemType = $returnType->getWrappedType();
|
$itemType = $returnType->getWrappedType();
|
||||||
|
|
||||||
Utils::invariant(
|
Utils::invariant(
|
||||||
is_array($result) || $result instanceof \Traversable,
|
is_array($result) || $result instanceof \Traversable,
|
||||||
'User Error: expected iterable, but did not find one.'
|
'User Error: expected iterable, but did not find one.'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// For Object[]:
|
||||||
|
// Allow all object fields to process list value in it's `map` callback:
|
||||||
|
if ($itemType instanceof ObjectType) {
|
||||||
|
// Filter out nulls (as `map` doesn't expect it):
|
||||||
|
$list = [];
|
||||||
|
foreach ($result as $index => $item) {
|
||||||
|
if (null !== $item) {
|
||||||
|
$list[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$subFieldASTs = self::collectSubFields($exeContext, $itemType, $fieldASTs);
|
||||||
|
$mapped = self::executeFields($exeContext, $itemType, $list, $subFieldASTs);
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
|
$completed = [];
|
||||||
|
foreach ($result as $index => $item) {
|
||||||
|
if (null === $item) {
|
||||||
|
// Complete nulls separately
|
||||||
|
$completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
|
||||||
|
} else {
|
||||||
|
// Assuming same order of mapped values
|
||||||
|
$completed[] = $mapped[$i++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $completed;
|
||||||
|
|
||||||
|
} else if ($itemType instanceof AbstractType) {
|
||||||
|
|
||||||
|
// Values sharded by ObjectType
|
||||||
|
$listPerObjectType = [];
|
||||||
|
|
||||||
|
// Helper structures to restore ordering after resolve calls
|
||||||
|
$resultTypeMap = [];
|
||||||
|
$typeNameMap = [];
|
||||||
|
$cursors = [];
|
||||||
|
|
||||||
|
foreach ($result as $index => $item) {
|
||||||
|
if (null !== $item) {
|
||||||
|
$objectType = $itemType->getObjectType($item, $info);
|
||||||
|
|
||||||
|
if ($objectType && !$itemType->isPossibleType($objectType)) {
|
||||||
|
$exeContext->addError(new Error(
|
||||||
|
"Runtime Object type \"$objectType\" is not a possible type for \"$itemType\"."
|
||||||
|
));
|
||||||
|
$result[$index] = null;
|
||||||
|
} else {
|
||||||
|
$listPerObjectType[$objectType->name][] = $item;
|
||||||
|
$resultTypeMap[$index] = $objectType->name;
|
||||||
|
$typeNameMap[$objectType->name] = $objectType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapped = [];
|
||||||
|
foreach ($listPerObjectType as $typeName => $list) {
|
||||||
|
$objectType = $typeNameMap[$typeName];
|
||||||
|
$subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs);
|
||||||
|
$mapped[$typeName] = self::executeFields($exeContext, $objectType, $list, $subFieldASTs);
|
||||||
|
$cursors[$typeName] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore order:
|
||||||
|
$completed = [];
|
||||||
|
foreach ($result as $index => $item) {
|
||||||
|
if (null === $item) {
|
||||||
|
// Complete nulls separately
|
||||||
|
$completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
|
||||||
|
} else {
|
||||||
|
$typeName = $resultTypeMap[$index];
|
||||||
|
$completed[] = $mapped[$typeName][$cursors[$typeName]++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $completed;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// For simple lists:
|
||||||
$tmp = [];
|
$tmp = [];
|
||||||
foreach ($result as $item) {
|
foreach ($result as $item) {
|
||||||
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
|
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
|
||||||
@ -504,17 +647,11 @@ class Executor
|
|||||||
return $tmp;
|
return $tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If field type is Scalar or Enum, serialize to a valid value, returning
|
|
||||||
// null if serialization is not possible.
|
|
||||||
if ($returnType instanceof ScalarType ||
|
|
||||||
$returnType instanceof EnumType) {
|
|
||||||
Utils::invariant(method_exists($returnType, 'serialize'), 'Missing serialize method on type');
|
|
||||||
return $returnType->serialize($result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field type must be Object, Interface or Union and expect sub-selections.
|
|
||||||
if ($returnType instanceof ObjectType) {
|
if ($returnType instanceof ObjectType) {
|
||||||
$objectType = $returnType;
|
$objectType = $returnType;
|
||||||
|
|
||||||
} else if ($returnType instanceof AbstractType) {
|
} else if ($returnType instanceof AbstractType) {
|
||||||
$objectType = $returnType->getObjectType($result, $info);
|
$objectType = $returnType->getObjectType($result, $info);
|
||||||
|
|
||||||
@ -542,6 +679,18 @@ class Executor
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Collect sub-fields to execute to complete this value.
|
// Collect sub-fields to execute to complete this value.
|
||||||
|
$subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs);
|
||||||
|
return self::executeFields($exeContext, $objectType, [$result], $subFieldASTs)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ExecutionContext $exeContext
|
||||||
|
* @param ObjectType $objectType
|
||||||
|
* @param $fieldASTs
|
||||||
|
* @return \ArrayObject
|
||||||
|
*/
|
||||||
|
private static function collectSubFields(ExecutionContext $exeContext, ObjectType $objectType, $fieldASTs)
|
||||||
|
{
|
||||||
$subFieldASTs = new \ArrayObject();
|
$subFieldASTs = new \ArrayObject();
|
||||||
$visitedFragmentNames = new \ArrayObject();
|
$visitedFragmentNames = new \ArrayObject();
|
||||||
for ($i = 0; $i < count($fieldASTs); $i++) {
|
for ($i = 0; $i < count($fieldASTs); $i++) {
|
||||||
@ -556,11 +705,9 @@ class Executor
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return $subFieldASTs;
|
||||||
return self::executeFields($exeContext, $objectType, $result, $subFieldASTs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If a resolve function is not given, then a default resolve behavior is used
|
* If a resolve function is not given, then a default resolve behavior is used
|
||||||
* which takes the property of the source object of the same name as the field
|
* which takes the property of the source object of the same name as the field
|
||||||
|
@ -23,13 +23,20 @@ class FieldDefinition
|
|||||||
public $args;
|
public $args;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* source?: any,
|
* Callback for resolving field value given parent value.
|
||||||
* args?: ?{[argName: string]: any},
|
* Mutually exclusive with `map`
|
||||||
* info
|
|
||||||
*
|
*
|
||||||
* @var callable
|
* @var callable
|
||||||
*/
|
*/
|
||||||
public $resolve;
|
public $resolveFn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for mapping list of parent values to list of field values.
|
||||||
|
* Mutually exclusive with `resolve`
|
||||||
|
*
|
||||||
|
* @var callable
|
||||||
|
*/
|
||||||
|
public $mapFn;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string|null
|
* @var string|null
|
||||||
@ -62,6 +69,7 @@ class FieldDefinition
|
|||||||
'defaultValue' => Config::ANY
|
'defaultValue' => Config::ANY
|
||||||
], Config::KEY_AS_NAME),
|
], Config::KEY_AS_NAME),
|
||||||
'resolve' => Config::CALLBACK,
|
'resolve' => Config::CALLBACK,
|
||||||
|
'map' => Config::CALLBACK,
|
||||||
'description' => Config::STRING,
|
'description' => Config::STRING,
|
||||||
'deprecationReason' => Config::STRING,
|
'deprecationReason' => Config::STRING,
|
||||||
]);
|
]);
|
||||||
@ -97,7 +105,8 @@ class FieldDefinition
|
|||||||
{
|
{
|
||||||
$this->name = $config['name'];
|
$this->name = $config['name'];
|
||||||
$this->type = $config['type'];
|
$this->type = $config['type'];
|
||||||
$this->resolve = isset($config['resolve']) ? $config['resolve'] : null;
|
$this->resolveFn = isset($config['resolve']) ? $config['resolve'] : null;
|
||||||
|
$this->mapFn = isset($config['map']) ? $config['map'] : null;
|
||||||
$this->args = isset($config['args']) ? FieldArgument::createMap($config['args']) : [];
|
$this->args = isset($config['args']) ? FieldArgument::createMap($config['args']) : [];
|
||||||
|
|
||||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||||
|
@ -26,7 +26,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
|||||||
/**
|
/**
|
||||||
* @var callback
|
* @var callback
|
||||||
*/
|
*/
|
||||||
private $_resolveType;
|
private $_resolveTypeFn;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the interfaces to know about this implementation.
|
* Update the interfaces to know about this implementation.
|
||||||
@ -52,14 +52,14 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
|||||||
FieldDefinition::getDefinition(),
|
FieldDefinition::getDefinition(),
|
||||||
Config::KEY_AS_NAME
|
Config::KEY_AS_NAME
|
||||||
),
|
),
|
||||||
'resolveType' => Config::CALLBACK,
|
'resolveType' => Config::CALLBACK, // function($value, ResolveInfo $info) => ObjectType
|
||||||
'description' => Config::STRING
|
'description' => Config::STRING
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->name = $config['name'];
|
$this->name = $config['name'];
|
||||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||||
$this->_fields = !empty($config['fields']) ? FieldDefinition::createMap($config['fields']) : [];
|
$this->_fields = !empty($config['fields']) ? FieldDefinition::createMap($config['fields']) : [];
|
||||||
$this->_resolveType = isset($config['resolveType']) ? $config['resolveType'] : null;
|
$this->_resolveTypeFn = isset($config['resolveType']) ? $config['resolveType'] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -104,7 +104,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
|||||||
*/
|
*/
|
||||||
public function getObjectType($value, ResolveInfo $info)
|
public function getObjectType($value, ResolveInfo $info)
|
||||||
{
|
{
|
||||||
$resolver = $this->_resolveType;
|
$resolver = $this->_resolveTypeFn;
|
||||||
return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this);
|
return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ class ObjectType extends Type implements OutputType, CompositeType
|
|||||||
/**
|
/**
|
||||||
* @var callable
|
* @var callable
|
||||||
*/
|
*/
|
||||||
public $resolveField;
|
public $resolveFieldFn;
|
||||||
|
|
||||||
public function __construct(array $config)
|
public function __construct(array $config)
|
||||||
{
|
{
|
||||||
@ -77,7 +77,7 @@ class ObjectType extends Type implements OutputType, CompositeType
|
|||||||
|
|
||||||
$this->name = $config['name'];
|
$this->name = $config['name'];
|
||||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||||
$this->resolveField = isset($config['resolveField']) ? $config['resolveField'] : null;
|
$this->resolveFieldFn = isset($config['resolveField']) ? $config['resolveField'] : null;
|
||||||
$this->_config = $config;
|
$this->_config = $config;
|
||||||
|
|
||||||
if (isset($config['interfaces'])) {
|
if (isset($config['interfaces'])) {
|
||||||
|
@ -25,7 +25,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
|||||||
Config::validate($config, [
|
Config::validate($config, [
|
||||||
'name' => Config::STRING | Config::REQUIRED,
|
'name' => Config::STRING | Config::REQUIRED,
|
||||||
'types' => Config::arrayOf(Config::OBJECT_TYPE | Config::REQUIRED),
|
'types' => Config::arrayOf(Config::OBJECT_TYPE | Config::REQUIRED),
|
||||||
'resolveType' => Config::CALLBACK,
|
'resolveType' => Config::CALLBACK, // function($value, ResolveInfo $info) => ObjectType
|
||||||
'description' => Config::STRING
|
'description' => Config::STRING
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -66,16 +66,19 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
|||||||
$this->_possibleTypeNames[$possibleType->name] = true;
|
$this->_possibleTypeNames[$possibleType->name] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $this->_possibleTypeNames[$type->name] === true;
|
return isset($this->_possibleTypeNames[$type->name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ObjectType $value
|
* @param ObjectType $value
|
||||||
|
* @param ResolveInfo $info
|
||||||
|
*
|
||||||
* @return Type
|
* @return Type
|
||||||
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function getObjectType($value, ResolveInfo $info)
|
public function getObjectType($value, ResolveInfo $info)
|
||||||
{
|
{
|
||||||
$resolver = $this->_resolveType;
|
$resolver = $this->_resolveType;
|
||||||
return $resolver ? call_user_func($resolver, $value) : Type::getTypeOf($value, $info, $this);
|
return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,12 @@ use GraphQL\FormattedError;
|
|||||||
use GraphQL\Language\Parser;
|
use GraphQL\Language\Parser;
|
||||||
use GraphQL\Language\SourceLocation;
|
use GraphQL\Language\SourceLocation;
|
||||||
use GraphQL\Schema;
|
use GraphQL\Schema;
|
||||||
use GraphQL\Type\Definition\Config;
|
use GraphQL\Type\Definition\InterfaceType;
|
||||||
use GraphQL\Type\Definition\ObjectType;
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
use GraphQL\Type\Definition\ResolveInfo;
|
||||||
use GraphQL\Type\Definition\Type;
|
use GraphQL\Type\Definition\Type;
|
||||||
|
use GraphQL\Type\Definition\UnionType;
|
||||||
|
use GraphQL\Utils;
|
||||||
|
|
||||||
class ExecutorTest extends \PHPUnit_Framework_TestCase
|
class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||||
{
|
{
|
||||||
@ -164,11 +167,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
|||||||
'c' => ['type' => Type::string(), 'resolve' => function () {
|
'c' => ['type' => Type::string(), 'resolve' => function () {
|
||||||
return 'Cherry';
|
return 'Cherry';
|
||||||
}],
|
}],
|
||||||
'deep' => ['type' => function () use (&$Type) {
|
'deep' => [
|
||||||
|
'type' => function () use (&$Type) {
|
||||||
return $Type;
|
return $Type;
|
||||||
}, 'resolve' => function () {
|
},
|
||||||
|
'resolve' => function () {
|
||||||
return [];
|
return [];
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
$schema = new Schema($Type);
|
$schema = new Schema($Type);
|
||||||
@ -503,72 +509,264 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
|||||||
];
|
];
|
||||||
|
|
||||||
$this->assertEquals($expected, $result->toArray());
|
$this->assertEquals($expected, $result->toArray());
|
||||||
|
|
||||||
/*
|
|
||||||
var query = parse('{ field(a: true, c: false, e: 0) }');
|
|
||||||
var result = await execute(schema, query);
|
|
||||||
|
|
||||||
expect(result).to.deep.equal({
|
|
||||||
data: {
|
|
||||||
field: '{"a":true,"c":false,"e":0}'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails when an isTypeOf check is not met', async () => {
|
|
||||||
class Special {
|
|
||||||
constructor(value) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotSpecial {
|
public function testExecutesMapCallbacksIfSet()
|
||||||
constructor(value) {
|
{
|
||||||
this.value = value;
|
$fooData = [
|
||||||
}
|
['field' => '1'],
|
||||||
}
|
['field' => null],
|
||||||
|
null,
|
||||||
|
['field' => '4'],
|
||||||
|
];
|
||||||
|
|
||||||
var SpecialType = new GraphQLObjectType({
|
$foo = new ObjectType([
|
||||||
name: 'SpecialType',
|
'name' => 'Foo',
|
||||||
isTypeOf(obj) {
|
'fields' => [
|
||||||
return obj instanceof Special;
|
'field' => [
|
||||||
},
|
'type' => Type::string(),
|
||||||
fields: {
|
'map' => function($listOfFoo, $args, $resolveInfo) use ($fooData) {
|
||||||
value: { type: GraphQLString }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var schema = new GraphQLSchema({
|
return Utils::map($listOfFoo, function($fooData) use ($args, $resolveInfo) {
|
||||||
query: new GraphQLObjectType({
|
return json_encode([
|
||||||
name: 'Query',
|
'value' => $fooData['field'] === null ? null : $fooData['field'] . 'x',
|
||||||
fields: {
|
'args' => $args,
|
||||||
specials: {
|
'gotResolveInfo' => $resolveInfo instanceof ResolveInfo
|
||||||
type: new GraphQLList(SpecialType),
|
|
||||||
resolve: rootValue => rootValue.specials
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
var query = parse('{ specials { value } }');
|
|
||||||
var value = {
|
|
||||||
specials: [ new Special('foo'), new NotSpecial('bar') ]
|
|
||||||
};
|
|
||||||
var result = await execute(schema, query, value);
|
|
||||||
|
|
||||||
expect(result.data).to.deep.equal({
|
|
||||||
specials: [
|
|
||||||
{ value: 'foo' },
|
|
||||||
null
|
|
||||||
]
|
|
||||||
});
|
|
||||||
expect(result.errors).to.have.lengthOf(1);
|
|
||||||
expect(result.errors).to.containSubset([
|
|
||||||
{ message:
|
|
||||||
'Expected value of type "SpecialType" but got: [object Object].',
|
|
||||||
locations: [ { line: 1, column: 3 } ] }
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
*/
|
},
|
||||||
|
'args' => [
|
||||||
|
'a' => ['type' => Type::boolean()],
|
||||||
|
'b' => ['type' => Type::boolean()],
|
||||||
|
'c' => ['type' => Type::int()]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$bar = new ObjectType([
|
||||||
|
'name' => 'Bar',
|
||||||
|
'fields' => [
|
||||||
|
'foo' => [
|
||||||
|
'type' => Type::listOf($foo),
|
||||||
|
'resolve' => function() use ($fooData) {
|
||||||
|
return $fooData;
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$schema = new Schema($bar);
|
||||||
|
|
||||||
|
$query = Parser::parse('{ foo { field(a: true, c: 0) } }');
|
||||||
|
$result = Executor::execute($schema, $query);
|
||||||
|
|
||||||
|
$expected = [
|
||||||
|
'data' => [
|
||||||
|
'foo' => [
|
||||||
|
['field' => '{"value":"1x","args":{"a":true,"c":0},"gotResolveInfo":true}'],
|
||||||
|
['field' => '{"value":null,"args":{"a":true,"c":0},"gotResolveInfo":true}'],
|
||||||
|
null,
|
||||||
|
['field' => '{"value":"4x","args":{"a":true,"c":0},"gotResolveInfo":true}'],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $result->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRespectsListsOfAbstractTypeWhenResolvingViaMap()
|
||||||
|
{
|
||||||
|
$type1 = null;
|
||||||
|
$type2 = null;
|
||||||
|
$type3 = null;
|
||||||
|
|
||||||
|
$resolveType = function($value) use (&$type1, &$type2, &$type3) {
|
||||||
|
switch ($value['type']) {
|
||||||
|
case 'Type1':
|
||||||
|
return $type1;
|
||||||
|
case 'Type2':
|
||||||
|
return $type2;
|
||||||
|
case 'Type3':
|
||||||
|
default:
|
||||||
|
return $type3;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$mapValues = function($typeValues, $args) {
|
||||||
|
return Utils::map($typeValues, function($value) use ($args) {
|
||||||
|
if (array_key_exists('foo', $value)) {
|
||||||
|
return json_encode([
|
||||||
|
'value' => $value,
|
||||||
|
'args' => $args,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$interface = new InterfaceType([
|
||||||
|
'name' => 'SomeInterface',
|
||||||
|
'fields' => [
|
||||||
|
'foo' => ['type' => Type::string()],
|
||||||
|
],
|
||||||
|
'resolveType' => $resolveType
|
||||||
|
]);
|
||||||
|
|
||||||
|
$type1 = new ObjectType([
|
||||||
|
'name' => 'Type1',
|
||||||
|
'fields' => [
|
||||||
|
'foo' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'map' => $mapValues
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'interfaces' => [$interface]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$type2 = new ObjectType([
|
||||||
|
'name' => 'Type2',
|
||||||
|
'fields' => [
|
||||||
|
'foo' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'map' => $mapValues
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'interfaces' => [$interface]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$type3 = new ObjectType([
|
||||||
|
'name' => 'Type3',
|
||||||
|
'fields' => [
|
||||||
|
'bar' => [
|
||||||
|
'type' => Type::listOf(Type::string()),
|
||||||
|
'map' => function($type3Values, $args) {
|
||||||
|
return Utils::map($type3Values, function($value) use ($args) {
|
||||||
|
return [
|
||||||
|
json_encode([
|
||||||
|
'value' => $value,
|
||||||
|
'args' => $args
|
||||||
|
])
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$union = new UnionType([
|
||||||
|
'name' => 'SomeUnion',
|
||||||
|
'types' => [$type1, $type3],
|
||||||
|
'resolveType' => $resolveType
|
||||||
|
]);
|
||||||
|
|
||||||
|
$complexType = new ObjectType([
|
||||||
|
'name' => 'ComplexType',
|
||||||
|
'fields' => [
|
||||||
|
'iface' => [
|
||||||
|
'type' => $interface
|
||||||
|
],
|
||||||
|
'ifaceList' => [
|
||||||
|
'type' => Type::listOf($interface)
|
||||||
|
],
|
||||||
|
'union' => [
|
||||||
|
'type' => $union
|
||||||
|
],
|
||||||
|
'unionList' => [
|
||||||
|
'type' => Type::listOf($union)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$type1values = [
|
||||||
|
['type' => 'Type1', 'foo' => 'str1'],
|
||||||
|
['type' => 'Type1'],
|
||||||
|
['type' => 'Type1', 'foo' => null],
|
||||||
|
];
|
||||||
|
|
||||||
|
$type2values = [
|
||||||
|
['type' => 'Type2', 'foo' => 'str1'],
|
||||||
|
['type' => 'Type2', 'foo' => null],
|
||||||
|
['type' => 'Type2'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$type3values = [
|
||||||
|
['type' => 'Type3', 'bar' => ['str1', 'str2']],
|
||||||
|
['type' => 'Type3', 'bar' => null],
|
||||||
|
];
|
||||||
|
|
||||||
|
$complexTypeValues = [
|
||||||
|
'iface' => $type1values[0],
|
||||||
|
'ifaceList' => array_merge($type1values, $type2values),
|
||||||
|
'union' => $type3values[0],
|
||||||
|
'unionList' => array_merge($type1values, $type3values)
|
||||||
|
];
|
||||||
|
|
||||||
|
$expected = [
|
||||||
|
'data' => [
|
||||||
|
'test' => [
|
||||||
|
'iface' => ['foo' => json_encode(['value' => $type1values[0], 'args' => []])],
|
||||||
|
'ifaceList' => [
|
||||||
|
['foo' => '{"value":{"type":"Type1","foo":"str1"},"args":[]}'],
|
||||||
|
['foo' => null],
|
||||||
|
['foo' => '{"value":{"type":"Type1","foo":null},"args":[]}'],
|
||||||
|
['foo' => '{"value":{"type":"Type2","foo":"str1"},"args":[]}'],
|
||||||
|
['foo' => '{"value":{"type":"Type2","foo":null},"args":[]}'],
|
||||||
|
['foo' => null],
|
||||||
|
],
|
||||||
|
'union' => [
|
||||||
|
'bar' => ['{"value":{"type":"Type3","bar":["str1","str2"]},"args":[]}']
|
||||||
|
],
|
||||||
|
'unionList' => [
|
||||||
|
['foo' => '{"value":{"type":"Type1","foo":"str1"},"args":[]}'],
|
||||||
|
['foo' => null],
|
||||||
|
['foo' => '{"value":{"type":"Type1","foo":null},"args":[]}'],
|
||||||
|
['bar' => ['{"value":{"type":"Type3","bar":["str1","str2"]},"args":[]}']],
|
||||||
|
['bar' => ['{"value":{"type":"Type3","bar":null},"args":[]}']],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$schema = new Schema(new ObjectType([
|
||||||
|
'name' => 'Query',
|
||||||
|
'fields' => [
|
||||||
|
'test' => [
|
||||||
|
'type' => $complexType,
|
||||||
|
'resolve' => function() use ($complexTypeValues) {
|
||||||
|
return $complexTypeValues;
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]));
|
||||||
|
|
||||||
|
$query = '{
|
||||||
|
test {
|
||||||
|
iface{foo},
|
||||||
|
ifaceList{foo}
|
||||||
|
union {
|
||||||
|
... on Type1 {
|
||||||
|
foo
|
||||||
|
}
|
||||||
|
... on Type3 {
|
||||||
|
bar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unionList {
|
||||||
|
... on Type1 {
|
||||||
|
foo
|
||||||
|
}
|
||||||
|
... on Type3 {
|
||||||
|
bar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}';
|
||||||
|
|
||||||
|
$query = Parser::parse($query);
|
||||||
|
$result = Executor::execute($schema, $query);
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $result->toArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user