From 8e39b3d88d2456d229519475f4eff21783be6f6a Mon Sep 17 00:00:00 2001 From: vladar Date: Wed, 14 Dec 2016 20:19:12 +0700 Subject: [PATCH] Added entry on data fetching to docs --- docs/data-fetching.md | 243 +++++++++++++++++++++++++++++++++++++++++ docs/error-handling.md | 6 +- mkdocs.yml | 2 +- 3 files changed, 247 insertions(+), 4 deletions(-) diff --git a/docs/data-fetching.md b/docs/data-fetching.md index e69de29..e1bf6f4 100644 --- a/docs/data-fetching.md +++ b/docs/data-fetching.md @@ -0,0 +1,243 @@ +# Overview +GraphQL is data-storage agnostic. You can use any underlying data storage engine, including SQL or NoSQL database, +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). + +Result returned by `resolve` function is directly included in response (for scalars and enums) +or passed down to nested fields (for objects). + +Let's walk through an example. Consider following GraphQL query: + +```graphql +{ + lastStory { + title + author { + name + } + } +} +``` + +We need Schema that can fulfill it. On the very top level Schema contains Query type: + +```php +$queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + + 'lastStory' => [ + 'type' => $blogStoryType, + 'resolve' => function() { + return [ + 'id' => 1, + 'title' => 'Example blog post', + 'authorId' => 1 + ]; + } + ] + + ] +]); +``` + +As we see field `lastStory` has `resolve` function that is responsible for fetching data. + +In our example we simply return array value, but in real-world application you would query +your database/cache/search index and return result. + +Since `lastStory` is of complex type `BlogStory` this result is passed down to fields of this type: + +```php +$blogStoryType = new ObjectType([ + 'name' => 'BlogStory', + 'fields' => [ + + 'author' => [ + 'type' => $userType, + 'resolve' => function($blogStory) { + $users = [ + 1 => [ + 'id' => 1, + 'name' => 'Smith' + ], + 2 => [ + 'id' => 2, + 'name' => 'Anderson' + ] + ]; + return $users[$blogStory['authorId']]; + } + ], + + 'title' => [ + 'type' => Type::string() + ] + + ] +]); +``` + +Here `$blogStory` is the array returned by `lastStory` field above. + +Again: in real-world applications you would fetch user data from datastore by `authorId` and return it. +Also note that you don't have to return arrays. You can return any value, **graphql-php** will pass it untouched +to nested resolvers. + +But then the question appears - field `title` has no `resolve` option. How is it resolved? + +The answer is: there is default resolver for all fields. When you define your own `resolve` function +for a field you simply override this default resolver. + +# Default Field Resolver +**graphql-php** provides following default field resolver: +```php +function defaultFieldResolver($source, $args, $context, ResolveInfo $info) +{ + $fieldName = $info->fieldName; + $property = null; + + if (is_array($source) || $source instanceof \ArrayAccess) { + if (isset($source[$fieldName])) { + $property = $source[$fieldName]; + } + } else if (is_object($source)) { + if (isset($source->{$fieldName})) { + $property = $source->{$fieldName}; + } + } + + return $property instanceof \Closure ? $property($source, $args, $context) : $property; +} +``` + +As you see it returns value by key (for arrays) or property (for objects). If value is not set - it returns `null`. + +To override default resolver - use: +```php +GraphQL\GraphQL::setDefaultFieldResolver($myResolverCallback); +``` + +# Default Field Resolver per Type +Sometimes it might be convenient to set default field resolver per type. You can do so by providing +[resolveField option in type config](type-system/object-types/#configuration-options). For example: + +```php +$userType = new ObjectType([ + 'name' => 'User', + 'fields' => [ + + 'name' => Type::string(), + 'email' => Type::string() + + ], + 'resolveField' => function(User $user, $args, $context, ResolveInfo $info) { + switch ($info->fieldName) { + case 'name': + return $user->getName(); + case 'email': + return $user->getEmail(); + default: + return null; + } + } +]); +``` + +Keep in mind that **field resolver** has precedence over **default field resolver per type** which in turn + has precedence over **default field resolver**. + + +# Solving N+1 Problem +One of the most annoying problems with data fetching is so-called [N+1 problem](https://secure.phabricator.com/book/phabcontrib/article/n_plus_one/). + +Consider following GraphQL query: +``` +{ + topStories(limit: 10) { + title + author { + name + email + } + } +} +``` + +Naive field resolution process would require up to 10 calls to underlying data store to fetch authors for all 10 stories. + +**graphql-php** provides tools to mitigate this problem: it allows you to defer actual field resolution to later stage +when one batched query could be executed instead of 10 distinct queries. + +Here is an example of `BlogStory` resolver for field `author` that uses deferring: +```php +'resolve' => function($blogStory) { + MyUserBuffer::add($blogStory['authorId']); + + return new GraphQL\Deferred(function () use ($blogStory) { + MyUserBuffer::loadOnce(); + return MyUserBuffer::get($blogStory['authorId']); + }); +} +``` + +In this example we fill up buffer with 10 author ids first. Then **graphql-php** continues +resolving other non-deferred fields until there are none of them left. + +After that it calls `Closures` wrapped by `GraphQL\Deferred` which in turn load all buffered +ids once (using SQL IN(?), Redis MGET or other similar tools) and return final field value. + +Originally this approach was advocated by Facebook in their [Dataloader](https://github.com/facebook/dataloader) +project. + +This solution enables very interesting optimizations at no cost. Consider following query: + +``` +{ + topStories(limit: 10) { + author { + email + } + } + category { + stories(limit: 10) { + author { + email + } + } + } +} +``` + +Even if `author` field is located on different levels of query - it can be buffered in the same buffer. +In this example only one query will be executed for all story authors comparing to 20 queries +in naive implementation. + +# Async PHP +If your project runs in environment that supports async operations +(like `HHVM`, `ReactPHP`, `Icicle.io`, `appserver.io` `PHP threads`, etc) you can leverage +the power of your platform to resolve fields asynchronously. + +The only requirement: your platform must support the concept of Promises compatible with +[Promises A+](https://promisesaplus.com/) specification. + +To enable async support - set adapter for promises: +``` +GraphQL\GraphQL::setPromiseAdapter($adapter); +``` + +Where `$adapter` is an instance of class implementing `GraphQL\Executor\Promise\PromiseAdapter` interface. + +Then in your `resolve` functions you should return `Promises` of your platform instead of +`GraphQL\Deferred` instances. + +Platforms supported out of the box: + +* [ReactPHP](https://github.com/reactphp/react) (requires **react/promise** as composer dependency): + `GraphQL\GraphQL::setPromiseAdapter(new GraphQL\Executor\Promise\Adapter\ReactPromiseAdapter());` +* HHVM: TODO + +To integrate other platform - implement `GraphQL\Executor\Promise\PromiseAdapter` interface. diff --git a/docs/error-handling.md b/docs/error-handling.md index c400491..998dbb1 100644 --- a/docs/error-handling.md +++ b/docs/error-handling.md @@ -64,9 +64,9 @@ GraphQL is forgiving to **Execution** errors which occur in resolvers of nullabl 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. -If exception is thrown in non-null field - it will be bubbled up to first nullable field which will -be replaced with `null` (and error entry added to response). If all fields up to the root are non-null - -**data** entry will be removed from n response and only **errors** key will be presented. +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. # Debugging tools diff --git a/mkdocs.yml b/mkdocs.yml index 3f068e4..55b69a3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,8 +14,8 @@ pages: - Directives: type-system/directives.md - Schema: type-system/schema.md - Executing Queries: executing-queries.md +- Fetching Data: data-fetching.md - Handling Errors: error-handling.md -# - Fetching Data: data-fetching.md # - Best Practices: best-practices.md - Complementary Tools: complementary-tools.md theme: readthedocs