Source of file Connection.php
Size: 11,812 Bytes - Last Modified: 2021-12-23T10:31:47+00:00
/var/www/docs.ssmods.com/process/src/src/Pagination/Connection.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479 | <?php namespace SilverStripe\GraphQL\Pagination; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; use InvalidArgumentException; use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injector; use SilverStripe\GraphQL\OperationResolver; use SilverStripe\GraphQL\Permission\PermissionCheckerAware; use SilverStripe\ORM\Limitable; use SilverStripe\ORM\Sortable; use SilverStripe\ORM\SS_List; /** * A connection to a list of items on a object type. Collections are paginated * and return a list of edges. * * <code> * friends(limit:2,offset:2,sortBy:[{field:Name,direction:ASC}]) { * edges { * node { * name * } * } * pageInfo { * totalCount * hasPreviousPage * hasNextPage * } * } * </code> */ class Connection implements OperationResolver { use Injectable; use PermissionCheckerAware; /** * @var string */ protected $connectionName; /** * Return a thunk function, which in turn returns the lazy-evaluated * {@link ObjectType}. * * @var ObjectType|Callable */ protected $connectedType; /** * Cache the instance of the connection type to guarantee referential equality for graphql-php * @var ObjectType */ protected $connectionTypeInstance; /** * @var string */ protected $description; /** * @var Callable */ protected $connectionResolver; /** * @var array */ protected $args = []; /** * @var array Keyed by field argument name, values as DataObject column names. * Does not support in-memory sorting for composite values (getters). */ protected $sortableFields = []; /** * @var int */ protected $defaultLimit = 100; /** * The maximum limit supported for the connection. Used to prevent excessive * load on the server. To override the default limit, use {@link setLimits} * * @var int */ protected $maximumLimit = 100; /** * @param string $connectionName */ public function __construct($connectionName) { $this->connectionName = $connectionName; } /** * @param Callable * * @return $this */ public function setConnectionResolver($func) { $this->connectionResolver = $func; return $this; } /** * Pass in the {@link ObjectType}. * * @param ObjectType|Callable $type Type, or callable to evaluate type * @return $this */ public function setConnectionType($type) { $this->connectedType = $type; return $this; } /** * Evaluate Connection type * * @param bool $evaluate * @return ObjectType|Callable */ public function getConnectionType($evaluate = true) { if ($this->connectionTypeInstance) { return $this->connectionTypeInstance; } $instance = ($evaluate && is_callable($this->connectedType)) ? call_user_func($this->connectedType) : $this->connectedType; $this->connectionTypeInstance = $instance; return $this->connectionTypeInstance; } /** * @return Callable */ public function getConnectionResolver() { return $this->connectionResolver; } /** * @param array|Callable * * @return $this */ public function setArgs($args) { $this->args = $args; return $this; } /** * @param string * * @return $this */ public function setDescription($string) { $this->description = $string; return $this; } /** * @return string */ public function getDescription() { return $this->description; } /** * @param array $fields See {@link $sortableFields} * @return $this */ public function setSortableFields($fields) { foreach ($fields as $field => $lookup) { $this->sortableFields[is_numeric($field) ? $lookup : $field] = $lookup; } return $this; } /** * @return array */ public function getSortableFields() { return $this->sortableFields; } /** * @param int * * @return $this */ public function setDefaultLimit($limit) { $this->defaultLimit = $limit; return $this; } /** * @return int */ public function getDefaultLimit() { return $this->defaultLimit; } /** * @param int * * @return $this */ public function setMaximumLimit($limit) { $this->maximumLimit = $limit; return $this; } /** * @return string */ public function getConnectionTypeName() { return $this->connectionName . 'Connection'; } /** * @return string */ public function getEdgeTypeName() { return $this->connectionName . 'Edge'; } /** * Pagination support for the connection type. Currently doesn't support * cursors, just basic offset pagination. * * @return array */ public function args() { $existing = is_callable($this->args) ? call_user_func($this->args) : $this->args; if (!is_array($existing)) { $existing = []; } $args = array_merge($existing, [ 'limit' => [ 'type' => Type::int(), ], 'offset' => [ 'type' => Type::int() ] ]); if ($fields = $this->getSortableFields()) { $args['sortBy'] = [ 'type' => Type::listOf( Injector::inst()->create(SortInputTypeCreator::class, $this->connectionName) ->setSortableFields($fields) ->toType() ) ]; } return $args; } /** * @return array */ public function fields() { return [ 'pageInfo' => [ 'type' => Type::nonNull( Injector::inst()->get(PageInfoTypeCreator::class)->toType() ), 'description' => 'Pagination information' ], 'edges' => [ 'type' => Type::listOf($this->getEdgeType()), 'description' => 'Collection of records' ], 'nodes' => [ 'type' => Type::listOf($this->getConnectionType()), 'description' => 'The node at the end of the collections edge', ] ]; } /** * @return ObjectType */ public function getEdgeType() { if (!$this->connectedType) { throw new InvalidArgumentException('Missing connectedType callable'); } return new ObjectType([ 'name' => $this->getEdgeTypeName(), 'description' => 'The collections edge', 'fields' => function () { return [ 'node' => [ 'type' => $this->getConnectionType(), 'description' => 'The node at the end of the collections edge', 'resolve' => function ($obj) { return $obj; } ] ]; } ]); } /** * @return ObjectType */ public function toType() { return new ObjectType([ 'name' => $this->getConnectionTypeName(), 'description' => $this->description, 'fields' => function () { return $this->fields(); }, ]); } /** * Returns the collection resolved with the pageInfo provided. * * @param mixed $value * @param array $args * @param array $context * @param ResolveInfo $info * @return array * @throws \Exception */ public function resolve($value, array $args, $context, ResolveInfo $info) { $result = call_user_func_array( $this->connectionResolver, func_get_args() ); if (!$result instanceof SS_List) { throw new \Exception('Connection::resolve() must resolve to a SS_List instance.'); } return $this->resolveList($result, $args, $context, $info); } /** * Wraps an {@link SS_List} with the required data in order to return it as * a response. If you wish to resolve a standard array as a list use * {@link ArrayList}. * * @param SS_List $list * @param array $args * @param null $context * @param ResolveInfo $info * @return array */ public function resolveList($list, array $args, $context = null, ResolveInfo $info = null) { // Apply sort if (!empty($args['sortBy'])) { $list = $this->applySort($list, $args['sortBy']); } // Default values $count = $list->count(); $nextPage = false; $previousPage = false; // If list is limitable, apply pagination if ($list instanceof Limitable) { $offset = empty($args['offset']) ? 0 : $args['offset']; $limit = empty($args['limit']) ? $this->defaultLimit : $args['limit']; if ($limit > $this->maximumLimit) { $limit = $this->maximumLimit; } // Apply limit $list = $list->limit($limit, $offset); // Flag prev-next page if ($limit && (($limit + $offset) < $count)) { $nextPage = true; } if ($offset > 0) { $previousPage = true; } } if ($checker = $this->getPermissionChecker()) { $currentUser = isset($context['currentUser']) ? $context['currentUser'] : null; $list = $checker->applyToList($list, $currentUser); } return [ 'edges' => $list, 'nodes' => $list, 'pageInfo' => [ 'totalCount' => $count, 'hasNextPage' => $nextPage, 'hasPreviousPage' => $previousPage ], ]; } /** * @param SS_List $list * @param array $sortBy * @return SS_List Sorted list, if sortable * @throws InvalidArgumentException If an invalid sort column is specified */ protected function applySort($list, $sortBy) { // Ensure list is sortable if (!$list instanceof Sortable) { return $list; } $sortableFields = $this->getSortableFields(); // convert the input from the input format of field, direction // to an accepted SS_List sort format. // https://github.com/graphql/graphql-relay-js/issues/20#issuecomment-220494222 $sort = []; foreach ($sortBy as $sortInput) { if (isset($sortInput['field'])) { $direction = isset($sortInput['direction']) ? $sortInput['direction'] : 'ASC'; if (!array_key_exists($sortInput['field'], $sortableFields)) { throw new InvalidArgumentException(sprintf( '"%s" is not a valid sort column', $sortInput['field'] )); } $column = $sortableFields[$sortInput['field']]; $sort[$column] = $direction; } } if ($sort) { $list = $list->sort($sort); } return $list; } } |