diff --git a/src/Executor/ExecutionContext.php b/src/Executor/ExecutionContext.php index 7619085..c068bc9 100644 --- a/src/Executor/ExecutionContext.php +++ b/src/Executor/ExecutionContext.php @@ -44,12 +44,17 @@ class ExecutionContext */ public $variableValues; + /** + * @var callable + */ + public $fieldResolver; + /** * @var array */ public $errors; - public function __construct($schema, $fragments, $root, $contextValue, $operation, $variables, $errors) + public function __construct($schema, $fragments, $root, $contextValue, $operation, $variables, $errors, $fieldResolver) { $this->schema = $schema; $this->fragments = $fragments; @@ -58,6 +63,7 @@ class ExecutionContext $this->operation = $operation; $this->variableValues = $variables; $this->errors = $errors ?: []; + $this->fieldResolver = $fieldResolver; } public function addError(Error $error) diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index b7fa041..061a3ce 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -89,9 +89,18 @@ class Executor * @param $contextValue * @param array|\ArrayAccess $variableValues * @param null $operationName + * @param callable $fieldResolver * @return ExecutionResult|Promise */ - public static function execute(Schema $schema, DocumentNode $ast, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null) + public static function execute( + Schema $schema, + DocumentNode $ast, + $rootValue = null, + $contextValue = null, + $variableValues = null, + $operationName = null, + $fieldResolver = null + ) { if (null !== $variableValues) { Utils::invariant( @@ -106,7 +115,15 @@ class Executor ); } - $exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $contextValue, $variableValues, $operationName); + $exeContext = self::buildExecutionContext( + $schema, + $ast, + $rootValue, + $contextValue, + $variableValues, + $operationName, + $fieldResolver + ); $promiseAdapter = self::getPromiseAdapter(); $executor = new self($exeContext, $promiseAdapter); @@ -129,6 +146,8 @@ class Executor * @param $contextValue * @param $rawVariableValues * @param string $operationName + * @param callable $fieldResolver + * * @return ExecutionContext * @throws Error */ @@ -138,7 +157,8 @@ class Executor $rootValue, $contextValue, $rawVariableValues, - $operationName = null + $operationName = null, + $fieldResolver = null ) { $errors = []; @@ -183,7 +203,16 @@ class Executor $rawVariableValues ?: [] ); - $exeContext = new ExecutionContext($schema, $fragments, $rootValue, $contextValue, $operation, $variableValues, $errors); + $exeContext = new ExecutionContext( + $schema, + $fragments, + $rootValue, + $contextValue, + $operation, + $variableValues, + $errors, + $fieldResolver ?: self::$defaultFieldResolver + ); return $exeContext; } @@ -625,7 +654,7 @@ class Executor } else if (isset($parentType->resolveFieldFn)) { $resolveFn = $parentType->resolveFieldFn; } else { - $resolveFn = self::$defaultFieldResolver; + $resolveFn = $this->exeContext->fieldResolver; } // The resolve function's optional third argument is a context value that diff --git a/src/GraphQL.php b/src/GraphQL.php index 1572c82..f7f7526 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -17,16 +17,60 @@ use GraphQL\Validator\Rules\QueryComplexity; class GraphQL { /** + * This is the primary entry point function for fulfilling GraphQL operations + * by parsing, validating, and executing a GraphQL document along side a + * GraphQL schema. + * + * More sophisticated GraphQL servers, such as those which persist queries, + * may wish to separate the validation and execution phases to a static time + * tooling step, and a server runtime step. + * + * schema: + * The GraphQL type system to use when validating and executing a query. + * requestString: + * A GraphQL language formatted string representing the requested operation. + * rootValue: + * The value provided as the first argument to resolver functions on the top + * level type (e.g. the query object type). + * variableValues: + * A mapping of variable name to runtime value to use for all variables + * defined in the requestString. + * operationName: + * The name of the operation to use if requestString contains multiple + * possible operations. Can be omitted if requestString contains only + * one operation. + * fieldResolver: + * A resolver function to use when one is not provided by the schema. + * If not provided, the default field resolver is used (which looks for a + * value or method on the source value with the field's name). + * * @param Schema $schema * @param string|DocumentNode $requestString * @param mixed $rootValue * @param array|null $variableValues * @param string|null $operationName + * @param callable $fieldResolver * @return Promise|array */ - public static function execute(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null) + public static function execute( + Schema $schema, + $requestString, + $rootValue = null, + $contextValue = null, + $variableValues = null, + $operationName = null, + $fieldResolver = null + ) { - $result = self::executeAndReturnResult($schema, $requestString, $rootValue, $contextValue, $variableValues, $operationName); + $result = self::executeAndReturnResult( + $schema, + $requestString, + $rootValue, + $contextValue, + $variableValues, + $operationName, + $fieldResolver + ); if ($result instanceof ExecutionResult) { return $result->toArray(); @@ -40,14 +84,26 @@ class GraphQL } /** + * Same as `execute`, but returns instance of ExecutionResult instead of array, + * which can be used for custom error formatting or adding extensions to response + * * @param Schema $schema * @param string|DocumentNode $requestString * @param mixed $rootValue * @param array|null $variableValues * @param string|null $operationName + * @param callable $fieldResolver * @return ExecutionResult|Promise */ - public static function executeAndReturnResult(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null) + public static function executeAndReturnResult( + Schema $schema, + $requestString, + $rootValue = null, + $contextValue = null, + $variableValues = null, + $operationName = null, + $fieldResolver = null + ) { try { if ($requestString instanceof DocumentNode) { @@ -66,7 +122,15 @@ class GraphQL if (!empty($validationErrors)) { return new ExecutionResult(null, $validationErrors); } else { - return Executor::execute($schema, $documentNode, $rootValue, $contextValue, $variableValues, $operationName); + return Executor::execute( + $schema, + $documentNode, + $rootValue, + $contextValue, + $variableValues, + $operationName, + $fieldResolver + ); } } catch (Error $e) { return new ExecutionResult(null, [$e]); diff --git a/tests/Executor/ExecutorTest.php b/tests/Executor/ExecutorTest.php index fb30aab..ecc2403 100644 --- a/tests/Executor/ExecutorTest.php +++ b/tests/Executor/ExecutorTest.php @@ -972,6 +972,44 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase } } + /** + * @it uses a custom field resolver + */ + public function testUsesACustomFieldResolver() + { + $query = Parser::parse('{ foo }'); + + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'foo' => ['type' => Type::string()] + ] + ]) + ]); + + // For the purposes of test, just return the name of the field! + $customResolver = function ($source, $args, $context, ResolveInfo $info) { + return $info->fieldName; + }; + + $result = Executor::execute( + $schema, + $query, + null, + null, + null, + null, + $customResolver + ); + + $expected = [ + 'data' => ['foo' => 'foo'] + ]; + + $this->assertEquals($expected, $result->toArray()); + } + public function testSubstitutesArgumentWithDefaultValue() { $schema = new Schema([