Skip to content

Commit

Permalink
refactor: tidy up cursor pagination implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
lindyhopchris committed Aug 7, 2024
1 parent d46b030 commit fcd5add
Show file tree
Hide file tree
Showing 8 changed files with 529 additions and 311 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. This projec

## Unreleased

### Added

- [#37](https://github.com/laravel-json-api/eloquent/pull/37) Add Eloquent cursor pagination implementation.

## [4.1.0] - 2024-06-26

### Added
Expand Down
59 changes: 18 additions & 41 deletions src/Pagination/Cursor/Cursor.php
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
<?php
/*
* Copyright 2023 Cloud Creativity Limited
* Copyright 2024 Cloud Creativity Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/

declare(strict_types=1);
Expand All @@ -21,40 +13,23 @@

use InvalidArgumentException;

class Cursor
final readonly class Cursor
{

/**
* @var string|null
*/
private ?string $before;

/**
* @var string|null
*/
private ?string $after;

/**
* @var int|null
*/
private ?int $limit;

/**
/**
* Cursor constructor.
*
* @param string|null $before
* @param string|null $after
* @param int|null $limit
*/
public function __construct(string $before = null, string $after = null, int $limit = null)
{
if (is_int($limit) && 1 > $limit) {
public function __construct(
private ?string $before = null,
private ?string $after = null,
private ?int $limit = null
) {
if (is_int($this->limit) && 1 > $this->limit) {
throw new InvalidArgumentException('Expecting a limit that is 1 or greater.');
}

$this->before = $before ?: null;
$this->after = $after ?: null;
$this->limit = $limit;
}

/**
Expand Down Expand Up @@ -97,10 +72,12 @@ public function getAfter(): ?string
*/
public function withDefaultLimit(int $limit): self
{
if (is_null($this->limit)) {
$copy = clone $this;
$copy->limit = $limit;
return $copy;
if ($this->limit === null) {
return new self(
before: $this->before,
after: $this->after,
limit: $limit,
);
}

return $this;
Expand Down
117 changes: 61 additions & 56 deletions src/Pagination/Cursor/CursorBuilder.php
Original file line number Diff line number Diff line change
@@ -1,50 +1,66 @@
<?php
/*
* Copyright 2024 Cloud Creativity Limited
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/

declare(strict_types=1);

namespace LaravelJsonApi\Eloquent\Pagination\Cursor;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Pagination\Cursor as LaravelCursor;
use LaravelJsonApi\Contracts\Schema\ID;
use LaravelJsonApi\Core\Schema\IdParser;

class CursorBuilder
final class CursorBuilder
{
private Builder|Relation $query;

private ID $id;

private string $keyName;
/**
* @var string
*/
private readonly string $keyName;

/**
* @var string
*/
private string $direction;

/**
* @var int|null
*/
private ?int $defaultPerPage = null;

/**
* @var bool
*/
private bool $withTotal;

/**
* @var bool
*/
private bool $keySort = true;

private CursorParser $parser;
/**
* @var CursorParser
*/
private readonly CursorParser $parser;

/**
* CursorBuilder constructor.
*
* @param Builder|Relation $query
* the column to use for the cursor
* @param string|null $key
* the key column that the before/after cursors related to
* @param Builder|Relation $query the column to use for the cursor
* @param ID $id
* @param string|null $key the key column that the before/after cursors related to
*/
public function __construct($query, ID $id, string $key = null)
{
if (!$query instanceof Builder && !$query instanceof Relation) {
throw new \InvalidArgumentException('Expecting an Eloquent query builder or relation.');
}

$this->query = $query;
$this->id = $id;
$this->keyName = $key ?: $this->guessKey();
public function __construct(
private readonly Builder|Relation $query,
private readonly ID $id,
?string $key = null
) {
$this->keyName = $key ?: $this->id->key();
$this->parser = new CursorParser(IdParser::make($this->id), $this->keyName);
}

Expand All @@ -62,8 +78,11 @@ public function withDefaultPerPage(?int $perPage): self
return $this;
}


public function withKeySort(bool $keySort): self
/**
* @param bool $keySort
* @return $this
*/
public function withKeySort(bool $keySort = true): self
{
$this->keySort = $keySort;

Expand All @@ -86,6 +105,10 @@ public function withDirection(string $direction): self
throw new \InvalidArgumentException('Unexpected query direction.');
}

/**
* @param bool $withTotal
* @return $this
*/
public function withTotal(bool $withTotal): self
{
$this->withTotal = $withTotal;
Expand All @@ -103,12 +126,20 @@ public function paginate(Cursor $cursor, array $columns = ['*']): CursorPaginato
$this->applyKeySort();

$total = $this->getTotal();
$laravelPaginator = $this->query->cursorPaginate($cursor->getLimit(), $columns, 'cursor', $this->parser->decode($cursor));
$laravelPaginator = $this->query->cursorPaginate(
$cursor->getLimit(),
$columns,
'cursor',
$this->parser->decode($cursor),
);
$paginator = new CursorPaginator($this->parser, $laravelPaginator, $cursor, $total);

return $paginator->withCurrentPath();
}

/**
* @return void
*/
private function applyKeySort(): void
{
if (!$this->keySort) {
Expand All @@ -125,35 +156,17 @@ private function applyKeySort(): void
}
}

/**
* @return int|null
*/
private function getTotal(): ?int
{
return $this->withTotal ? $this->query->count() : null;
}

private function convertCursor(Cursor $cursor): ?LaravelCursor
{
$encodedCursor = $cursor->isBefore() ? $cursor->getBefore() : $cursor->getAfter();
if (!is_string($encodedCursor)) {
return null;
}

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

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

$pointsToNextItems = $parameters['_pointsToNextItems'];
unset($parameters['_pointsToNextItems']);
if (isset($parameters[$this->keyName])) {
$parameters[$this->keyName] = IdParser::make($this->id)->decode(
(string) $parameters[$this->keyName],
);
}

return new LaravelCursor($parameters, $pointsToNextItems);
}

/**
* @return int
*/
private function getDefaultPerPage(): int
{
if (is_int($this->defaultPerPage)) {
Expand All @@ -162,12 +175,4 @@ private function getDefaultPerPage(): int

return $this->query->getModel()->getPerPage();
}

/**
* Guess the key to use for the cursor.
*/
private function guessKey(): string
{
return $this->id?->key() ?? $this->query->getModel()->getKeyName();
}
}
Loading

0 comments on commit fcd5add

Please sign in to comment.