Skip to content
Merged
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
52 changes: 42 additions & 10 deletions Relay/Connection/Paginator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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,
]);
});
}

/**
Expand All @@ -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),
]);
});
}
}

Expand All @@ -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
*
Expand Down
36 changes: 27 additions & 9 deletions Resources/doc/helpers/relay-paginator.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -120,7 +120,7 @@ $paginator = new Paginator(function ($offset, $limit) {
$result = $paginator->forward(
new Argument(
[
'first' => 1,
'first' => 1,
'after' => base64_encode('arrayconnection:2')
]
)
Expand All @@ -145,7 +145,7 @@ array(1) {
```

**Important note:**

The callback function will receive:

- `$offset = 2`
Expand Down Expand Up @@ -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);
}
```
19 changes: 19 additions & 0 deletions Tests/Relay/Connection/PaginatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}