diff --git a/README.md b/README.md index 7a03353..a057e7d 100644 --- a/README.md +++ b/README.md @@ -71,10 +71,13 @@ minimal outgoing data requests. #### Batch Function A batch loading function accepts an Array of keys, and returns a Promise which -resolves to an Array of values. There are a few constraints that must be upheld: +resolves to values in one of these forms: - * The Array of values must be the same length as the Array of keys. - * Each index in the Array of values must correspond to the same index in the Array of keys. + * A list with the same length and order as the Array of keys. + * A keyed Array or `ArrayAccess` value. Missing keys resolve to `null`. + +Object keys require positional list results or an `ArrayAccess` result that +supports those keys. For example, if your batch function was provided the Array of keys: `[ 2, 9, 6, 1 ]`, and loading from a back-end service returned the values: diff --git a/lib/promise-adapter/src/Adapter/WebonyxGraphQLSyncPromiseAdapter.php b/lib/promise-adapter/src/Adapter/WebonyxGraphQLSyncPromiseAdapter.php index 7fb5eaa..dd60566 100644 --- a/lib/promise-adapter/src/Adapter/WebonyxGraphQLSyncPromiseAdapter.php +++ b/lib/promise-adapter/src/Adapter/WebonyxGraphQLSyncPromiseAdapter.php @@ -32,7 +32,7 @@ class WebonyxGraphQLSyncPromiseAdapter implements PromiseAdapterInterface public function __construct(?SyncPromiseAdapter $webonyxPromiseAdapter = null) { - $webonyxPromiseAdapter = $webonyxPromiseAdapter?:new SyncPromiseAdapter(); + $webonyxPromiseAdapter = $webonyxPromiseAdapter?:new \Overblog\DataLoader\Promise\Adapter\Webonyx\GraphQL\SyncPromiseAdapter(); $this->setWebonyxPromiseAdapter($webonyxPromiseAdapter); } diff --git a/src/DataLoader.php b/src/DataLoader.php index 7cd10a3..4d8a63c 100644 --- a/src/DataLoader.php +++ b/src/DataLoader.php @@ -417,39 +417,61 @@ private function dispatchQueueBatch(array $queue) // Await the resolution of the call to batchLoadFn. $batchPromise->then( - function ($values) use ($keys, $queue) { - // Assert the expected resolution from batchLoadFn. - if (!is_array($values) && !$values instanceof \Traversable) { - throw new \RuntimeException( - 'DataLoader must be constructed with a function which accepts ' . - 'Array and returns Promise>, but the function did ' . - sprintf('not return a Promise of an Array: %s.', gettype($values)) - ); - } - if (count($values) !== count($keys)) { - throw new \RuntimeException( - 'DataLoader must be constructed with a function which accepts ' . - 'Array and returns Promise>, but the function did ' . - 'not return a Promise of an Array of the same length as the Array of keys.' - ); - } - - // Step through the values, resolving or rejecting each Promise in the - // loaded queue. - foreach ($queue as $index => $data) { - $value = $values[$index]; - if ($value instanceof \Throwable) { - $data['reject']($value); - } else { - $data['resolve']($value); - } - }; + function ($values) use ($queue) { + $this->resolveDispatchedBatch($values, $queue); } )->then(null, function ($error) use ($queue) { $this->failedDispatch($queue, $error); }); } + /** + * Fan a resolved batch result out to the individual queued promises. + * + * @param mixed $values + * @param array $queue + */ + private function resolveDispatchedBatch($values, $queue) + { + // Assert the expected resolution from batchLoadFn. + if (!is_array($values) && !$values instanceof \Traversable) { + throw new \RuntimeException( + 'DataLoader must be constructed with a function which accepts ' . + 'Array and returns Promise>, but the function did ' . + sprintf('not return a Promise of an Array: %s.', gettype($values)) + ); + } + + if ($values instanceof \Traversable && !$values instanceof \ArrayAccess) { + $values = iterator_to_array($values); + } + + // Step through the values, resolving or rejecting each Promise in the + // loaded queue. + foreach ($queue as $index => $data) { + $key = $data['key']; + if (is_array($values)) { + if (array_is_list($values)) { + $value = $values[$index] ?? null; + } elseif (is_int($key) || is_string($key) || is_float($key) || is_bool($key)) { + $value = $values[$key] ?? null; + } else { + $value = null; + } + } elseif ($values instanceof \ArrayAccess) { + $value = $values->offsetExists($key) ? $values->offsetGet($key) : null; + } else { + $value = null; + } + + if ($value instanceof \Throwable) { + $data['reject']($value); + } else { + $data['resolve']($value); + } + } + } + /** * Do not cache individual loads if the entire batch dispatch fails, * but still reject each request so they do not hang. diff --git a/tests/AbuseTestCase.php b/tests/AbuseTestCase.php index dadd0a0..dfd9c8f 100644 --- a/tests/AbuseTestCase.php +++ b/tests/AbuseTestCase.php @@ -84,14 +84,13 @@ public function testBatchFunctionMustReturnAPromiseOfAnArrayNotNull() /** * @group provides-descriptive-error-messages-for-api-abuse */ - public function testBatchFunctionMustPromiseAnArrayOfCorrectLength() + public function testBatchFunctionMayReturnMissingValues() { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('DataLoader must be constructed with a function which accepts Array and returns Promise>, but the function did not return a Promise of an Array of the same length as the Array of keys.'); - - DataLoader::await(self::idLoader(function () { + $value = DataLoader::await(self::idLoader(function () { return self::$promiseAdapter->createFulfilled([]); })->load(1)); + + $this->assertNull($value); } /** diff --git a/tests/DataLoadTestCase.php b/tests/DataLoadTestCase.php index 289ae25..5d8b3cd 100644 --- a/tests/DataLoadTestCase.php +++ b/tests/DataLoadTestCase.php @@ -51,6 +51,24 @@ public function testSupportsLoadingMultipleKeysInOneCall() $this->assertEquals([], DataLoader::await($promiseEmpty)); } + /** + * @group accepts-any-kind-of-key + */ + public function testSupportsKeyedResultsWithMissingValues() + { + $loader = new DataLoader(function () { + return self::$promiseAdapter->createFulfilled(['present' => 'value']); + }, self::$promiseAdapter); + + list($present, $missing) = DataLoader::await(self::$promiseAdapter->createAll([ + $loader->load('present'), + $loader->load('missing'), + ])); + + $this->assertEquals('value', $present); + $this->assertNull($missing); + } + /** * @group primary-api */