The task
Suppose we have an API POST endpoint which returns answer like this:
{
"data": [1,2,3],
"total_rows": 20
}
Which means that we received data partially, only the first page of 3 entries. The total entries count is 20, which means we want to call other pages using some offset in request. The total count (and possibly offset) is therefore only known after the first call is completed, while other calls do not depend on each other and can be done simultaneously.
The Toolset
It's PHP and for this task Guzzle 6 is used with Promises/A+. Additionally, Guzzle offers EachPromise class, which receives an iterable of multiple promises and a config hash to setup processing. As per PhpDoc block of that class constructor:
Configuration hash can include the following key value pairs:
- fulfilled: (callable) Invoked when a promise fulfills. The function is invoked with three arguments: the fulfillment value, the index
position from the iterable list of the promise, and the aggregate
promise that manages all of the promises. The aggregate promise may
be resolved from within the callback to short-circuit the promise.- rejected: (callable) Invoked when a promise is rejected. The function is invoked with three arguments: the rejection reason, the
index position from the iterable list of the promise, and the
aggregate promise that manages all of the promises. The aggregate
promise may be resolved from within the callback to short-circuit
the promise.- concurrency: (integer) Pass this configuration option to limit the allowed number of outstanding concurrently executing promises,
creating a capped pool of promises. There is no limit by default.
The code
$paginatedResult = $this->client->sendAsync($this->createRequest($requestBody))
->then(function (ResponseInterface $response) use ($requestBody) {
return $this->deserializeToPaginatedResult(
$response,
$requestBody->paginatedResultClass()
);
}
)->wait();
$pageGenerator = function () use ($paginatedResult, $requestBody) {
$perPageCount = count($paginatedResult->getItems());
$totalItems = $paginatedResult->getTotalCount();
for ($currentOffset = $perPageCount; $currentOffset <= $totalItems; $currentOffset += $perPageCount) {
$newRequestBody = clone $requestBody;
$newRequestBody->setOffset($currentOffset);
yield $this->client->sendAsync($this->createRequest($newRequestBody));
}
};
$aggregatedResult = (new EachPromise(
$pageGenerator(), [
'concurrency' => 4,
'fulfilled' => function ($promiseResult, $promiseIndex, $promiseAggregate) use ($requestBody) {
$paginatedResult = $this->deserializeToPaginatedResult(
$promiseResult,
$requestBody->paginatedResultClass()
);
return $paginatedResult->getItems();
},
]
))->promise()
->then(
function ($promisedAggregatedResult) {
var_dump($promisedAggregatedResult);
}
)
->wait();
var_dump($aggregatedResult);
The problem
In the config hash the fulfilled
callback receives 3 objects, as documentation states. The $promiseResult
can be properly handled and $paginatedResult->getItems()
actually returns an array of items in requested page, but I cannot aggregate those items. The $aggregatedResult
is null
, and the $promisedAggregatedResult
inside the last thened fulfillment callback is also null
.
The question
How should one properly use Guzzle's EachPromise
(and it's helper functions each
and each_limit
) to aggregate results of all the promises passed to it?