Skip to content

Commit

Permalink
Added cursor paginator for hyperf/database. (#6809)
Browse files Browse the repository at this point in the history
  • Loading branch information
zds-s authored May 30, 2024
1 parent 8367a1b commit e69c853
Show file tree
Hide file tree
Showing 9 changed files with 1,250 additions and 0 deletions.
545 changes: 545 additions & 0 deletions src/AbstractCursorPaginator.php

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions src/AbstractPaginator.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ abstract class AbstractPaginator implements PaginatorInterface, ArrayAccess, Str
*/
protected static ?Closure $currentPageResolver = null;

/**
* The query string resolver callback.
*/
protected static ?Closure $queryStringResolver = null;

/**
* Make dynamic calls into the collection.
*/
Expand Down Expand Up @@ -411,6 +416,38 @@ public function offsetUnset(mixed $offset): void
$this->items->forget($offset);
}

/**
* Add all current query string values to the paginator.
*/
public function withQueryString(): static
{
if (isset(static::$queryStringResolver)) {
return $this->appends(call_user_func(static::$queryStringResolver));
}

return $this;
}

/**
* Resolve the query string or return the default value.
*/
public static function resolveQueryString(null|array|string $default = null): string
{
if (isset(static::$queryStringResolver)) {
return (static::$queryStringResolver)();
}

return $default;
}

/**
* Set with query string resolver callback.
*/
public static function queryStringResolver(Closure $resolver): void
{
static::$queryStringResolver = $resolver;
}

/**
* Determine if the given value is a valid page number.
*/
Expand Down
93 changes: 93 additions & 0 deletions src/Contract/CursorPaginator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact [email protected]
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/

namespace Hyperf\Paginator\Contract;

use Hyperf\Paginator\Cursor;

interface CursorPaginator
{
/**
* Get the URL for a given cursor.
*/
public function url(?Cursor $cursor): string;

/**
* Add a set of query string values to the paginator.
*/
public function appends(null|array|string $key, ?string $value = null): static;

/**
* Get / set the URL fragment to be appended to URLs.
*/
public function fragment(?string $fragment = null): null|static|string;

/**
* Add all current query string values to the paginator.
*/
public function withQueryString(): static;

/**
* Get the URL for the previous page, or null.
*/
public function previousPageUrl(): ?string;

/**
* The URL for the next page, or null.
*/
public function nextPageUrl(): ?string;

/**
* Get all of the items being paginated.
*/
public function items(): array;

/**
* Get the "cursor" of the previous set of items.
*/
public function previousCursor(): ?Cursor;

/**
* Get the "cursor" of the next set of items.
*/
public function nextCursor(): ?Cursor;

/**
* Determine how many items are being shown per page.
*/
public function perPage(): int;

/**
* Get the current cursor being paginated.
*/
public function cursor(): ?Cursor;

/**
* Determine if there are enough items to split into multiple pages.
*/
public function hasPages(): bool;

/**
* Get the base path for paginator generated URLs.
*/
public function path(): ?string;

/**
* Determine if the list of items is empty or not.
*/
public function isEmpty(): bool;

/**
* Determine if the list of items is not empty.
*/
public function isNotEmpty(): bool;
}
110 changes: 110 additions & 0 deletions src/Cursor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact [email protected]
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/

namespace Hyperf\Paginator;

use Hyperf\Contract\Arrayable;
use UnexpectedValueException;

use function Hyperf\Collection\collect;

class Cursor implements Arrayable
{
/**
* Create a new cursor instance.
* @param array $parameters the parameters associated with the cursor
* @param bool $pointsToNextItems determine whether the cursor points to the next or previous set of items
*/
public function __construct(
protected array $parameters,
protected bool $pointsToNextItems = true
) {
}

/**
* Get the given parameter from the cursor.
*/
public function parameter(string $parameterName): ?string
{
if (! array_key_exists($parameterName, $this->parameters)) {
throw new UnexpectedValueException("Unable to find parameter [{$parameterName}] in pagination item.");
}

return (string) $this->parameters[$parameterName];
}

/**
* Get the given parameters from the cursor.
*/
public function parameters(array $parameterNames): array
{
return collect($parameterNames)->map(function ($parameterName) {
return $this->parameter($parameterName);
})->toArray();
}

/**
* Determine whether the cursor points to the next set of items.
*/
public function pointsToNextItems(): bool
{
return $this->pointsToNextItems;
}

/**
* Determine whether the cursor points to the previous set of items.
*/
public function pointsToPreviousItems(): bool
{
return ! $this->pointsToNextItems;
}

/**
* Get the array representation of the cursor.
*/
public function toArray(): array
{
return array_merge($this->parameters, [
'_pointsToNextItems' => $this->pointsToNextItems,
]);
}

/**
* Get the encoded string representation of the cursor to construct a URL.
*/
public function encode(): string
{
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode(json_encode($this->toArray(), JSON_THROW_ON_ERROR)));
}

