diff --git a/Relay/Connection/Paginator.php b/Relay/Connection/Paginator.php index a59031c07..b247844cd 100644 --- a/Relay/Connection/Paginator.php +++ b/Relay/Connection/Paginator.php @@ -11,23 +11,34 @@ namespace Overblog\GraphQLBundle\Relay\Connection; +use GraphQL\Executor\Promise\Promise; use Overblog\GraphQLBundle\Definition\Argument; use Overblog\GraphQLBundle\Relay\Connection\Output\Connection; use Overblog\GraphQLBundle\Relay\Connection\Output\ConnectionBuilder; class Paginator { + const MODE_REGULAR = false; + const MODE_PROMISE = true; + /** * @var callable */ private $fetcher; + /** + * @var bool + */ + private $promise; + /** * @param callable $fetcher + * @param bool $promise */ - public function __construct(callable $fetcher) + public function __construct(callable $fetcher, $promise = self::MODE_REGULAR) { $this->fetcher = $fetcher; + $this->promise = $promise; } /** @@ -47,10 +58,12 @@ public function backward($args, $total, array $callableArgs = []) $entities = call_user_func($this->fetcher, $offset, $limit); - return ConnectionBuilder::connectionFromArraySlice($entities, $args, [ - 'sliceStart' => $offset, - 'arrayLength' => $total, - ]); + return $this->handleEntities($entities, function ($entities) use ($args, $offset, $total) { + return ConnectionBuilder::connectionFromArraySlice($entities, $args, [ + 'sliceStart' => $offset, + 'arrayLength' => $total, + ]); + }); } /** @@ -68,14 +81,18 @@ public function forward($args) if (!is_numeric(ConnectionBuilder::cursorToOffset($args['after'])) || !$args['after']) { $entities = call_user_func($this->fetcher, $offset, $limit + 1); - return ConnectionBuilder::connectionFromArray($entities, $args); + return $this->handleEntities($entities, function ($entities) use ($args) { + return ConnectionBuilder::connectionFromArray($entities, $args); + }); } else { $entities = call_user_func($this->fetcher, $offset, $limit + 2); - return ConnectionBuilder::connectionFromArraySlice($entities, $args, [ - 'sliceStart' => $offset, - 'arrayLength' => $offset + count($entities), - ]); + return $this->handleEntities($entities, function ($entities) use ($args, $offset) { + return ConnectionBuilder::connectionFromArraySlice($entities, $args, [ + 'sliceStart' => $offset, + 'arrayLength' => $offset + count($entities), + ]); + }); } } @@ -97,6 +114,21 @@ public function auto($args, $total, $callableArgs = []) } } + /** + * @param array|object $entities An array of entities to paginate or a promise + * @param callable $callback + * + * @return Connection|object A connection or a promise + */ + private function handleEntities($entities, callable $callback) + { + if ($this->promise) { + return $entities->then($callback); + } + + return call_user_func($callback, $entities); + } + /** * @param Argument|array $args * diff --git a/Resources/doc/helpers/relay-paginator.md b/Resources/doc/helpers/relay-paginator.md index d12cf32da..8e606faa4 100644 --- a/Resources/doc/helpers/relay-paginator.md +++ b/Resources/doc/helpers/relay-paginator.md @@ -16,20 +16,20 @@ This method can be used to get a slice of a data set by passing: - the args, as a `ConnectionArguments` object - the meta, as a `ArraySliceMetaInfo` object -The sliced data set must contains: +The sliced data set must contains: - the item before the first item you want - the item after the slice, so `PageInfo->hasNextPage` can be calculated - -Exemple: - + +Example: + - full data set is `['A','B','C','D','E']` - we want 2 items after `A`, meaning `['B','C']` - `after` cursor will be `arrayconnection:0` - `offset` will be calculated to `0` -- so we need to passed a sliced data with `['A','B','C','D']` to `connectionFromArraySlice()` +- so we need to passed a sliced data with `['A','B','C','D']` to `connectionFromArraySlice()` ## Paginator @@ -39,8 +39,8 @@ The purpose if this helper is to provide an easy way to paginate in a data set p When constructing the paginator, you need to pass a callable which will be responsible for providing the sliced data set. -### Exemple - +### Example + #### With a `first` Relay parameter ```php @@ -120,7 +120,7 @@ $paginator = new Paginator(function ($offset, $limit) { $result = $paginator->forward( new Argument( [ - 'first' => 1, + 'first' => 1, 'after' => base64_encode('arrayconnection:2') ] ) @@ -145,7 +145,7 @@ array(1) { ``` **Important note:** - + The callback function will receive: - `$offset = 2` @@ -194,3 +194,21 @@ $result = $paginator->backward( ``` You should get the 4 last items of the _data set_. + +#### Promise handling + +Paginator also supports promises if you [use that feature](https://github.com/webonyx/graphql-php/pull/67) +with the bundle. All you have to do is to toggle the `MODE_PROMISE` flag on and +update your callback to return a `Executor/Promise/Promise` instance. + +```php +// Let's pretend we use dataloader ( https://github.com/overblog/dataloader-php ) +public function resolveList($args) +{ + $pagination = new Paginator(function ($offset, $limit) { + return $this->dataLoader->loadMany($this->elasticsearch->getIds($offset, $limit)); + }, Paginator::MODE_PROMISE); // This flag indicates that we will return a promise instead of an array of instances + + return $pagination->forward($args); +} +``` diff --git a/Tests/Relay/Connection/PaginatorTest.php b/Tests/Relay/Connection/PaginatorTest.php index e330e0713..de386bca4 100644 --- a/Tests/Relay/Connection/PaginatorTest.php +++ b/Tests/Relay/Connection/PaginatorTest.php @@ -282,4 +282,23 @@ public function testTotalCallableWithArguments() $this->assertSameEdgeNodeValue(['B', 'C', 'D', 'E'], $result); $this->assertTrue($result->pageInfo->hasPreviousPage); } + + public function testPromiseMode() + { + $promise = $this->getMockBuilder('GraphQL\Executor\Promise\Promise') + ->setMethods(['then']) + ->getMock() + ; + + $promise->expects($this->once())->method('then'); + + $paginator = new Paginator(function ($offset, $limit) use ($promise) { + $this->assertSame(0, $offset); + $this->assertSame(5, $limit); + + return $promise; + }, Paginator::MODE_PROMISE); + + $result = $paginator->auto(new Argument(['first' => 4]), 5); + } }