The Paginator
can be used in resolvers to get a sliced data set when dealing with Relay Connections.
Relay specification: https://facebook.github.io/relay/graphql/connections.htm
Connection implementation in JS: https://github.com/graphql/graphql-relay-js/tree/master/src/connection
The connectionFromArraySlice()
method:
This method can be used to get a slice of a data set by passing:
- the sliced data set to calculate the edges from
- the args, as a
ConnectionArguments
object - the meta, as a
ArraySliceMetaInfo
object
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
Example:
-
full data set is
['A','B','C','D','E']
-
we want 2 items after
A
, meaning['B','C']
-
after
cursor will bearrayconnection:0
-
offset
will be calculated to0
-
so we need to passed a sliced data with
['A','B','C','D']
toconnectionFromArraySlice()
See: Overblog\GraphQLBundle\Relay\Connection\Paginator
The purpose of this helper is to provide an easy way to paginate in a data set provided by a backend.
When constructing the paginator, you need to pass a callable which will be responsible for providing the sliced data set.
<?php
use Overblog\GraphQLBundle\Definition\Argument;
use Overblog\GraphQLBundle\Relay\Connection\Paginator;
function getData($offset = 0)
{
return array_slice(['A', 'B', 'C', 'D', 'E'], $offset);
}
$paginator = new Paginator(function ($offset, $limit) {
return getData($offset);
});
$result = $paginator->forward(new Argument(['first' => 4]));
var_dump($result->edges);
Output
array(4) {
[0]=>
object(Overblog\GraphQLBundle\Relay\Connection\Output\Edge)#24 (2) {
["cursor"]=>
string(24) "YXJyYXljb25uZWN0aW9uOjA="
["node"]=>
string(1) "A"
}
[1]=>
object(Overblog\GraphQLBundle\Relay\Connection\Output\Edge)#25 (2) {
["cursor"]=>
string(24) "YXJyYXljb25uZWN0aW9uOjE="
["node"]=>
string(1) "B"
}
[2]=>
object(Overblog\GraphQLBundle\Relay\Connection\Output\Edge)#26 (2) {
["cursor"]=>
string(24) "YXJyYXljb25uZWN0aW9uOjI="
["node"]=>
string(1) "C"
}
[3]=>
object(Overblog\GraphQLBundle\Relay\Connection\Output\Edge)#27 (2) {
["cursor"]=>
string(24) "YXJyYXljb25uZWN0aW9uOjM="
["node"]=>
string(1) "D"
}
}
Note: we want 1 item after C
so the decoded cursor is arrayconnection:2
<?php
use Overblog\GraphQLBundle\Definition\Argument;
use Overblog\GraphQLBundle\Relay\Connection\Paginator;
function getData($offset = 0)
{
return array_slice(['A', 'B', 'C', 'D', 'E'], $offset);
}
$paginator = new Paginator(function ($offset, $limit) {
return getData($offset);
});
$result = $paginator->forward(
new Argument(
[
'first' => 1,
'after' => base64_encode('arrayconnection:2')
]
)
);
var_dump($result->edges);
Output
array(1) {
[0]=>
object(Overblog\GraphQLBundle\Relay\Connection\Output\Edge)#26 (2) {
["cursor"]=>
string(24) "YXJyYXljb25uZWN0aW9uOjM="
["node"]=>
string(1) "D"
}
}
Important note:
The callback function will receive:
$offset = 2
$limit = 3
And it must return at least ['C','D','E']
<?php
use Overblog\GraphQLBundle\Definition\Argument;
use Overblog\GraphQLBundle\Relay\Connection\Paginator;
class DataBackend
{
public $data = ['A', 'B', 'C', 'D', 'E'];
public function getData($offset = 0)
{
return array_slice($this->data, $offset);
}
public function count($array)
{
return count($array);
}
public function countAll()
{
return count($this->data);
}
}
$backend = new DataBackend();
$paginator = new Paginator(function ($offset, $limit) use ($backend) {
return $backend->getData($offset);
});
$result = $paginator->backward(
new Argument(
[
'last' => 4,
]
),
[$backend, 'count'],
['array' => $backend->getData]
);
You should get the 4 last items of the data set.
resolve: '@=resolver("App\\GraphQL\\Resolver\\Greetings::sayHello", [args])'
sayHello(first: 1, after: "YXJyYXljb25uZWN0aW9uOjI="){ # after: base64_encode('arrayconnection:2')
edges {
cursor # YXJyYXljb25uZWN0aW9uOjM=
node # D
}
pageInfo {
hasNextPage # true
}
}
<?php
namespace App\GraphQL\Resolver;
use Overblog\GraphQLBundle\Definition\Argument;
use Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface;
use Overblog\GraphQLBundle\Relay\Connection\Paginator;
class Greetings implements ResolverInterface
{
public function sayHello(Argument $args)
{
$backend = new DataBackend();
$paginator = new Paginator(function ($offset, $limit) use ($backend) {
return $backend->getData($offset, $limit);
});
return $paginator->auto($args, function() use ($backend) {
return $backend->countAll();
});
}
}
Paginator also supports promises if you use that feature
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.
// 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);
}
Sometimes, you want to add fields to your Connection or Edges. In order to do so, you'll have to pass a custom instance of ConnectionBuilder
to your Paginator as follow:
use Overblog\GraphQLBundle\Relay\Connection\ConnectionBuilder;
use Overblog\GraphQLBundle\Relay\Connection\Cursor\Base64CursorEncoder;
public function resolveSomething(Argument $args)
{
$connectionBuilder = new ConnectionBuilder(
new Base64CursorEncoder(),
function(iterable $edges, PageInfo $pageInfo) : FriendsConnection {
$connection = new FriendsConnection($edges, $pageInfo);
$connection->setAverageAge(calculateAverage($edges));
return $connection;
},
function(string $cursor, UserFriend $entity, int $index): FriendEdge {
$edge = new FriendEdge($cursor, $entity->getUser());
$edge->setFriendshipTime($entity->getCreatedAt());
return $edge;
}
);
$paginator = new Paginator(function ($offset, $limit) use ($backend) {
return $backend->getData($offset);
}, true, $connectionBuilder);
}
The ConnectionBuilder
constructor accepts three parameters. The first one is an encoder that will be used to encode the cursor of the edges, the second is a callback to build the Connection object and the last one is a callback to build an Edge object.
The connection callback will be call with the following parameters :
edges
An array of edges object implementingOverblog\GraphQLBundle\Relay\Connection\EdgeInterface
pageInfo
a PageInfo objectOverblog\GraphQLBundle\Relay\Connection\Output\PageInfo
This callback MUST return an instance of Overblog\GraphQLBundle\Relay\Connection\ConnectionInterface
The edge callback will be call with the following parameters :
cursor
The cursorvalue
A value returned by the paginator data fetcherindex
The index of the value
This callback MUST return an instance of Overblog\GraphQLBundle\Relay\Connection\EdgeInterface
If no callback are specified for the ConnectionBuilder
, it'll generate instance of Overblog\GraphQLBundle\Relay\Connection\Output\Connection
and Overblog\GraphQLBundle\Relay\Connection\Output\Edge