/**
* Get a cursor instance from the encoded string representation.
*/
public static function fromEncoded(?string $encodedString): ?static
{
if (! is_string($encodedString)) {
return null;
}

$parameters = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $encodedString)), true);

if (json_last_error() !== JSON_ERROR_NONE) {
return null;
}

$pointsToNextItems = $parameters['_pointsToNextItems'];

unset($parameters['_pointsToNextItems']);

return new static($parameters, $pointsToNextItems);
}
}
138 changes: 138 additions & 0 deletions src/CursorPaginator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact [email protected]
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/

namespace Hyperf\Paginator;

use ArrayAccess;
use Countable;
use Hyperf\Collection\Collection;
use Hyperf\Contract\Arrayable;
use Hyperf\Contract\Jsonable;
use Hyperf\Paginator\Contract\CursorPaginator as CursorPaginatorContract;
use IteratorAggregate;
use JsonSerializable;

class CursorPaginator extends AbstractCursorPaginator implements ArrayAccess, Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable, CursorPaginatorContract
{
/**
* Indicates whether there are more items in the data source.
*
* @return bool
*/
protected bool $hasMore;

/**
* Create a new paginator instance.
*/
public function __construct(mixed $items, int $perPage, ?Cursor $cursor = null, array $options = [])
{
$this->options = $options;

foreach ($options as $key => $value) {
$this->{$key} = $value;
}

$this->perPage = (int) $perPage;
$this->cursor = $cursor;
$this->path = $this->path !== '/' ? rtrim($this->path, '/') : $this->path;

$this->setItems($items);
}

public function __toString(): string
{
return $this->toJson();
}

/**
* Determine if there are more items in the data source.
*/
public function hasMorePages(): bool
{
return (is_null($this->cursor) && $this->hasMore)
|| (! is_null($this->cursor) && $this->cursor->pointsToNextItems() && $this->hasMore)
|| (! is_null($this->cursor) && $this->cursor->pointsToPreviousItems());
}

/**
* Determine if there are enough items to split into multiple pages.
*/
public function hasPages(): bool
{
return ! $this->onFirstPage() || $this->hasMorePages();
}

/**
* Determine if the paginator is on the first page.
*/
public function onFirstPage(): bool
{
return is_null($this->cursor) || ($this->cursor->pointsToPreviousItems() && ! $this->hasMore);
}

/**
* Determine if the paginator is on the last page.
*/
public function onLastPage(): bool
{
return ! $this->hasMorePages();
}

/**
* Get the instance as an array.
*/
public function toArray(): array
{
return [
'data' => $this->items->toArray(),
'path' => $this->path(),
'per_page' => $this->perPage(),
'next_cursor' => $this->nextCursor()?->encode(),
'next_page_url' => $this->nextPageUrl(),
'prev_cursor' => $this->previousCursor()?->encode(),
'prev_page_url' => $this->previousPageUrl(),
];
}

/**
* Convert the object into something JSON serializable.
*/
public function jsonSerialize(): array
{
return $this->toArray();
}

/**
* Convert the object to its JSON representation.
*/
public function toJson(int $options = 0): string
{
return json_encode($this->jsonSerialize(), JSON_THROW_ON_ERROR | $options);
}

/**
* Set the items for the paginator.
* @param mixed $items
*/
protected function setItems($items)
{
$this->items = $items instanceof Collection ? $items : Collection::make($items);

$this->hasMore = $this->items->count() > $this->perPage;

$this->items = $this->items->slice(0, $this->perPage);

if (! is_null($this->cursor) && $this->cursor->pointsToPreviousItems()) {
$this->items = $this->items->reverse()->values();
}
}
}
Loading

0 comments on commit e69c853

Please sign in to comment.