@@ -23,8 +23,10 @@ composer require "overblog/dataloader-php"
2323
2424To 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
6769distribute data fetching requirements throughout your application and maintain
6870minimal 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);
84122var_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
92133Here'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"';
96146if (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
0 commit comments