From a2be92937e5c80d4e20c8ae829f8fecf486a9104 Mon Sep 17 00:00:00 2001 From: Vladimir Razuvaev Date: Thu, 17 Aug 2017 02:17:01 +0700 Subject: [PATCH] Documentation improvements (wip) --- docs/data-fetching.md | 2 +- docs/error-handling.md | 236 +++++++++++++++++++------------------ docs/executing-queries.md | 73 ++++++++++-- docs/getting-started.md | 8 +- docs/type-system/schema.md | 11 +- 5 files changed, 197 insertions(+), 133 deletions(-) diff --git a/docs/data-fetching.md b/docs/data-fetching.md index 5c86947..7b2b5e5 100644 --- a/docs/data-fetching.md +++ b/docs/data-fetching.md @@ -4,7 +4,7 @@ plain files or in-memory data structures. In order to convert GraphQL query to PHP array **graphql-php** traverses query fields (using depth-first algorithm) and runs special `resolve` function on each field. This `resolve` function is provided by you as a part of -[field definition](type-system/object-types/#field-configuration-options). +[field definition](type-system/object-types/#field-configuration-options) or [query execution call](executing-queries/#overview). Result returned by `resolve` function is directly included in response (for scalars and enums) or passed down to nested fields (for objects). diff --git a/docs/error-handling.md b/docs/error-handling.md index d8d3dbe..a4129ef 100644 --- a/docs/error-handling.md +++ b/docs/error-handling.md @@ -1,143 +1,145 @@ # Errors in GraphQL -Query execution process never throws exceptions. Instead all errors that occur during query execution -are caught, collected and included in response. +Query execution process never throws exceptions. Instead all errors are caught and collected in +[execution result](executing-queries/#execution-result). -There are 3 types of errors in GraphQL (Syntax, Validation and Execution errors): +Later `$result->toArray()` automatically converts these errors to array using default +error formatting. But you can apply [custom error filtering and formatting](#custom-error-filtering-and-formatting) +for your specific requirements. + +# Default Error formatting +By default each error entry is converted to associative array with following structure: -**Syntax** errors are returned in response when query has invalid syntax and could not be parsed. -Example output for invalid query `{hello` (missing bracket): ```php [ - 'errors' => [ - [ - 'message' => "Syntax Error GraphQL request (1:7) Expected Name, found \n\n1: {hello\n ^\n", - 'locations' => [ - ['line' => 1, 'column' => 7] - ] - ] - ] -] -``` - -**Validation** errors - returned in response when query has semantic errors. -Example output for invalid query `{unknownField}`: -```php -[ - 'errors' => [ - [ - 'message' => 'Cannot query field "unknownField" on type "Query".', - 'locations' => [ - ['line' => 1, 'column' => 2] - ] - ] - ] -] -``` - -**Execution** errors - included in response when some field resolver throws -(or returns unexpected value). Example output for query with exception thrown in -field resolver `{fieldWithException}`: -```php -[ - 'data' => [ - 'fieldWithException' => null + 'message' => 'Error message', + 'category' => 'graphql', + 'locations' => [ + ['line' => 1, 'column' => 2] ], - 'errors' => [ - [ - 'message' => 'Exception message thrown in field resolver', - 'locations' => [ - ['line' => 1, 'column' => 2] - ], - 'path': [ - 'fieldWithException' - ] - ] + 'path': [ + 'listField', + 0, + 'fieldWithException' ] ] ``` +Entry at key **locations** points to character in query string which caused the error. +In some cases (like deep fragment fields) locations will include several entries to track down path to +field with error in query. -Obviously when **Syntax** or **Validation** error is detected - process is interrupted and query is not -executed. In such scenarios response only contains **errors**, but not **data**. +Entry at key **path** exists only for errors caused by exceptions thrown in resolvers. It contains path +from the very root field to actual field value producing an error +(including indexes for list types and field names for composite types). -GraphQL is forgiving to **Execution** errors which occur in resolvers of nullable fields. -If such field throws or returns unexpected value the value of the field in response will be simply -replaced with `null` and error entry will be added to response. +**Internal errors** -If exception is thrown in non-null field - error bubbles up to first nullable field. This nullable field is -replaced with `null` and error entry is added to response. If all fields up to the root are non-null - -**data** entry will be removed from response and only **errors** key will be presented. +As of version **0.10.0** all exceptions thrown in resolvers are reported with generic message **"Internal server error"**. +This is done to avoid information leak in production environments (e.g. database connection errors, file access errors, etc). -# Debugging tools - -Each error entry contains pointer to line and column in original query string which caused -the error: - -```php -'locations' => [ - ['line' => 1, 'column' => 2] -] -``` - - GraphQL clients like **Relay** or **GraphiQL** leverage this information to highlight -actual piece of query containing error. - -In some cases (like deep fragment fields) locations will include several entries to track down the -path to field with error in query. - -**Execution** errors also contain **path** from the very root field to actual field value producing -an error (including indexes for array types and fieldNames for object types). So in complex situation -this path could look like this: - -```php -'path' => [ - 'lastStoryPosted', - 'author', - 'friends', - 3 - 'fieldWithException' -] -``` - -# Custom Error Formatting - -If you want to apply custom formatting to errors - use **GraphQL::executeAndReturnResult()** instead -of **GraphQL::execute()**. - -It has exactly the same [signature](executing-queries/), but instead of array it -returns `GraphQL\Executor\ExecutionResult` instance which holds errors in public **$errors** -property and data in **$data** property. - -Each entry of **$errors** array contains instance of `GraphQL\Error\Error` which wraps original -exceptions thrown by resolvers. To access original exceptions use `$error->getPrevious()` method. -But note that previous exception is only available for **Execution** errors and will be `null` -for **Syntax** or **Validation** errors. +Only exceptions implementing interface `GraphQL\Error\ClientAware` and claiming themselves as **safe** will +be reported with full error message. For example: +```php +use GraphQL\Error\ClientAware; + +class MySafeException extends \Exception implements ClientAware +{ + public function isClientSafe() + { + return true; + } + + public function getCategory() + { + return 'businessLogic'; + } +} +``` +When such exception is thrown it will be reported with full error message: +```php +[ + 'message' => 'My reported error', + 'category' => 'businessLogic', + 'locations' => [ + ['line' => 10, 'column' => 2] + ], + 'path': [ + 'path', + 'to', + 'fieldWithException' + ] +] +``` + +To change default **"Internal server error"** message to something else, use: +``` +GraphQL\Error\FormattedError::setInternalErrorMessage("Unexpected error"); +``` + +#Debugging tools + +During development or debugging use `$result->toArray(true)` to add **debugMessage** key to +each formatted error entry. If you also want to add exception trace - pass flags instead: + +``` +use GraphQL\Error\FormattedError; +$debug = FormattedError::INCLUDE_DEBUG_MESSAGE | FormattedError::INCLUDE_TRACE; +$result = GraphQL::executeQuery(/*args*/)->toArray($debug); +``` + +This will make each error entry to look like this: +```php +[ + 'message' => 'Internal server error', + 'debugMessage' => 'Actual exception message', + 'category' => 'internal', + 'locations' => [ + ['line' => 10, 'column' => 2] + ], + 'path': [ + 'listField', + 0, + 'fieldWithException' + ], + 'trace' => [ + /* Formatted original exception trace */ + ] +] +``` + +# Custom Error Handling and Formatting +It is possible to define custom **formatter** and **handler** for result errors. + +**Formatter** is responsible for converting instances of `GraphQL\Error\Error` to array. +**Handler** is useful for error filtering and logging. + +For example these are default formatter and handler: ```php -$result = GraphQL::executeAndReturnResult() - ->setErrorFormatter(function(GraphQL\Error\Error $err) { - $resolverException = $err->getPrevious(); +use GraphQL\Error\Error; +use GraphQL\Error\FormattedError; - if ($resolverException instanceof MyResolverException) { - $formattedError = [ - 'message' => $resolverException->getMessage(), - 'code' => $resolverException->getCode() - ]; - } else { - $formattedError = [ - 'message' => $err->getMessage() - ]; - } - return $formattedError; - }) +$myErrorFormatter = function(Error $error) { + return FormattedError::createFromException($error); +}; + +$myErrorHandler = function(array $errors, callable $formatter) { + return array_map($formatter, $errors); +}; + +$result = GraphQL::executeQuery(/* $args */) + ->setErrorFormatter($myErrorFormatter) + ->setErrorHandler($myErrorHandler) ->toArray(); ``` +You may also re-throw exceptions in result handler for debugging, etc. + # Schema Errors So far we only covered errors which occur during query execution process. But schema definition can -also throw if there is an error in one of type definitions. +also throw `GraphQL\Error\InvariantViolation` if there is an error in one of type definitions. Usually such errors mean that there is some logical error in your schema and it is the only case when it makes sense to return `500` error code for GraphQL endpoint: @@ -148,7 +150,7 @@ try { // ... ]); - $body = GraphQL::execute($schema, $query); + $body = GraphQL::executeQuery($schema, $query); $status = 200; } catch(\Exception $e) { $body = json_encode([ diff --git a/docs/executing-queries.md b/docs/executing-queries.md index c676c36..3df5435 100644 --- a/docs/executing-queries.md +++ b/docs/executing-queries.md @@ -2,26 +2,36 @@ Query execution is a complex process involving multiple steps, including query **parsing**, **validating** and finally **executing** against your [schema](type-system/schema/). -**graphql-php** provides convenient facade for this process in class `GraphQL\GraphQL`: +**graphql-php** provides convenient facade for this process in class +[`GraphQL\GraphQL`](/reference/#graphqlgraphql): ```php use GraphQL\GraphQL; -$result = GraphQL::execute( +$result = GraphQL::executeQuery( $schema, $queryString, $rootValue = null, $contextValue = null, $variableValues = null, - $operationName = null + $operationName = null, + $fieldResolver = null, + $validationRules = null, + $promiseAdapter = null ); ``` -Method returns `array` with **data** and **errors** keys, as described by -[GraphQL specs](http://facebook.github.io/graphql/#sec-Response-Format). -This array is suitable for further serialization (e.g. using `json_encode`). -See also section on [error handling](error-handling/). +It returns an instance of [`GraphQL\Executor\ExecutionResult`](/reference/#graphqlexecutorexecutionresult) +which can be easily converted to array: +```php +$serializableResult = $result->toArray(); +``` + +Returned array contains **data** and **errors** keys, as described by +[GraphQL spec](http://facebook.github.io/graphql/#sec-Response-Format). +This array is suitable for further serialization (e.g. using `json_encode`). +See also section on [error handling and formatting](error-handling/). Description of method arguments: @@ -33,10 +43,39 @@ rootValue | `mixed` | Any value that represents a root of your data graph. It i contextValue | `mixed` | Any value that holds information shared between all field resolvers. Most often they use it to pass currently logged in user, locale details, etc.

It will be available as 3rd argument in all field resolvers. (see section on [Field Definitions](type-system/object-types/#field-configuration-options) for reference) **graphql-php** never modifies this value and passes it *as is* to all underlying resolvers. variableValues | `array` | Map of variable values passed along with query string. See section on [query variables on official GraphQL website](http://graphql.org/learn/queries/#variables) operationName | `string` | Allows the caller to specify which operation in queryString will be run, in cases where queryString contains multiple top-level operations. +fieldResolver | `callable` | A resolver function to use when one is not provided by the schema. If not provided, the [default field resolver is used](data-fetching/#default-field-resolver). +validationRules | `array` | A set of rules for query validation step. Default value is all available rules. Empty array would allow to skip query validation (may be convenient for persisted queries which are validated before persisting and assumed valid during execution) +promiseAdapter | `GraphQL\Executir\Promise\PromiseAdapter` | Adapter for async-enabled PHP platforms like ReactPHP (read about [async platforms integration](data-fetching/#async-php)) + +# Execution Result +```php +namespace GraphQL\Executor; + +class ExecutionResult +{ + /** + * @var array + */ + public $data; + + /** + * @var GraphQL\Error\Error[] + */ + public $errors; + + /** + * + */ + public setErrorsHandler(callable $errorsHandler); + + public setErrorFormatter(callable $errorsHandler); +} +``` + # Parsing Following reading describes implementation details of query execution process. It may clarify some -internals of GraphQL but is not required in order to use it. Feel free to skip to next section +internals of GraphQL but is not required to use it. Feel free to skip to next section on [Error Handling](error-handling/) for essentials. TODOC @@ -46,3 +85,21 @@ TODOC # Executing TODOC + +# Errors explained +There are 3 types of errors in GraphQL: + +- **Syntax**: query has invalid syntax and could not be parsed; +- **Validation**: query is incompatible with type system (e.g. unknown field is requested); +- **Execution**: occurs when some field resolver throws (or returns unexpected value). + +Obviously when **Syntax** or **Validation** error is detected - process is interrupted and query is not +executed. + +GraphQL is forgiving to **Execution** errors which occur in resolvers of nullable fields. +If such field throws or returns unexpected value the value of the field in response will be simply +replaced with `null` and error entry will be registered. + +If exception is thrown in non-null field - error bubbles up to first nullable field. This nullable field is +replaced with `null` and error entry is added to response. If all fields up to the root are non-null - +**data** entry will be removed from response and only **errors** key will be presented. diff --git a/docs/getting-started.md b/docs/getting-started.md index af08080..cd4e77c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -88,11 +88,13 @@ $variableValues = isset($input['variables']) ? $input['variables'] : null; try { $rootValue = ['prefix' => 'You said: ']; - $result = GraphQL::execute($schema, $query, $rootValue, null, $variableValues); + $result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variableValues); } catch (\Exception $e) { $result = [ - 'error' => [ - 'message' => $e->getMessage() + 'errors' => [ + [ + 'message' => $e->getMessage() + ] ] ]; } diff --git a/docs/type-system/schema.md b/docs/type-system/schema.md index 2991d53..7c84440 100644 --- a/docs/type-system/schema.md +++ b/docs/type-system/schema.md @@ -2,16 +2,18 @@ Schema is a container of your type hierarchy, which accepts root types in constructor and provides methods for receiving information about your types to internal GrahpQL tools. -In **graphql-php** schema is an instance of `GraphQL\Type\Schema` which accepts configuration array -in constructor: +In **graphql-php** schema is an instance of [`GraphQL\Type\Schema`](/reference/#graphqltypeschema) +which accepts configuration array in constructor: ```php +use GraphQL\Type\Schema; + $schema = new Schema([ 'query' => $queryType, 'mutation' => $mutationType, ]); ``` -See possible constructor options [below](#configuration-options) +See possible constructor options [below](#configuration-options). # Query and Mutation types Schema consists of two root types: @@ -76,7 +78,8 @@ Field names of Mutation type are usually verbs and they almost always have argum with complex input values (see [Input Types](input-types/) for details). # Configuration Options -Schema constructor expects an array with following options: +Schema constructor expects an instance of [`GraphQL\Type\SchemaConfig`](/reference/#graphqltypeschemaconfig) +or an array with following options: Option | Type | Notes ------------ | -------- | -----