Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
76 changes: 49 additions & 27 deletions src/DataLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<key> and returns Promise<Array<value>>, 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<key> and returns Promise<Array<value>>, 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<key> and returns Promise<Array<value>>, 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.
Expand Down
9 changes: 4 additions & 5 deletions tests/AbuseTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<key> and returns Promise<Array<value>>, 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);
}

/**
Expand Down
18 changes: 18 additions & 0 deletions tests/DataLoadTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Loading