Skip to content

Commit 797c493

Browse files
authored
Merge pull request #12 from mcg-web/supports-webonyx-graphql-sync-promise-adapter
Supports webonyx graphql sync promise adapter
2 parents ed7b52b + 95d16c5 commit 797c493

File tree

22 files changed

+952
-32
lines changed

22 files changed

+952
-32
lines changed

README.md

Lines changed: 193 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ composer require "overblog/dataloader-php"
2323

2424
To get started, create a `DataLoader` object.
2525

26-
Batching is not an advanced feature, it's DataLoaderPHP's primary feature.
27-
Create loaders by providing a batch loading instance.
26+
## Batching
27+
28+
Batching is not an advanced feature, it's DataLoader's primary feature.
29+
Create loaders by providing a batch loading function.
2830

2931

3032
```php
@@ -67,50 +69,149 @@ presented to your batch loading function. This allows your application to safely
6769
distribute data fetching requirements throughout your application and maintain
6870
minimal outgoing data requests.
6971

70-
### Caching (current PHP instance)
72+
#### Batch Function
73+
74+
A batch loading function accepts an Array of keys, and returns a Promise which
75+
resolves to an Array of values. There are a few constraints that must be upheld:
76+
77+
* The Array of values must be the same length as the Array of keys.
78+
* Each index in the Array of values must correspond to the same index in the Array of keys.
7179

72-
After being loaded once, the resulting value is cached, eliminating
73-
redundant requests.
80+
For example, if your batch function was provided the Array of keys: `[ 2, 9, 6, 1 ]`,
81+
and loading from a back-end service returned the values:
82+
83+
```php
84+
[
85+
['id' => 9, 'name' => 'Chicago'],
86+
['id' => 1, 'name' => 'New York'],
87+
['id' => 2, 'name' => 'San Francisco']
88+
]
89+
```
7490

75-
In the example above, if User `1` was last invited by User `2`, only a single
76-
round trip will occur.
91+
Our back-end service returned results in a different order than we requested, likely
92+
because it was more efficient for it to do so. Also, it omitted a result for key `6`,
93+
which we can interpret as no value existing for that key.
7794

78-
Caching results in creating fewer objects which may relieve memory pressure on
79-
your application:
95+
To uphold the constraints of the batch function, it must return an Array of values
96+
the same length as the Array of keys, and re-order them to ensure each index aligns
97+
with the original keys `[ 2, 9, 6, 1 ]`:
8098

8199
```php
100+
[
101+
['id' => 2, 'name' => 'San Francisco'],
102+
['id' => 9, 'name' => 'Chicago'],
103+
null,
104+
['id' => 1, 'name' => 'New York']
105+
]
106+
```
107+
108+
109+
### Caching (current PHP instance)
110+
111+
DataLoader provides a memoization cache for all loads which occur in a single
112+
request to your application. After `->load()` is called once with a given key,
113+
the resulting value is cached to eliminate redundant loads.
114+
115+
In addition to reliving pressure on your data storage, caching results per-request
116+
also creates fewer objects which may relieve memory pressure on your application:
117+
118+
```php
119+
$userLoader = new DataLoader(...);
82120
$promise1A = $userLoader->load(1);
83121
$promise1B = $userLoader->load(1);
84122
var_dump($promise1A === $promise1B); // bool(true)
85123
```
86124

87-
There are two common examples when clearing the loader's cache is necessary:
125+
#### Clearing Cache
126+
127+
In certain uncommon cases, clearing the request cache may be necessary.
88128

89-
*Mutations:* after a mutation or update, a cached value may be out of date.
90-
Future loads should not use any possibly cached value.
129+
The most common example when clearing the loader's cache is necessary is after
130+
a mutation or update within the same request, when a cached value could be out of
131+
date and future loads should not use any possibly cached value.
91132

92133
Here's a simple example using SQL UPDATE to illustrate.
93134

94135
```php
136+
use Overblog\DataLoader\DataLoader;
137+
138+
// Request begins...
139+
$userLoader = new DataLoader(...);
140+
141+
// And a value happens to be loaded (and cached).
142+
$userLoader->load(4)->then(...);
143+
144+
// A mutation occurs, invalidating what might be in cache.
95145
$sql = 'UPDATE users WHERE id=4 SET username="zuck"';
96146
if (true === $conn->query($sql)) {
97147
$userLoader->clear(4);
98148
}
149+
150+
// Later the value load is loaded again so the mutated data appears.
151+
$userLoader->load(4)->then(...);
152+
153+
// Request completes.
99154
```
100155

101-
*Transient Errors:* A load may fail because it simply can't be loaded
102-
(a permanent issue) or it may fail because of a transient issue such as a down
103-
database or network issue. For transient errors, clear the cache:
156+
#### Caching Errors
157+
158+
If a batch load fails (that is, a batch function throws or returns a rejected
159+
Promise), then the requested values will not be cached. However if a batch
160+
function returns an `Error` instance for an individual value, that `Error` will
161+
be cached to avoid frequently loading the same `Error`.
162+
163+
In some circumstances you may wish to clear the cache for these individual Errors:
104164

105165
```php
106-
$userLoader->load(1)->otherwise(function ($exception) {
166+
$userLoader->load(1)->then(null, function ($exception) {
107167
if (/* determine if error is transient */) {
108168
$userLoader->clear(1);
109169
}
110170
throw $exception;
111171
});
112172
```
113173

174+
#### Disabling Cache
175+
176+
In certain uncommon cases, a DataLoader which *does not* cache may be desirable.
177+
Calling `new DataLoader(myBatchFn, new Option(['cache' => false ]))` will ensure that every
178+
call to `->load()` will produce a *new* Promise, and requested keys will not be
179+
saved in memory.
180+
181+
However, when the memoization cache is disabled, your batch function will
182+
receive an array of keys which may contain duplicates! Each key will be
183+
associated with each call to `->load()`. Your batch loader should provide a value
184+
for each instance of the requested key.
185+
186+
For example:
187+
188+
```php
189+
$myLoader = new DataLoader(function ($keys) {
190+
echo json_encode($keys);
191+
return someBatchLoadFn($keys);
192+
}, new Option(['cache' => false ]));
193+
194+
$myLoader->load('A');
195+
$myLoader->load('B');
196+
$myLoader->load('A');
197+
198+
// [ 'A', 'B', 'A' ]
199+
```
200+
201+
More complex cache behavior can be achieved by calling `->clear()` or `->clearAll()`
202+
rather than disabling the cache completely. For example, this DataLoader will
203+
provide unique keys to a batch function due to the memoization cache being
204+
enabled, but will immediately clear its cache when the batch function is called
205+
so later requests will load new values.
206+
207+
```php
208+
$myLoader = new DataLoader(function($keys) use ($identityLoader) {
209+
$identityLoader->clearAll();
210+
return someBatchLoadFn($keys);
211+
});
212+
```
213+
214+
114215
## API
115216

116217
#### class DataLoader
@@ -204,7 +305,82 @@ Await method process all waiting promise in all dataLoaderPHP instances.
204305

205306
## Using with Webonyx/GraphQL
206307

207-
Here [an example](https://github.com/mcg-web/sandbox-dataloader-graphql-php/blob/master/with-dataloader.php).
308+
DataLoader pairs nicely well with [Webonyx/GraphQL](https://github.com/webonyx/graphql-php). GraphQL fields are
309+
designed to be stand-alone functions. Without a caching or batching mechanism,
310+
it's easy for a naive GraphQL server to issue new database requests each time a
311+
field is resolved.
312+
313+
Consider the following GraphQL request:
314+
315+
```graphql
316+
{
317+
me {
318+
name
319+
bestFriend {
320+
name
321+
}
322+
friends(first: 5) {
323+
name
324+
bestFriend {
325+
name
326+
}
327+
}
328+
}
329+
}
330+
```
331+
332+
Naively, if `me`, `bestFriend` and `friends` each need to request the backend,
333+
there could be at most 13 database requests!
334+
335+
When using DataLoader, we could define the `User` type
336+
at most 4 database requests,
337+
and possibly fewer if there are cache hits.
338+
339+
```php
340+
<?php
341+
use GraphQL\Type\Definition\ObjectType;
342+
use GraphQL\Type\Definition\Type;
343+
344+
/**
345+
* @var \Overblog\DataLoader\DataLoader $userLoader
346+
* @var \PDO $dbh
347+
*/
348+
// ...
349+
350+
$userType = new ObjectType([
351+
'name' => 'User',
352+
'fields' => function () use (&$userType, $userLoader, $dbh) {
353+
return [
354+
'name' => ['type' => Type::string()],
355+
'bestFriend' => [
356+
'type' => $userType,
357+
'resolve' => function ($user) use ($userLoader) {
358+
$userLoader->load($user['bestFriendID']);
359+
}
360+
],
361+
'friends' => [
362+
'args' => [
363+
'first' => ['type' => Type::int() ],
364+
],
365+
'type' => Type::listOf($userType),
366+
'resolve' => function ($user, $args) use ($userLoader, $dbh) {
367+
$sth = $dbh->prepare('SELECT toID FROM friends WHERE fromID=:userID LIMIT :first');
368+
$sth->bindParam(':userID', $user['id'], PDO::PARAM_INT);
369+
$sth->bindParam(':first', $args['first'], PDO::PARAM_INT);
370+
$friendIDs = $sth->execute();
371+
372+
return $userLoader->loadMany($friendIDs);
373+
}
374+
]
375+
];
376+
}
377+
]);
378+
```
379+
You can also see [an example](https://github.com/mcg-web/sandbox-dataloader-graphql-php/blob/master/with-dataloader.php).
380+
381+
## Using with Symfony
382+
383+
See the [bundle](https://github.com/overblog/dataloader-bundle).
208384

209385
## Credits
210386

composer.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
"psr-4": {
1919
"Overblog\\DataLoader\\Test\\": "tests/",
2020
"Overblog\\PromiseAdapter\\Test\\": "lib/promise-adapter/tests/"
21-
}
21+
},
22+
"files": [
23+
"vendor/webonyx/graphql-php/tests/StarWarsData.php"
24+
]
2225
},
2326
"replace": {
2427
"overblog/promise-adapter": "self.version"
@@ -29,15 +32,17 @@
2932
"require-dev": {
3033
"guzzlehttp/promises": "^1.3.0",
3134
"phpunit/phpunit": "^4.1|^5.1",
32-
"react/promise": "^2.5.0"
35+
"react/promise": "^2.5.0",
36+
"webonyx/graphql-php": "^0.9.0"
3337
},
3438
"suggest": {
3539
"guzzlehttp/promises": "To use with Guzzle promise",
36-
"react/promise": "To use with ReactPhp promise"
40+
"react/promise": "To use with ReactPhp promise",
41+
"webonyx/graphql-php": "To use with Webonyx GraphQL native promise"
3742
},
3843
"extra": {
3944
"branch-alias": {
40-
"dev-master": "0.3-dev"
45+
"dev-master": "0.4-dev"
4146
}
4247
}
4348
}

lib/promise-adapter/composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@
2828
"require-dev": {
2929
"guzzlehttp/promises": "^1.3.0",
3030
"phpunit/phpunit": "^4.1|^5.1",
31-
"react/promise": "^2.5.0"
31+
"react/promise": "^2.5.0",
32+
"webonyx/graphql-php": "^0.9.0"
3233
},
3334
"suggest": {
3435
"guzzlehttp/promises": "To use with Guzzle promise",
35-
"react/promise": "To use with ReactPhp promise"
36+
"react/promise": "To use with ReactPhp promise",
37+
"webonyx/graphql-php": "To use with Webonyx GraphQL native promise"
3638
},
3739
"license": "MIT"
3840
}

lib/promise-adapter/src/Adapter/ReactPromiseAdapter.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,6 @@ public function await($promise = null, $unwrap = false)
102102
$wait = false;
103103
});
104104

105-
while ($wait) {
106-
}
107-
108105
if ($exception instanceof \Exception) {
109106
if (!$unwrap) {
110107
return $exception;

0 commit comments

Comments
 (0)