Skip to content

Commit bd9951d

Browse files
spawniasimPod
andauthored
Improve docs for optimizing resolvers (#1101)
Co-authored-by: Simon Podlipsky <[email protected]>
1 parent a427591 commit bd9951d

21 files changed

+225
-274
lines changed

UPGRADE.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,6 @@ You can always switch to [custom error formatting](https://webonyx.github.io/gra
320320
It is disabled by default. To enable it, do the following
321321

322322
```php
323-
<?php
324323
use GraphQL\Executor\Executor;
325324
use GraphQL\Experimental\Executor\CoroutineExecutor;
326325

docs/concepts.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ It provides the following tools and primitives to describe your App as a hierarc
124124
Same example expressed in **graphql-php**:
125125

126126
```php
127-
<?php
128127
use GraphQL\Type\Definition\Type;
129128
use GraphQL\Type\Definition\ObjectType;
130129

docs/data-fetching.md

Lines changed: 119 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
## Overview
22

3-
GraphQL is data-storage agnostic. You can use any underlying data storage engine, including SQL or NoSQL database,
4-
plain files or in-memory data structures.
3+
GraphQL is data-storage agnostic. You can use any underlying data storage engine, including but not limited to
4+
SQL or NoSQL databases, plain files or in-memory data structures.
55

6-
In order to convert the GraphQL query to PHP array, **graphql-php** traverses query fields (using depth-first algorithm) and
7-
runs special **resolve** function on each field. This **resolve** function is provided by you as a part of
6+
In order to convert the GraphQL query to a PHP array, **graphql-php** traverses query fields (using depth-first algorithm)
7+
and runs the special **resolve** function on each field. This **resolve** function is provided by you as a part of the
88
[field definition](type-definitions/object-types.md#field-configuration-options) or [query execution call](executing-queries.md).
99

10-
Result returned by **resolve** function is directly included in the response (for scalars and enums)
10+
The result returned by the **resolve** function is directly included in the response (for scalars and enums)
1111
or passed down to nested fields (for objects).
1212

13-
Let's walk through an example. Consider following GraphQL query:
13+
Let's walk through an example. Consider the following GraphQL query:
1414

1515
```graphql
1616
{
@@ -23,150 +23,166 @@ Let's walk through an example. Consider following GraphQL query:
2323
}
2424
```
2525

26-
We need a Schema that can fulfill it. On the very top level the Schema contains Query type:
26+
We need a `Schema` that can fulfill it. On the very top level, the `Schema` contains the `Query` type:
2727

2828
```php
29-
<?php
3029
use GraphQL\Type\Definition\ObjectType;
3130

3231
$queryType = new ObjectType([
33-
'name' => 'Query',
34-
'fields' => [
35-
36-
'lastStory' => [
37-
'type' => $blogStoryType,
38-
'resolve' => function() {
39-
return [
40-
'id' => 1,
41-
'title' => 'Example blog post',
42-
'authorId' => 1
43-
];
44-
}
32+
'name' => 'Query',
33+
'fields' => [
34+
'lastStory' => [
35+
'type' => $blogStoryType,
36+
'resolve' => fn (): array => [
37+
'id' => 1,
38+
'title' => 'Example blog post',
39+
'authorId' => 1
40+
],
41+
]
4542
]
46-
47-
]
4843
]);
4944
```
5045

51-
As we see field **lastStory** has **resolve** function that is responsible for fetching data.
46+
As we see, the field **lastStory** has a **resolve** function that is responsible for fetching data.
5247

53-
In our example, we simply return array value, but in the real-world application you would query
54-
your database/cache/search index and return the result.
48+
In our example, we simply return a static value, but in the real-world application you would query
49+
your data source and return the result from there.
5550

56-
Since **lastStory** is of composite type **BlogStory** this result is passed down to fields of this type:
51+
Since **lastStory** is of composite type **BlogStory**, this result is passed down to fields of this type:
5752

5853
```php
59-
<?php
6054
use GraphQL\Type\Definition\Type;
6155
use GraphQL\Type\Definition\ObjectType;
6256

63-
$blogStoryType = new ObjectType([
64-
'name' => 'BlogStory',
65-
'fields' => [
66-
67-
'author' => [
68-
'type' => $userType,
69-
'resolve' => function($blogStory) {
70-
$users = [
71-
1 => [
72-
'id' => 1,
73-
'name' => 'Smith'
74-
],
75-
2 => [
76-
'id' => 2,
77-
'name' => 'Anderson'
78-
]
79-
];
80-
return $users[$blogStory['authorId']];
81-
}
57+
const USERS = [
58+
1 => [
59+
'id' => 1,
60+
'name' => 'Smith'
8261
],
83-
84-
'title' => [
85-
'type' => Type::string()
62+
2 => [
63+
'id' => 2,
64+
'name' => 'Anderson'
8665
]
66+
];
8767

88-
]
68+
$blogStoryType = new ObjectType([
69+
'name' => 'BlogStory',
70+
'fields' => [
71+
'author' => [
72+
'type' => $userType,
73+
'resolve' => fn (array $blogStory): array => USERS[$blogStory['authorId']],
74+
],
75+
'title' => [
76+
'type' => Type::string()
77+
]
78+
]
8979
]);
9080
```
9181

92-
Here **$blogStory** is the array returned by **lastStory** field above.
82+
Here **$blogStory** is the array returned by the **lastStory** field above.
9383

94-
Again: in the real-world applications you would fetch user data from data store by **authorId** and return it.
84+
Again: in real-world applications you would fetch user data from your data source by **authorId** and return it.
9585
Also, note that you don't have to return arrays. You can return any value, **graphql-php** will pass it untouched
9686
to nested resolvers.
9787

98-
But then the question appears - field **title** has no **resolve** option. How is it resolved?
99-
100-
There is a default resolver for all fields. When you define your own **resolve** function
101-
for a field you simply override this default resolver.
88+
But then the question appears - the field **title** has no **resolve** option, how is it resolved?
89+
When you define no custom resolver, [the default field resolver](#default-field-resolver) applies.
10290

10391
## Default Field Resolver
10492

105-
**graphql-php** provides following default field resolver:
93+
**graphql-php** provides the following default field resolver:
10694

10795
```php
108-
<?php
109-
function defaultFieldResolver($objectValue, $args, $context, \GraphQL\Type\Definition\ResolveInfo $info)
110-
{
111-
$fieldName = $info->fieldName;
112-
$property = null;
113-
114-
if (is_array($objectValue) || $objectValue instanceof \ArrayAccess) {
115-
if (isset($objectValue[$fieldName])) {
116-
$property = $objectValue[$fieldName];
117-
}
118-
} elseif (is_object($objectValue)) {
119-
if (isset($objectValue->{$fieldName})) {
120-
$property = $objectValue->{$fieldName};
121-
}
122-
}
96+
use GraphQL\Type\Definition\ResolveInfo;
12397

124-
return $property instanceof Closure
125-
? $property($objectValue, $args, $context, $info)
126-
: $property;
98+
function defaultFieldResolver($objectValue, array $args, $context, ResolveInfo $info)
99+
{
100+
$fieldName = $info->fieldName;
101+
$property = null;
102+
103+
if (is_array($objectValue) || $objectValue instanceof ArrayAccess) {
104+
if (isset($objectValue[$fieldName])) {
105+
$property = $objectValue[$fieldName];
106+
}
107+
} elseif (is_object($objectValue)) {
108+
if (isset($objectValue->{$fieldName})) {
109+
$property = $objectValue->{$fieldName};
110+
}
127111
}
112+
113+
return $property instanceof Closure
114+
? $property($objectValue, $args, $contextValue, $info)
115+
: $property;
116+
}
128117
```
129118

130-
As you see it returns value by key (for arrays) or property (for objects).
131-
If the value is not set - it returns **null**.
119+
It returns value by key (for arrays) or property (for objects).
120+
If the value is not set, it returns **null**.
132121

133-
To override the default resolver, pass it as an argument of [executeQuery](executing-queries.md) call.
122+
To override the default resolver, pass it as an argument to [executeQuery](executing-queries.md).
134123

135124
## Default Field Resolver per Type
136125

137126
Sometimes it might be convenient to set default field resolver per type. You can do so by providing
138127
[resolveField option in type config](type-definitions/object-types.md#configuration-options). For example:
139128

140129
```php
141-
<?php
142130
use GraphQL\Type\Definition\Type;
143131
use GraphQL\Type\Definition\ObjectType;
144132
use GraphQL\Type\Definition\ResolveInfo;
145133

146134
$userType = new ObjectType([
147-
'name' => 'User',
148-
'fields' => [
149-
150-
'name' => Type::string(),
151-
'email' => Type::string()
152-
153-
],
154-
'resolveField' => function(User $user, $args, $context, ResolveInfo $info) {
155-
switch ($info->fieldName) {
156-
case 'name':
157-
return $user->getName();
158-
case 'email':
159-
return $user->getEmail();
160-
default:
161-
return null;
162-
}
163-
}
135+
'name' => 'User',
136+
'fields' => [
137+
'name' => Type::string(),
138+
'email' => Type::string()
139+
],
140+
'resolveField' => function (User $user, array $args, $context, ResolveInfo $info) {
141+
switch ($info->fieldName) {
142+
case 'name': return $user->getName();
143+
case 'email': return $user->getEmail();
144+
default: return null;
145+
}
146+
},
164147
]);
165148
```
166149

167150
Keep in mind that **field resolver** has precedence over **default field resolver per type** which in turn
168151
has precedence over **default field resolver**.
169152

153+
## Optimize Resolvers
154+
155+
The 4th argument of **resolver** functions is an instance of [ResolveInfo](./class-reference.md#graphqltypedefinitionresolveinfo).
156+
It contains information that is useful for the field resolution process.
157+
158+
Depending on which data source is used, knowing which fields the client queried can be used to optimize
159+
the performance of a resolver. For example, an SQL query may only need to select the queried fields.
160+
161+
The following example limits which columns are selected from the database:
162+
163+
```php
164+
use GraphQL\Type\Definition\ObjectType;
165+
use GraphQL\Type\Definition\ResolveInfo;
166+
167+
$queryType = new ObjectType([
168+
'name' => 'Query',
169+
'fields' => [
170+
'lastStory' => [
171+
'type' => $storyType,
172+
'resolve' => function ($root, array $args, $context, ResolveInfo $resolveInfo): Story {
173+
// Fictitious API, use whatever database access your application/framework provides
174+
$builder = Story::builder();
175+
foreach ($resolveInfo->getFieldSelection() as $field => $_) {
176+
$builder->addSelect($field);
177+
}
178+
179+
return $builder->last();
180+
}
181+
]
182+
]
183+
]);
184+
```
185+
170186
## Solving N+1 Problem
171187

172188
Since: 0.9.0
@@ -175,7 +191,7 @@ One of the most annoying problems with data fetching is a so-called
175191
[N+1 problem](https://secure.phabricator.com/book/phabcontrib/article/n_plus_one/). <br>
176192
Consider following GraphQL query:
177193

178-
```
194+
```graphql
179195
{
180196
topStories(limit: 10) {
181197
title
@@ -195,11 +211,12 @@ when one batched query could be executed instead of 10 distinct queries.
195211
Here is an example of **BlogStory** resolver for field **author** that uses deferring:
196212

197213
```php
198-
<?php
199-
'resolve' => function($blogStory) {
214+
use GraphQL\Deferred;
215+
216+
'resolve' => function (array $blogStory): Deferred {
200217
MyUserBuffer::add($blogStory['authorId']);
201218

202-
return new GraphQL\Deferred(function () use ($blogStory) {
219+
return new Deferred(function () use ($blogStory): User {
203220
MyUserBuffer::loadBuffered();
204221

205222
return MyUserBuffer::get($blogStory['authorId']);
@@ -211,7 +228,7 @@ In this example, we fill up the buffer with 10 author ids first. Then **graphql-
211228
resolving other non-deferred fields until there are none of them left.
212229

213230
After that, it calls closures wrapped by `GraphQL\Deferred` which in turn load all buffered
214-
ids once (using SQL IN(?), Redis MGET or other similar tools) and returns final field value.
231+
ids once (using SQL `IN(?)`, Redis `MGET` or similar tools) and returns the final field value.
215232

216233
Originally this approach was advocated by Facebook in their [Dataloader](https://github.com/graphql/dataloader)
217234
project. This solution enables very interesting optimizations at no cost. Consider the following query:
@@ -252,7 +269,6 @@ To start using this feature, switch facade method for query execution from
252269
**executeQuery** to **promiseToExecute**:
253270

254271
```php
255-
<?php
256272
use GraphQL\GraphQL;
257273
use GraphQL\Executor\ExecutionResult;
258274

@@ -267,9 +283,7 @@ $promise = GraphQL::promiseToExecute(
267283
$fieldResolver = null,
268284
$validationRules = null
269285
);
270-
$promise->then(function(ExecutionResult $result) {
271-
return $result->toArray();
272-
});
286+
$promise->then(fn (ExecutionResult $result): array => $result->toArray());
273287
```
274288

275289
Where **$promiseAdapter** is an instance of:

0 commit comments

Comments
 (0)