├── .php-cs-fixer.php
├── LICENSE
├── README.md
├── composer.json
├── lib
└── promise-adapter
│ ├── LICENSE
│ ├── README.md
│ ├── docs
│ └── usage.md
│ └── src
│ ├── Adapter
│ ├── GuzzleHttpPromiseAdapter.php
│ ├── ReactPromiseAdapter.php
│ └── WebonyxGraphQLSyncPromiseAdapter.php
│ └── PromiseAdapterInterface.php
└── src
├── CacheMap.php
├── DataLoader.php
├── DataLoaderInterface.php
├── Option.php
└── Promise
└── Adapter
└── Webonyx
└── GraphQL
└── SyncPromiseAdapter.php
/.php-cs-fixer.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | use Symfony\CS\Fixer\Contrib\HeaderCommentFixer;
13 |
14 | $header = <<
18 |
19 | For the full copyright and license information, please view the LICENSE
20 | file that was distributed with this source code.
21 | EOF;
22 |
23 | return (new PhpCsFixer\Config())
24 | ->setRules([
25 | '@PSR2' => true,
26 | 'array_syntax' => ['syntax' => 'short'],
27 | 'no_unreachable_default_argument_value' => false,
28 | 'heredoc_to_nowdoc' => false,
29 | 'header_comment' => ['header' => $header],
30 | ])
31 | ->setRiskyAllowed(true)
32 | ->setFinder(
33 | PhpCsFixer\Finder::create()
34 | ->in(__DIR__)
35 | ->exclude(['vendor'])
36 | )
37 | ;
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Overblog
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DataLoaderPHP
2 |
3 | DataLoaderPHP is a generic utility to be used as part of your application's data
4 | fetching layer to provide a simplified and consistent API over various remote
5 | data sources such as databases or web services via batching and caching.
6 |
7 | [![GitHub Actions][GA master image]][GA master]
8 | [![Code Coverage][Coverage image]][CodeCov Master]
9 | [](https://packagist.org/packages/overblog/dataloader-php)
10 |
11 | ## Requirements
12 |
13 | This library requires PHP >= 7.3 to work.
14 |
15 | ## Getting Started
16 |
17 | First, install DataLoaderPHP using composer.
18 |
19 | ```sh
20 | composer require "overblog/dataloader-php"
21 | ```
22 |
23 | To get started, create a `DataLoader` object.
24 |
25 | ## Batching
26 |
27 | Batching is not an advanced feature, it's DataLoader's primary feature.
28 | Create loaders by providing a batch loading function.
29 |
30 |
31 | ```php
32 | use Overblog\DataLoader\DataLoader;
33 |
34 | $myBatchGetUsers = function ($keys) { /* ... */ };
35 | $promiseAdapter = new MyPromiseAdapter();
36 |
37 | $userLoader = new DataLoader($myBatchGetUsers, $promiseAdapter);
38 | ```
39 |
40 | A batch loading callable / callback accepts an Array of keys, and returns a Promise which
41 | resolves to an Array of values.
42 |
43 | Then load individual values from the loader. DataLoaderPHP will coalesce all
44 | individual loads which occur within a single frame of execution (using `await` method)
45 | and then call your batch function with all requested keys.
46 |
47 | ```php
48 | $userLoader->load(1)
49 | ->then(function ($user) use ($userLoader) { return $userLoader->load($user->invitedByID); })
50 | ->then(function ($invitedBy) { echo "User 1 was invited by $invitedBy"; });
51 |
52 | // Elsewhere in your application
53 | $userLoader->load(2)
54 | ->then(function ($user) use ($userLoader) { return $userLoader->load($user->invitedByID); })
55 | ->then(function ($invitedBy) { echo "User 2 was invited by $invitedBy"; });
56 |
57 | // Synchronously waits on the promise to complete, if not using EventLoop.
58 | $userLoader->await(); // or `DataLoader::await()`
59 | ```
60 | A naive application may have issued four round-trips to a backend for the
61 | required information, but with DataLoaderPHP this application will make at most
62 | two.
63 |
64 | DataLoaderPHP allows you to decouple unrelated parts of your application without
65 | sacrificing the performance of batch data-loading. While the loader presents an
66 | API that loads individual values, all concurrent requests will be coalesced and
67 | presented to your batch loading function. This allows your application to safely
68 | distribute data fetching requirements throughout your application and maintain
69 | minimal outgoing data requests.
70 |
71 | #### Batch Function
72 |
73 | A batch loading function accepts an Array of keys, and returns a Promise which
74 | resolves to an Array of values. There are a few constraints that must be upheld:
75 |
76 | * The Array of values must be the same length as the Array of keys.
77 | * Each index in the Array of values must correspond to the same index in the Array of keys.
78 |
79 | For example, if your batch function was provided the Array of keys: `[ 2, 9, 6, 1 ]`,
80 | and loading from a back-end service returned the values:
81 |
82 | ```php
83 | [
84 | ['id' => 9, 'name' => 'Chicago'],
85 | ['id' => 1, 'name' => 'New York'],
86 | ['id' => 2, 'name' => 'San Francisco']
87 | ]
88 | ```
89 |
90 | Our back-end service returned results in a different order than we requested, likely
91 | because it was more efficient for it to do so. Also, it omitted a result for key `6`,
92 | which we can interpret as no value existing for that key.
93 |
94 | To uphold the constraints of the batch function, it must return an Array of values
95 | the same length as the Array of keys, and re-order them to ensure each index aligns
96 | with the original keys `[ 2, 9, 6, 1 ]`:
97 |
98 | ```php
99 | [
100 | ['id' => 2, 'name' => 'San Francisco'],
101 | ['id' => 9, 'name' => 'Chicago'],
102 | null,
103 | ['id' => 1, 'name' => 'New York']
104 | ]
105 | ```
106 |
107 |
108 | ### Caching (current PHP instance)
109 |
110 | DataLoader provides a memoization cache for all loads which occur in a single
111 | request to your application. After `->load()` is called once with a given key,
112 | the resulting value is cached to eliminate redundant loads.
113 |
114 | In addition to relieving pressure on your data storage, caching results per-request
115 | also creates fewer objects which may relieve memory pressure on your application:
116 |
117 | ```php
118 | $userLoader = new DataLoader(...);
119 | $promise1A = $userLoader->load(1);
120 | $promise1B = $userLoader->load(1);
121 | var_dump($promise1A === $promise1B); // bool(true)
122 | ```
123 |
124 | #### Clearing Cache
125 |
126 | In certain uncommon cases, clearing the request cache may be necessary.
127 |
128 | The most common example when clearing the loader's cache is necessary is after
129 | a mutation or update within the same request, when a cached value could be out of
130 | date and future loads should not use any possibly cached value.
131 |
132 | Here's a simple example using SQL UPDATE to illustrate.
133 |
134 | ```php
135 | use Overblog\DataLoader\DataLoader;
136 |
137 | // Request begins...
138 | $userLoader = new DataLoader(...);
139 |
140 | // And a value happens to be loaded (and cached).
141 | $userLoader->load(4)->then(...);
142 |
143 | // A mutation occurs, invalidating what might be in cache.
144 | $sql = 'UPDATE users WHERE id=4 SET username="zuck"';
145 | if (true === $conn->query($sql)) {
146 | $userLoader->clear(4);
147 | }
148 |
149 | // Later the value load is loaded again so the mutated data appears.
150 | $userLoader->load(4)->then(...);
151 |
152 | // Request completes.
153 | ```
154 |
155 | #### Caching Errors
156 |
157 | If a batch load fails (that is, a batch function throws or returns a rejected
158 | Promise), then the requested values will not be cached. However if a batch
159 | function returns an `Error` instance for an individual value, that `Error` will
160 | be cached to avoid frequently loading the same `Error`.
161 |
162 | In some circumstances you may wish to clear the cache for these individual Errors:
163 |
164 | ```php
165 | $userLoader->load(1)->then(null, function ($exception) {
166 | if (/* determine if error is transient */) {
167 | $userLoader->clear(1);
168 | }
169 | throw $exception;
170 | });
171 | ```
172 |
173 | #### Disabling Cache
174 |
175 | In certain uncommon cases, a DataLoader which *does not* cache may be desirable.
176 | Calling `new DataLoader(myBatchFn, new Option(['cache' => false ]))` will ensure that every
177 | call to `->load()` will produce a *new* Promise, and requested keys will not be
178 | saved in memory.
179 |
180 | However, when the memoization cache is disabled, your batch function will
181 | receive an array of keys which may contain duplicates! Each key will be
182 | associated with each call to `->load()`. Your batch loader should provide a value
183 | for each instance of the requested key.
184 |
185 | For example:
186 |
187 | ```php
188 | $myLoader = new DataLoader(function ($keys) {
189 | echo json_encode($keys);
190 | return someBatchLoadFn($keys);
191 | }, $promiseAdapter, new Option(['cache' => false ]));
192 |
193 | $myLoader->load('A');
194 | $myLoader->load('B');
195 | $myLoader->load('A');
196 |
197 | // [ 'A', 'B', 'A' ]
198 | ```
199 |
200 | More complex cache behavior can be achieved by calling `->clear()` or `->clearAll()`
201 | rather than disabling the cache completely. For example, this DataLoader will
202 | provide unique keys to a batch function due to the memoization cache being
203 | enabled, but will immediately clear its cache when the batch function is called
204 | so later requests will load new values.
205 |
206 | ```php
207 | $myLoader = new DataLoader(function($keys) use ($identityLoader) {
208 | $identityLoader->clearAll();
209 | return someBatchLoadFn($keys);
210 | }, $promiseAdapter);
211 | ```
212 |
213 |
214 | ## API
215 |
216 | #### class DataLoader
217 |
218 | DataLoaderPHP creates a public API for loading data from a particular
219 | data back-end with unique keys such as the `id` column of a SQL table or
220 | document name in a MongoDB database, given a batch loading function.
221 |
222 | Each `DataLoaderPHP` instance contains a unique memoized cache. Use caution when
223 | used in long-lived applications or those which serve many users with different
224 | access permissions and consider creating a new instance per web request.
225 |
226 | ##### `new DataLoader(callable $batchLoadFn, PromiseAdapterInterface $promiseAdapter [, Option $options])`
227 |
228 | Create a new `DataLoaderPHP` given a batch loading instance and options.
229 |
230 | - *$batchLoadFn*: A callable / callback which accepts an Array of keys, and returns a Promise which resolves to an Array of values.
231 | - *$promiseAdapter*: Any object that implements `Overblog\PromiseAdapter\PromiseAdapterInterface`. (see [Overblog/Promise-Adapter](./lib/promise-adapter/docs/usage.md))
232 | - *$options*: An optional object of options:
233 |
234 | - *batch*: Default `true`. Set to `false` to disable batching, instead
235 | immediately invoking `batchLoadFn` with a single load key.
236 |
237 | - *maxBatchSize*: Default `Infinity`. Limits the number of items that get
238 | passed in to the `batchLoadFn`.
239 |
240 | - *cache*: Default `true`. Set to `false` to disable caching, instead
241 | creating a new Promise and new key in the `batchLoadFn` for every load.
242 |
243 | - *cacheKeyFn*: A function to produce a cache key for a given load key.
244 | Defaults to `key`. Useful to provide when an objects are keys
245 | and two similarly shaped objects should be considered equivalent.
246 |
247 | - *cacheMap*: An instance of `CacheMap` to be
248 | used as the underlying cache for this loader. Default `new CacheMap()`.
249 |
250 | ##### `load($key)`
251 |
252 | Loads a key, returning a `Promise` for the value represented by that key.
253 |
254 | - *$key*: An key value to load.
255 |
256 | ##### `loadMany($keys)`
257 |
258 | Loads multiple keys, promising an array of values:
259 |
260 | ```php
261 | list($a, $b) = DataLoader::await($myLoader->loadMany(['a', 'b']));
262 | ```
263 |
264 | This is equivalent to the more verbose:
265 |
266 | ```php
267 | list($a, $b) = DataLoader::await(\React\Promise\all([
268 | $myLoader->load('a'),
269 | $myLoader->load('b')
270 | ]));
271 | ```
272 |
273 | - *$keys*: An array of key values to load.
274 |
275 | ##### `clear($key)`
276 |
277 | Clears the value at `$key` from the cache, if it exists. Returns itself for
278 | method chaining.
279 |
280 | - *$key*: An key value to clear.
281 |
282 | ##### `clearAll()`
283 |
284 | Clears the entire cache. To be used when some event results in unknown
285 | invalidations across this particular `DataLoaderPHP`. Returns itself for
286 | method chaining.
287 |
288 | ##### `prime($key, $value)`
289 |
290 | Primes the cache with the provided key and value. If the key already exists, no
291 | change is made. (To forcefully prime the cache, clear the key first with
292 | `$loader->clear($key)->prime($key, $value)`. Returns itself for method chaining.
293 |
294 | ##### `static await([$promise][, $unwrap])`
295 |
296 | You can synchronously force promises to complete using DataLoaderPHP's await method.
297 | When an await function is invoked it is expected to deliver a value to the promise or reject the promise.
298 | Await method process all waiting promise in all dataLoaderPHP instances.
299 |
300 | - *$promise*: Optional promise to complete.
301 |
302 | - *$unwrap*: controls whether or not the value of the promise is returned for a fulfilled promise
303 | or if an exception is thrown if the promise is rejected. Default `true`.
304 |
305 | ## Using with Webonyx/GraphQL
306 |
307 | DataLoader pairs nicely well with [Webonyx/GraphQL](https://github.com/webonyx/graphql-php). GraphQL fields are
308 | designed to be stand-alone functions. Without a caching or batching mechanism,
309 | it's easy for a naive GraphQL server to issue new database requests each time a
310 | field is resolved.
311 |
312 | Consider the following GraphQL request:
313 |
314 | ```graphql
315 | {
316 | me {
317 | name
318 | bestFriend {
319 | name
320 | }
321 | friends(first: 5) {
322 | name
323 | bestFriend {
324 | name
325 | }
326 | }
327 | }
328 | }
329 | ```
330 |
331 | Naively, if `me`, `bestFriend` and `friends` each need to request the backend,
332 | there could be at most 13 database requests!
333 |
334 | When using DataLoader, we could define the `User` type
335 | at most 4 database requests,
336 | and possibly fewer if there are cache hits.
337 |
338 | ```php
339 | 'User',
360 | 'fields' => function () use (&$userType, $userLoader, $dbh) {
361 | return [
362 | 'name' => ['type' => Type::string()],
363 | 'bestFriend' => [
364 | 'type' => $userType,
365 | 'resolve' => function ($user) use ($userLoader) {
366 | $userLoader->load($user['bestFriendID']);
367 | }
368 | ],
369 | 'friends' => [
370 | 'args' => [
371 | 'first' => ['type' => Type::int() ],
372 | ],
373 | 'type' => Type::listOf($userType),
374 | 'resolve' => function ($user, $args) use ($userLoader, $dbh) {
375 | $sth = $dbh->prepare('SELECT toID FROM friends WHERE fromID=:userID LIMIT :first');
376 | $sth->bindParam(':userID', $user['id'], PDO::PARAM_INT);
377 | $sth->bindParam(':first', $args['first'], PDO::PARAM_INT);
378 | $friendIDs = $sth->execute();
379 |
380 | return $userLoader->loadMany($friendIDs);
381 | }
382 | ]
383 | ];
384 | }
385 | ]);
386 | ```
387 | You can also see [an example](https://github.com/mcg-web/sandbox-dataloader-graphql-php).
388 |
389 | ## Using with Symfony
390 |
391 | See the [bundle](https://github.com/overblog/dataloader-bundle).
392 |
393 | ## Credits
394 |
395 | Overblog/DataLoaderPHP is a port of [dataLoader NodeJS version](https://github.com/facebook/dataloader)
396 | by [Facebook](https://github.com/facebook).
397 |
398 | Also, large parts of the documentation have been ported from the dataLoader NodeJS version
399 | [Docs](https://github.com/facebook/dataloader/blob/master/README.md).
400 |
401 | ## License
402 |
403 | Overblog/DataLoaderPHP is released under the [MIT](https://github.com/overblog/dataloader-php/blob/master/LICENSE) license.
404 |
405 | [Coverage image]: https://codecov.io/gh/overblog/dataloader-php/branch/master/graph/badge.svg
406 | [CodeCov Master]: https://codecov.io/gh/overblog/dataloader-php/branch/master
407 | [GA master]: https://github.com/overblog/dataloader-php/actions?query=workflow%3A%22CI%22+branch%3Amaster
408 | [GA master image]: https://github.com/overblog/dataloader-php/workflows/CI/badge.svg
409 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "overblog/dataloader-php",
3 | "type": "library",
4 | "license": "MIT",
5 | "description": "DataLoaderPhp is a generic utility to be used as part of your application's data fetching layer to provide a simplified and consistent API over various remote data sources such as databases or web services via batching and caching.",
6 | "keywords": ["dataLoader", "caching", "batching"],
7 | "config" : {
8 | "sort-packages": true
9 | },
10 | "autoload": {
11 | "psr-4": {
12 | "Overblog\\DataLoader\\": "src/",
13 | "Overblog\\PromiseAdapter\\": "lib/promise-adapter/src/"
14 | }
15 | },
16 | "autoload-dev": {
17 | "psr-4": {
18 | "Overblog\\DataLoader\\Test\\": "tests/",
19 | "Overblog\\PromiseAdapter\\Test\\": "lib/promise-adapter/tests/"
20 | }
21 | },
22 | "replace": {
23 | "overblog/promise-adapter": "self.version"
24 | },
25 | "require": {
26 | "php": "^8.1"
27 | },
28 | "require-dev": {
29 | "guzzlehttp/promises": "^1.5.0 || ^2.0.0",
30 | "phpunit/phpunit": "^10.3",
31 | "react/promise": "^2.8.0",
32 | "webonyx/graphql-php": "^15.0"
33 | },
34 | "suggest": {
35 | "guzzlehttp/promises": "To use with Guzzle promise",
36 | "react/promise": "To use with ReactPhp promise",
37 | "webonyx/graphql-php": "To use with Webonyx GraphQL native promise"
38 | },
39 | "extra": {
40 | "branch-alias": {
41 | "dev-master": "0.6-dev"
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/promise-adapter/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Overblog
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/promise-adapter/README.md:
--------------------------------------------------------------------------------
1 | # PromiseAdapter
2 |
3 | This library tries to create a simple promise adapter standard while waiting for a psr.
4 | It Comes out of the box with adapter for [ReactPhp/Promise](https://github.com/reactphp/promise) and
5 | [Guzzle/Promises](https://github.com/guzzle/promises).
6 |
7 | [](https://travis-ci.org/overblog/promise-adapter)
8 | [](https://coveralls.io/github/overblog/promise-adapter?branch=master)
9 | [](https://packagist.org/packages/overblog/promise-adapter)
10 | [](https://packagist.org/packages/overblog/promise-adapter)
11 |
12 | ## Installation
13 |
14 | First, install PromiseAdapter using composer.
15 |
16 | ```sh
17 | composer require "overblog/promise-adapter"
18 | ```
19 |
20 | # Usage
21 |
22 | see [here](./docs/usage.md)
23 |
24 | ## License
25 |
26 | Overblog/PromiseAdapter is released under the [MIT](https://github.com/overblog/promise-adapter/blob/master/LICENSE) license.
27 |
--------------------------------------------------------------------------------
/lib/promise-adapter/docs/usage.md:
--------------------------------------------------------------------------------
1 | # Promise adapter usage
2 |
3 | ## Optional requirements
4 |
5 | Optional to use Guzzle:
6 |
7 | ```sh
8 | composer require "guzzlehttp/promises"
9 | ```
10 |
11 | Optional to use ReactPhp:
12 |
13 | ```sh
14 | composer require "react/promise"
15 | ```
16 |
17 | ## Supported Adapter
18 |
19 | *Guzzle*: `Overblog\PromiseAdapter\Adapter\GuzzleHttpPromiseAdapter`
20 |
21 | *ReactPhp*: `Overblog\PromiseAdapter\Adapter\ReactPromiseAdapter`
22 |
23 | To use a custom Promise lib you can implement `Overblog\PromiseAdapter\PromiseAdapterInterface`
24 |
--------------------------------------------------------------------------------
/lib/promise-adapter/src/Adapter/GuzzleHttpPromiseAdapter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Overblog\PromiseAdapter\Adapter;
13 |
14 | use GuzzleHttp\Promise\Create;
15 | use GuzzleHttp\Promise\FulfilledPromise;
16 | use GuzzleHttp\Promise\Promise;
17 | use GuzzleHttp\Promise\PromiseInterface;
18 | use GuzzleHttp\Promise\RejectedPromise;
19 | use GuzzleHttp\Promise\Utils;
20 | use Overblog\PromiseAdapter\PromiseAdapterInterface;
21 |
22 | /**
23 | * @implements PromiseAdapterInterface
24 | */
25 | class GuzzleHttpPromiseAdapter implements PromiseAdapterInterface
26 | {
27 | /**
28 | * {@inheritdoc}
29 | *
30 | * @return Promise
31 | */
32 | public function create(&$resolve = null, &$reject = null, ?callable $canceller = null)
33 | {
34 | $queue = Utils::queue();
35 | $promise = new Promise([$queue, 'run'], $canceller);
36 |
37 | $reject = [$promise, 'reject'];
38 | $resolve = [$promise, 'resolve'];
39 |
40 | return $promise;
41 | }
42 |
43 | /**
44 | * {@inheritdoc}
45 | *
46 | * @return FulfilledPromise a full filed Promise
47 | */
48 | public function createFulfilled($promiseOrValue = null)
49 | {
50 | $promise = Create::promiseFor($promiseOrValue);
51 |
52 | return $promise;
53 | }
54 |
55 | /**
56 | * {@inheritdoc}
57 | *
58 | * @return RejectedPromise a rejected promise
59 | */
60 | public function createRejected($reason)
61 | {
62 | $promise = Create::rejectionFor($reason);
63 |
64 | return $promise;
65 | }
66 |
67 | /**
68 | * {@inheritdoc}
69 | *
70 | * @return Promise
71 | */
72 | public function createAll($promisesOrValues)
73 | {
74 | $promise = empty($promisesOrValues) ? $this->createFulfilled($promisesOrValues) : Utils::all($promisesOrValues);
75 |
76 | return $promise;
77 | }
78 |
79 | /**
80 | * {@inheritdoc}
81 | */
82 | public function isPromise($value, $strict = false)
83 | {
84 | $isStrictPromise = $value instanceof PromiseInterface;
85 |
86 | if ($strict) {
87 | return $isStrictPromise;
88 | }
89 |
90 | return $isStrictPromise || is_callable([$value, 'then']);
91 | }
92 |
93 | /**
94 | * {@inheritdoc}
95 | */
96 | public function await($promise = null, $unwrap = false)
97 | {
98 | $resolvedValue = null;
99 |
100 | if (null !== $promise) {
101 | $exception = null;
102 | if (!static::isPromise($promise)) {
103 | throw new \InvalidArgumentException(sprintf('The "%s" method must be called with a Promise ("then" method).', __METHOD__));
104 | }
105 |
106 | /** @var Promise $promise */
107 | $promise->then(function ($values) use (&$resolvedValue) {
108 | $resolvedValue = $values;
109 | }, function ($reason) use (&$exception) {
110 | $exception = $reason;
111 | });
112 | Utils::queue()->run();
113 |
114 | if ($exception instanceof \Exception) {
115 | if (!$unwrap) {
116 | return $exception;
117 | }
118 | throw $exception;
119 | }
120 | } else {
121 | Utils::queue()->run();
122 | }
123 |
124 | return $resolvedValue;
125 | }
126 |
127 | /**
128 | * Cancel a promise
129 | *
130 | * @param PromiseInterface $promise
131 | * {@inheritdoc}
132 | */
133 | public function cancel($promise)
134 | {
135 | if (!static::isPromise($promise, true)) {
136 | throw new \InvalidArgumentException(sprintf('The "%s" method must be called with a compatible Promise.', __METHOD__));
137 | }
138 | $promise->cancel();
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/lib/promise-adapter/src/Adapter/ReactPromiseAdapter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Overblog\PromiseAdapter\Adapter;
13 |
14 | use Overblog\PromiseAdapter\PromiseAdapterInterface;
15 | use React\Promise\CancellablePromiseInterface;
16 | use React\Promise\Deferred;
17 | use React\Promise\FulfilledPromise;
18 | use React\Promise\Promise;
19 | use React\Promise\PromiseInterface;
20 | use React\Promise\RejectedPromise;
21 |
22 | /**
23 | * @implements PromiseAdapterInterface
24 | */
25 | class ReactPromiseAdapter implements PromiseAdapterInterface
26 | {
27 | /**
28 | * {@inheritdoc}
29 | *
30 | * @return Promise
31 | */
32 | public function create(&$resolve = null, &$reject = null, ?callable $canceller = null)
33 | {
34 | $deferred = new Deferred($canceller);
35 |
36 | $reject = [$deferred, 'reject'];
37 | $resolve = [$deferred, 'resolve'];
38 |
39 | return $deferred->promise();
40 | }
41 |
42 | /**
43 | * {@inheritdoc}
44 | *
45 | * @return FulfilledPromise a full filed Promise
46 | */
47 | public function createFulfilled($promiseOrValue = null)
48 | {
49 | return \React\Promise\resolve($promiseOrValue);
50 | }
51 |
52 | /**
53 | * {@inheritdoc}
54 | *
55 | * @return RejectedPromise a rejected promise
56 | */
57 | public function createRejected($reason)
58 | {
59 | return \React\Promise\reject($reason);
60 | }
61 |
62 | /**
63 | * {@inheritdoc}
64 | *
65 | * @return Promise
66 | */
67 | public function createAll($promisesOrValues)
68 | {
69 | return \React\Promise\all($promisesOrValues);
70 | }
71 |
72 | /**
73 | * {@inheritdoc}
74 | */
75 | public function isPromise($value, $strict = false)
76 | {
77 | $isStrictPromise = $value instanceof PromiseInterface;
78 |
79 | if ($strict) {
80 | return $isStrictPromise;
81 | }
82 |
83 | return $isStrictPromise || is_callable([$value, 'then']);
84 | }
85 |
86 | /**
87 | * {@inheritdoc}
88 | */
89 | public function await($promise = null, $unwrap = false)
90 | {
91 | if (null === $promise) {
92 | return null;
93 | }
94 | $wait = true;
95 | $resolvedValue = null;
96 | $exception = null;
97 | if (!static::isPromise($promise)) {
98 | throw new \InvalidArgumentException(sprintf('The "%s" method must be called with a Promise ("then" method).', __METHOD__));
99 | }
100 | $promise->then(function ($values) use (&$resolvedValue, &$wait) {
101 | $resolvedValue = $values;
102 | $wait = false;
103 | }, function ($reason) use (&$exception, &$wait) {
104 | $exception = $reason;
105 | $wait = false;
106 | });
107 |
108 | if ($exception instanceof \Exception) {
109 | if (!$unwrap) {
110 | return $exception;
111 | }
112 | throw $exception;
113 | }
114 |
115 | return $resolvedValue;
116 | }
117 |
118 | /**
119 | * Cancel a promise
120 | *
121 | * @param CancellablePromiseInterface $promise
122 | */
123 | public function cancel($promise)
124 | {
125 | if (!$promise instanceof CancellablePromiseInterface) {
126 | throw new \InvalidArgumentException(sprintf('The "%s" method must be called with a compatible Promise.', __METHOD__));
127 | }
128 | $promise->cancel();
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/lib/promise-adapter/src/Adapter/WebonyxGraphQLSyncPromiseAdapter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Overblog\PromiseAdapter\Adapter;
13 |
14 | use GraphQL\Deferred;
15 | use GraphQL\Executor\Promise\Adapter\SyncPromise;
16 | use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
17 | use GraphQL\Executor\Promise\Promise;
18 | use Overblog\PromiseAdapter\PromiseAdapterInterface;
19 |
20 | /**
21 | * @implements PromiseAdapterInterface
22 | */
23 | class WebonyxGraphQLSyncPromiseAdapter implements PromiseAdapterInterface
24 | {
25 | /** @var callable[] */
26 | private $cancellers = [];
27 |
28 | /**
29 | * @var SyncPromiseAdapter
30 | */
31 | private $webonyxPromiseAdapter;
32 |
33 | public function __construct(?SyncPromiseAdapter $webonyxPromiseAdapter = null)
34 | {
35 | $webonyxPromiseAdapter = $webonyxPromiseAdapter?:new SyncPromiseAdapter();
36 | $this->setWebonyxPromiseAdapter($webonyxPromiseAdapter);
37 | }
38 |
39 | /**
40 | * @return SyncPromiseAdapter
41 | */
42 | public function getWebonyxPromiseAdapter()
43 | {
44 | return $this->webonyxPromiseAdapter;
45 | }
46 |
47 | /**
48 | * @param SyncPromiseAdapter $webonyxPromiseAdapter
49 | */
50 | public function setWebonyxPromiseAdapter(SyncPromiseAdapter $webonyxPromiseAdapter)
51 | {
52 | $this->webonyxPromiseAdapter = $webonyxPromiseAdapter;
53 | }
54 |
55 | /**
56 | * {@inheritdoc}
57 | */
58 | public function create(&$resolve = null, &$reject = null, ?callable $canceller = null)
59 | {
60 | $promise = $this->webonyxPromiseAdapter->create(function ($res, $rej) use (&$resolve, &$reject) {
61 | $resolve = $res;
62 | $reject = $rej;
63 | });
64 | $this->cancellers[spl_object_hash($promise)] = $canceller;
65 |
66 | return $promise;
67 | }
68 |
69 | /**
70 | * {@inheritdoc}
71 | */
72 | public function createFulfilled($promiseOrValue = null)
73 | {
74 | return $this->getWebonyxPromiseAdapter()->createFulfilled($promiseOrValue);
75 | }
76 |
77 | /**
78 | * {@inheritdoc}
79 | */
80 | public function createRejected($reason)
81 | {
82 | return $this->getWebonyxPromiseAdapter()->createRejected($reason);
83 | }
84 |
85 | /**
86 | * {@inheritdoc}
87 | */
88 | public function createAll($promisesOrValues)
89 | {
90 | return $this->getWebonyxPromiseAdapter()->all($promisesOrValues);
91 | }
92 |
93 | /**
94 | * {@inheritdoc}
95 | */
96 | public function isPromise($value, $strict = false)
97 | {
98 | if ($value instanceof Promise) {
99 | $value = $value->adoptedPromise;
100 | }
101 | $isStrictPromise = $value instanceof SyncPromise;
102 | if ($strict) {
103 | return $isStrictPromise;
104 | }
105 |
106 | return $isStrictPromise || is_callable([$value, 'then']);
107 | }
108 |
109 | /**
110 | * {@inheritdoc}
111 | */
112 | public function await($promise = null, $unwrap = false)
113 | {
114 | if (null === $promise) {
115 | Deferred::runQueue();
116 | SyncPromise::runQueue();
117 | $this->cancellers = [];
118 | return null;
119 | }
120 | $promiseAdapter = $this->getWebonyxPromiseAdapter();
121 |
122 | $resolvedValue = null;
123 | $exception = null;
124 | if (!$this->isPromise($promise)) {
125 | throw new \InvalidArgumentException(sprintf('The "%s" method must be called with a Promise ("then" method).', __METHOD__));
126 | }
127 |
128 | try {
129 | $resolvedValue = $promiseAdapter->wait($promise);
130 | } catch (\Exception $reason) {
131 | $exception = $reason;
132 | }
133 | if ($exception instanceof \Exception) {
134 | if (!$unwrap) {
135 | return $exception;
136 | }
137 | throw $exception;
138 | }
139 |
140 | $hash = spl_object_hash($promise);
141 | unset($this->cancellers[$hash]);
142 | return $resolvedValue;
143 | }
144 |
145 | /**
146 | * {@inheritdoc}
147 | */
148 | public function cancel($promise)
149 | {
150 | $hash = spl_object_hash($promise);
151 | if (!$this->isPromise($promise) || !isset($this->cancellers[$hash])) {
152 | throw new \InvalidArgumentException(sprintf('The "%s" method must be called with a compatible Promise.', __METHOD__));
153 | }
154 | $canceller = $this->cancellers[$hash];
155 | unset($this->cancellers[$hash]);
156 | $adoptedPromise = $promise;
157 | if ($promise instanceof Promise) {
158 | $adoptedPromise = $promise->adoptedPromise;
159 | }
160 | try {
161 | $canceller([$adoptedPromise, 'resolve'], [$adoptedPromise, 'reject']);
162 | } catch (\Exception $reason) {
163 | $adoptedPromise->reject($reason);
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/lib/promise-adapter/src/PromiseAdapterInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Overblog\PromiseAdapter;
13 |
14 | /**
15 | * @template TPromise
16 | */
17 | interface PromiseAdapterInterface
18 | {
19 | /**
20 | * Creates a Promise
21 | *
22 | * @param $resolve
23 | * @param $reject
24 | * @param callable $canceller
25 | *
26 | * @return TPromise a Promise
27 | */
28 | public function create(&$resolve = null, &$reject = null, ?callable $canceller = null);
29 |
30 | /**
31 | * Creates a full filed Promise for a value if the value is not a promise.
32 | *
33 | * @param mixed $promiseOrValue
34 | *
35 | * @return TPromise a full filed Promise
36 | */
37 | public function createFulfilled($promiseOrValue = null);
38 |
39 | /**
40 | * Creates a rejected promise for a reason if the reason is not a promise. If
41 | * the provided reason is a promise, then it is returned as-is.
42 | *
43 | * @param mixed $reason
44 | *
45 | * @return TPromise a rejected promise
46 | */
47 | public function createRejected($reason);
48 |
49 | /**
50 | * Given an array of promises, return a promise that is fulfilled when all the
51 | * items in the array are fulfilled.
52 | *
53 | * @param mixed $promisesOrValues Promises or values.
54 | *
55 | * @return TPromise a Promise
56 | */
57 | public function createAll($promisesOrValues);
58 |
59 | /**
60 | * Check if value is a promise
61 | *
62 | * @param mixed $value
63 | * @param bool $strict
64 | *
65 | * @return bool
66 | */
67 | public function isPromise($value, $strict = false);
68 |
69 | /**
70 | * Cancel a promise
71 | *
72 | * @param TPromise $promise
73 | */
74 | public function cancel($promise);
75 |
76 | /**
77 | * wait for Promise to complete
78 | * @param TPromise $promise
79 | * @param bool $unwrap
80 | *
81 | * @return mixed
82 | */
83 | public function await($promise = null, $unwrap = false);
84 | }
85 |
--------------------------------------------------------------------------------
/src/CacheMap.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Overblog\DataLoader;
13 |
14 | class CacheMap
15 | {
16 | private $promiseCache = [];
17 |
18 | public function get($key)
19 | {
20 | $key = self::serializedKey($key);
21 |
22 | return isset($this->promiseCache[$key]) ? $this->promiseCache[$key] : null;
23 | }
24 |
25 | public function has($key)
26 | {
27 | return isset($this->promiseCache[self::serializedKey($key)]);
28 | }
29 |
30 | public function set($key, $promise)
31 | {
32 | $this->promiseCache[self::serializedKey($key)] = $promise;
33 |
34 | return $this;
35 | }
36 |
37 | public function clear($key)
38 | {
39 | unset($this->promiseCache[self::serializedKey($key)]);
40 |
41 | return $this;
42 | }
43 |
44 | public function clearAll()
45 | {
46 | $this->promiseCache = [];
47 |
48 | return $this;
49 | }
50 |
51 | private static function serializedKey($key)
52 | {
53 | if (is_object($key)) {
54 | return spl_object_hash($key);
55 | } elseif (is_array($key)) {
56 | return json_encode($key);
57 | }
58 |
59 | return $key;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/DataLoader.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Overblog\DataLoader;
13 |
14 | use Overblog\PromiseAdapter\PromiseAdapterInterface;
15 |
16 | class DataLoader implements DataLoaderInterface
17 | {
18 | /**
19 | * @var callable
20 | */
21 | private $batchLoadFn;
22 |
23 | /**
24 | * @var Option
25 | */
26 | private $options;
27 |
28 | /**
29 | * @var CacheMap
30 | */
31 | private $promiseCache;
32 |
33 | /**
34 | * @var array
35 | */
36 | private $queue = [];
37 |
38 | /**
39 | * @var self[]
40 | */
41 | private static $instances = [];
42 |
43 | /**
44 | * @var PromiseAdapterInterface
45 | */
46 | private $promiseAdapter;
47 |
48 | public function __construct(callable $batchLoadFn, PromiseAdapterInterface $promiseFactory, ?Option $options = null)
49 | {
50 | $this->batchLoadFn = $batchLoadFn;
51 | $this->promiseAdapter = $promiseFactory;
52 | $this->options = $options ?: new Option();
53 | $this->promiseCache = $this->options->getCacheMap();
54 | self::$instances[] = $this;
55 | }
56 |
57 | /**
58 | * {@inheritdoc}
59 | */
60 | public function load($key)
61 | {
62 | $this->checkKey($key, __METHOD__);
63 | // Determine options
64 | $shouldBatch = $this->options->shouldBatch();
65 | $shouldCache = $this->options->shouldCache();
66 | $cacheKey = $this->getCacheKeyFromKey($key);
67 |
68 | // If caching and there is a cache-hit, return cached Promise.
69 | if ($shouldCache) {
70 | $cachedPromise = $this->promiseCache->get($cacheKey);
71 | if ($cachedPromise) {
72 | return $cachedPromise;
73 | }
74 | }
75 |
76 | // Otherwise, produce a new Promise for this value.
77 | $promise = $this->getPromiseAdapter()->create(
78 | $resolve,
79 | $reject,
80 | function () {
81 | // Cancel/abort any running operations like network connections, streams etc.
82 |
83 | throw new \RuntimeException('DataLoader destroyed before promise complete.');
84 | }
85 | );
86 |
87 | $this->queue[] = [
88 | 'key' => $key,
89 | 'resolve' => $resolve,
90 | 'reject' => $reject,
91 | 'promise' => $promise,
92 | ];
93 |
94 | // Determine if a dispatch of this queue should be scheduled.
95 | // A single dispatch should be scheduled per queue at the time when the
96 | // queue changes from "empty" to "full".
97 | if (count($this->queue) === 1) {
98 | if (!$shouldBatch) {
99 | // Otherwise dispatch the (queue of one) immediately.
100 | $this->dispatchQueue();
101 | }
102 | }
103 | // If caching, cache this promise.
104 | if ($shouldCache) {
105 | $this->promiseCache->set($cacheKey, $promise);
106 | }
107 |
108 | return $promise;
109 | }
110 |
111 | /**
112 | * {@inheritdoc}
113 | */
114 | public function loadMany($keys)
115 | {
116 | if (!is_array($keys) && !$keys instanceof \Traversable) {
117 | throw new \InvalidArgumentException(sprintf('The "%s" method must be called with Array but got: %s.', __METHOD__, gettype($keys)));
118 | }
119 | return $this->getPromiseAdapter()->createAll(array_map(
120 | function ($key) {
121 | return $this->load($key);
122 | },
123 | $keys
124 | ));
125 | }
126 |
127 | /**
128 | * {@inheritdoc}
129 | */
130 | public function clear($key)
131 | {
132 | $this->checkKey($key, __METHOD__);
133 | $cacheKey = $this->getCacheKeyFromKey($key);
134 | $this->promiseCache->clear($cacheKey);
135 |
136 | return $this;
137 | }
138 |
139 | /**
140 | * {@inheritdoc}
141 | */
142 | public function clearAll()
143 | {
144 | $this->promiseCache->clearAll();
145 |
146 | return $this;
147 | }
148 |
149 | /**
150 | * {@inheritdoc}
151 | */
152 | public function prime($key, $value)
153 | {
154 | $this->checkKey($key, __METHOD__);
155 |
156 | $cacheKey = $this->getCacheKeyFromKey($key);
157 |
158 | // Only add the key if it does not already exist.
159 | if (!$this->promiseCache->has($cacheKey)) {
160 | // Cache a rejected promise if the value is an Error, in order to match
161 | // the behavior of load(key).
162 | $promise = $value instanceof \Exception ? $this->getPromiseAdapter()->createRejected($value) : $this->getPromiseAdapter()->createFulfilled($value);
163 |
164 | $this->promiseCache->set($cacheKey, $promise);
165 | }
166 |
167 | return $this;
168 | }
169 |
170 | public function __destruct()
171 | {
172 | if ($this->needProcess()) {
173 | foreach ($this->queue as $data) {
174 | try {
175 | $this->getPromiseAdapter()->cancel($data['promise']);
176 | } catch (\Exception $e) {
177 | // no need to do nothing if cancel failed
178 | }
179 | }
180 | $this->await();
181 | }
182 | foreach (self::$instances as $i => $instance) {
183 | if ($this !== $instance) {
184 | continue;
185 | }
186 | unset(self::$instances[$i]);
187 | }
188 | }
189 |
190 | protected function needProcess()
191 | {
192 | return count($this->queue) > 0;
193 | }
194 |
195 | protected function process()
196 | {
197 | if ($this->needProcess()) {
198 | $this->getPromiseAdapter()->await();
199 | $this->dispatchQueue();
200 | }
201 | }
202 |
203 | protected function getPromiseAdapter()
204 | {
205 | return $this->promiseAdapter;
206 | }
207 |
208 | /**
209 | * {@inheritdoc}
210 | */
211 | public static function await($promise = null, $unwrap = true)
212 | {
213 | self::awaitInstances();
214 |
215 | if (null === $promise) {
216 | return null;
217 | }
218 |
219 | if (is_callable([$promise, 'then'])) {
220 | $isPromiseCompleted = false;
221 | $resolvedValue = null;
222 | $rejectedReason = null;
223 |
224 | $promise->then(
225 | function ($value) use (&$isPromiseCompleted, &$resolvedValue) {
226 | $isPromiseCompleted = true;
227 | $resolvedValue = $value;
228 | },
229 | function ($reason) use (&$isPromiseCompleted, &$rejectedReason) {
230 | $isPromiseCompleted = true;
231 | $rejectedReason = $reason;
232 | }
233 | );
234 |
235 | //Promise is completed?
236 | if ($isPromiseCompleted) {
237 | // rejected ?
238 | if ($rejectedReason instanceof \Exception || (interface_exists('\Throwable') && $rejectedReason instanceof \Throwable)) {
239 | if (!$unwrap) {
240 | return $rejectedReason;
241 | }
242 | throw $rejectedReason;
243 | }
244 |
245 | return $resolvedValue;
246 | }
247 | }
248 |
249 | if (empty(self::$instances)) {
250 | throw new \RuntimeException('Found no active DataLoader instance.');
251 | }
252 |
253 | return self::$instances[0]->getPromiseAdapter()->await($promise, $unwrap);
254 | }
255 |
256 | private static function awaitInstances()
257 | {
258 | do {
259 | $wait = false;
260 | $dataLoaders = self::$instances;
261 |
262 | foreach ($dataLoaders as $dataLoader) {
263 | if (!$dataLoader || !$dataLoader->needProcess()) {
264 | $wait |= false;
265 | continue;
266 | }
267 | $wait = true;
268 | $dataLoader->process();
269 | }
270 | } while ($wait);
271 | }
272 |
273 | /**
274 | * @param $key
275 | *
276 | * @return mixed
277 | */
278 | protected function getCacheKeyFromKey($key)
279 | {
280 | $cacheKeyFn = $this->options->getCacheKeyFn();
281 | $cacheKey = $cacheKeyFn ? $cacheKeyFn($key) : $key;
282 |
283 | return $cacheKey;
284 | }
285 |
286 | /**
287 | * @param $key
288 | * @param $method
289 | */
290 | protected function checkKey($key, $method)
291 | {
292 | if (null === $key) {
293 | throw new \InvalidArgumentException(
294 | sprintf('The "%s" method must be called with a value, but got: %s.', $method, gettype($key))
295 | );
296 | }
297 | }
298 |
299 | /**
300 | * Given the current state of a Loader instance, perform a batch load
301 | * from its current queue.
302 | */
303 | private function dispatchQueue()
304 | {
305 | // Take the current loader queue, replacing it with an empty queue.
306 | $queue = $this->queue;
307 | $this->queue = [];
308 | $queueLength = count($queue);
309 | // If a maxBatchSize was provided and the queue is longer, then segment the
310 | // queue into multiple batches, otherwise treat the queue as a single batch.
311 | $maxBatchSize = $this->options->getMaxBatchSize();
312 | if ($maxBatchSize && $maxBatchSize > 0 && $maxBatchSize < $queueLength) {
313 | for ($i = 0; $i < $queueLength / $maxBatchSize; $i++) {
314 | $offset = $i * $maxBatchSize;
315 | $length = ($i + 1) * $maxBatchSize - $offset;
316 |
317 | $this->dispatchQueueBatch(array_slice($queue, $offset, $length));
318 | }
319 | } else {
320 | $this->dispatchQueueBatch($queue);
321 | }
322 | }
323 |
324 | private function dispatchQueueBatch(array $queue)
325 | {
326 | // Collect all keys to be loaded in this dispatch
327 | $keys = array_column($queue, 'key');
328 |
329 | // Call the provided batchLoadFn for this loader with the loader queue's keys.
330 | $batchLoadFn = $this->batchLoadFn;
331 | $batchPromise = $batchLoadFn($keys);
332 |
333 | // Assert the expected response from batchLoadFn
334 | if (!$batchPromise || !is_callable([$batchPromise, 'then'])) {
335 | $this->failedDispatch($queue, new \RuntimeException(
336 | 'DataLoader must be constructed with a function which accepts ' .
337 | 'Array and returns Promise>, but the function did ' .
338 | sprintf('not return a Promise: %s.', gettype($batchPromise))
339 | ));
340 |
341 | return;
342 | }
343 |
344 | // Await the resolution of the call to batchLoadFn.
345 | $batchPromise->then(
346 | function ($values) use ($keys, $queue) {
347 | // Assert the expected resolution from batchLoadFn.
348 | if (!is_array($values) && !$values instanceof \Traversable) {
349 | throw new \RuntimeException(
350 | 'DataLoader must be constructed with a function which accepts ' .
351 | 'Array and returns Promise>, but the function did ' .
352 | sprintf('not return a Promise of an Array: %s.', gettype($values))
353 | );
354 | }
355 | if (count($values) !== count($keys)) {
356 | throw new \RuntimeException(
357 | 'DataLoader must be constructed with a function which accepts ' .
358 | 'Array and returns Promise>, but the function did ' .
359 | 'not return a Promise of an Array of the same length as the Array of keys.'
360 | );
361 | }
362 |
363 | // Step through the values, resolving or rejecting each Promise in the
364 | // loaded queue.
365 | foreach ($queue as $index => $data) {
366 | $value = $values[$index];
367 | if ($value instanceof \Exception) {
368 | $data['reject']($value);
369 | } else {
370 | $data['resolve']($value);
371 | }
372 | };
373 | }
374 | )->then(null, function ($error) use ($queue) {
375 | $this->failedDispatch($queue, $error);
376 | });
377 | }
378 |
379 | /**
380 | * Do not cache individual loads if the entire batch dispatch fails,
381 | * but still reject each request so they do not hang.
382 | * @param array $queue
383 | * @param \Exception $error
384 | */
385 | private function failedDispatch($queue, \Exception $error)
386 | {
387 | foreach ($queue as $index => $data) {
388 | $this->clear($data['key']);
389 | $data['reject']($error);
390 | }
391 | }
392 | }
393 |
--------------------------------------------------------------------------------
/src/DataLoaderInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Overblog\DataLoader;
13 |
14 | interface DataLoaderInterface
15 | {
16 | /**
17 | * Loads a key, returning a `Promise` for the value represented by that key.
18 | *
19 | * @param mixed $key
20 | *
21 | * @return mixed return a Promise
22 | */
23 | public function load($key);
24 |
25 | /**
26 | * Loads multiple keys, promising an array of values:
27 | *
28 | * list($a, $b) = $myLoader->loadMany(['a', 'b']);
29 | *
30 | * This is equivalent to the more verbose:
31 | *
32 | * list($a, $b) = \React\Promise\all([
33 | * $myLoader->load('a'),
34 | * $myLoader->load('b')
35 | * ]);
36 | * @param array $keys
37 | *
38 | * @return mixed return a Promise
39 | */
40 | public function loadMany($keys);
41 |
42 | /**
43 | * Clears the value at `key` from the cache, if it exists.
44 | *
45 | * @param $key
46 | * @return $this
47 | */
48 | public function clear($key);
49 |
50 | /**
51 | * Clears the entire cache. To be used when some event results in unknown
52 | * invalidations across this particular `DataLoader`.
53 | *
54 | * @return $this
55 | */
56 | public function clearAll();
57 |
58 | /**
59 | * Adds the provided key and value to the cache. If the key already exists, no
60 | * change is made. Returns itself for method chaining.
61 | * @param $key
62 | * @param $value
63 | * @return $this
64 | */
65 | public function prime($key, $value);
66 |
67 | /**
68 | * @param $promise
69 | * @param bool $unwrap controls whether or not the value of the promise is returned for a fulfilled promise or if an exception is thrown if the promise is rejected
70 | * @return mixed
71 | * @throws \Exception
72 | */
73 | public static function await($promise = null, $unwrap = true);
74 | }
75 |
--------------------------------------------------------------------------------
/src/Option.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Overblog\DataLoader;
13 |
14 | class Option
15 | {
16 | /**
17 | * @var bool
18 | */
19 | private $batch;
20 |
21 | /**
22 | * @var |null
23 | */
24 | private $maxBatchSize;
25 |
26 | /**
27 | * @var bool
28 | */
29 | private $cache;
30 |
31 | /**
32 | * @var callable
33 | */
34 | private $cacheKeyFn;
35 |
36 | /**
37 | * @var CacheMap
38 | */
39 | private $cacheMap;
40 |
41 | public function __construct(array $params = [])
42 | {
43 | $defaultOptions = [
44 | 'batch' => true,
45 | 'maxBatchSize' => null,
46 | 'cache' => true,
47 | 'cacheKeyFn' => null,
48 | 'cacheMap' => new CacheMap()
49 | ];
50 |
51 | $options = array_merge($defaultOptions, $params);
52 |
53 | foreach ($options as $property => $value) {
54 | $this->$property = $value;
55 | }
56 | }
57 |
58 | /**
59 | * @return boolean
60 | */
61 | public function shouldBatch()
62 | {
63 | return $this->batch;
64 | }
65 |
66 | /**
67 | * @return int
68 | */
69 | public function getMaxBatchSize()
70 | {
71 | return $this->maxBatchSize;
72 | }
73 |
74 | /**
75 | * @return boolean
76 | */
77 | public function shouldCache()
78 | {
79 | return $this->cache;
80 | }
81 |
82 | /**
83 | * @return callable
84 | */
85 | public function getCacheKeyFn()
86 | {
87 | return $this->cacheKeyFn;
88 | }
89 |
90 | /**
91 | * @return CacheMap
92 | */
93 | public function getCacheMap()
94 | {
95 | return $this->cacheMap;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/Promise/Adapter/Webonyx/GraphQL/SyncPromiseAdapter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Overblog\DataLoader\Promise\Adapter\Webonyx\GraphQL;
13 |
14 | use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter as BaseSyncPromiseAdapter;
15 | use GraphQL\Executor\Promise\Promise;
16 | use Overblog\DataLoader\DataLoader;
17 |
18 | class SyncPromiseAdapter extends BaseSyncPromiseAdapter
19 | {
20 | protected function beforeWait(Promise $promise): void
21 | {
22 | DataLoader::await();
23 | }
24 |
25 | protected function onWait(Promise $promise): void
26 | {
27 | DataLoader::await();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------