Skip to content

Commit fcd5add

Browse files
committed
refactor: tidy up cursor pagination implementation
1 parent d46b030 commit fcd5add

File tree

8 files changed

+529
-311
lines changed

8 files changed

+529
-311
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. This projec
55

66
## Unreleased
77

8+
### Added
9+
10+
- [#37](https://github.com/laravel-json-api/eloquent/pull/37) Add Eloquent cursor pagination implementation.
11+
812
## [4.1.0] - 2024-06-26
913

1014
### Added

src/Pagination/Cursor/Cursor.php

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
<?php
22
/*
3-
* Copyright 2023 Cloud Creativity Limited
3+
* Copyright 2024 Cloud Creativity Limited
44
*
5-
* Licensed under the Apache License, Version 2.0 (the "License");
6-
* you may not use this file except in compliance with the License.
7-
* You may obtain a copy of the License at
8-
*
9-
* http://www.apache.org/licenses/LICENSE-2.0
10-
*
11-
* Unless required by applicable law or agreed to in writing, software
12-
* distributed under the License is distributed on an "AS IS" BASIS,
13-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14-
* See the License for the specific language governing permissions and
15-
* limitations under the License.
5+
* Use of this source code is governed by an MIT-style
6+
* license that can be found in the LICENSE file or at
7+
* https://opensource.org/licenses/MIT.
168
*/
179

1810
declare(strict_types=1);
@@ -21,40 +13,23 @@
2113

2214
use InvalidArgumentException;
2315

24-
class Cursor
16+
final readonly class Cursor
2517
{
26-
27-
/**
28-
* @var string|null
29-
*/
30-
private ?string $before;
31-
32-
/**
33-
* @var string|null
34-
*/
35-
private ?string $after;
36-
37-
/**
38-
* @var int|null
39-
*/
40-
private ?int $limit;
41-
42-
/**
18+
/**
4319
* Cursor constructor.
4420
*
4521
* @param string|null $before
4622
* @param string|null $after
4723
* @param int|null $limit
4824
*/
49-
public function __construct(string $before = null, string $after = null, int $limit = null)
50-
{
51-
if (is_int($limit) && 1 > $limit) {
25+
public function __construct(
26+
private ?string $before = null,
27+
private ?string $after = null,
28+
private ?int $limit = null
29+
) {
30+
if (is_int($this->limit) && 1 > $this->limit) {
5231
throw new InvalidArgumentException('Expecting a limit that is 1 or greater.');
5332
}
54-
55-
$this->before = $before ?: null;
56-
$this->after = $after ?: null;
57-
$this->limit = $limit;
5833
}
5934

6035
/**
@@ -97,10 +72,12 @@ public function getAfter(): ?string
9772
*/
9873
public function withDefaultLimit(int $limit): self
9974
{
100-
if (is_null($this->limit)) {
101-
$copy = clone $this;
102-
$copy->limit = $limit;
103-
return $copy;
75+
if ($this->limit === null) {
76+
return new self(
77+
before: $this->before,
78+
after: $this->after,
79+
limit: $limit,
80+
);
10481
}
10582

10683
return $this;
Lines changed: 61 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,66 @@
11
<?php
2+
/*
3+
* Copyright 2024 Cloud Creativity Limited
4+
*
5+
* Use of this source code is governed by an MIT-style
6+
* license that can be found in the LICENSE file or at
7+
* https://opensource.org/licenses/MIT.
8+
*/
29

310
declare(strict_types=1);
411

512
namespace LaravelJsonApi\Eloquent\Pagination\Cursor;
613

714
use Illuminate\Database\Eloquent\Builder;
815
use Illuminate\Database\Eloquent\Relations\Relation;
9-
use Illuminate\Pagination\Cursor as LaravelCursor;
1016
use LaravelJsonApi\Contracts\Schema\ID;
1117
use LaravelJsonApi\Core\Schema\IdParser;
1218

13-
class CursorBuilder
19+
final class CursorBuilder
1420
{
15-
private Builder|Relation $query;
16-
17-
private ID $id;
18-
19-
private string $keyName;
21+
/**
22+
* @var string
23+
*/
24+
private readonly string $keyName;
2025

26+
/**
27+
* @var string
28+
*/
2129
private string $direction;
2230

31+
/**
32+
* @var int|null
33+
*/
2334
private ?int $defaultPerPage = null;
2435

36+
/**
37+
* @var bool
38+
*/
2539
private bool $withTotal;
2640

41+
/**
42+
* @var bool
43+
*/
2744
private bool $keySort = true;
2845

29-
private CursorParser $parser;
46+
/**
47+
* @var CursorParser
48+
*/
49+
private readonly CursorParser $parser;
3050

3151
/**
3252
* CursorBuilder constructor.
3353
*
34-
* @param Builder|Relation $query
35-
* the column to use for the cursor
36-
* @param string|null $key
37-
* the key column that the before/after cursors related to
54+
* @param Builder|Relation $query the column to use for the cursor
55+
* @param ID $id
56+
* @param string|null $key the key column that the before/after cursors related to
3857
*/
39-
public function __construct($query, ID $id, string $key = null)
40-
{
41-
if (!$query instanceof Builder && !$query instanceof Relation) {
42-
throw new \InvalidArgumentException('Expecting an Eloquent query builder or relation.');
43-
}
44-
45-
$this->query = $query;
46-
$this->id = $id;
47-
$this->keyName = $key ?: $this->guessKey();
58+
public function __construct(
59+
private readonly Builder|Relation $query,
60+
private readonly ID $id,
61+
?string $key = null
62+
) {
63+
$this->keyName = $key ?: $this->id->key();
4864
$this->parser = new CursorParser(IdParser::make($this->id), $this->keyName);
4965
}
5066

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

65-
66-
public function withKeySort(bool $keySort): self
81+
/**
82+
* @param bool $keySort
83+
* @return $this
84+
*/
85+
public function withKeySort(bool $keySort = true): self
6786
{
6887
$this->keySort = $keySort;
6988

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

108+
/**
109+
* @param bool $withTotal
110+
* @return $this
111+
*/
89112
public function withTotal(bool $withTotal): self
90113
{
91114
$this->withTotal = $withTotal;
@@ -103,12 +126,20 @@ public function paginate(Cursor $cursor, array $columns = ['*']): CursorPaginato
103126
$this->applyKeySort();
104127

105128
$total = $this->getTotal();
106-
$laravelPaginator = $this->query->cursorPaginate($cursor->getLimit(), $columns, 'cursor', $this->parser->decode($cursor));
129+
$laravelPaginator = $this->query->cursorPaginate(
130+
$cursor->getLimit(),
131+
$columns,
132+
'cursor',
133+
$this->parser->decode($cursor),
134+
);
107135
$paginator = new CursorPaginator($this->parser, $laravelPaginator, $cursor, $total);
108136

109137
return $paginator->withCurrentPath();
110138
}
111139

140+
/**
141+
* @return void
142+
*/
112143
private function applyKeySort(): void
113144
{
114145
if (!$this->keySort) {
@@ -125,35 +156,17 @@ private function applyKeySort(): void
125156
}
126157
}
127158

159+
/**
160+
* @return int|null
161+
*/
128162
private function getTotal(): ?int
129163
{
130164
return $this->withTotal ? $this->query->count() : null;
131165
}
132166

133-
private function convertCursor(Cursor $cursor): ?LaravelCursor
134-
{
135-
$encodedCursor = $cursor->isBefore() ? $cursor->getBefore() : $cursor->getAfter();
136-
if (!is_string($encodedCursor)) {
137-
return null;
138-
}
139-
140-
$parameters = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $encodedCursor)), true);
141-
142-
if (json_last_error() !== JSON_ERROR_NONE) {
143-
return null;
144-
}
145-
146-
$pointsToNextItems = $parameters['_pointsToNextItems'];
147-
unset($parameters['_pointsToNextItems']);
148-
if (isset($parameters[$this->keyName])) {
149-
$parameters[$this->keyName] = IdParser::make($this->id)->decode(
150-
(string) $parameters[$this->keyName],
151-
);
152-
}
153-
154-
return new LaravelCursor($parameters, $pointsToNextItems);
155-
}
156-
167+
/**
168+
* @return int
169+
*/
157170
private function getDefaultPerPage(): int
158171
{
159172
if (is_int($this->defaultPerPage)) {
@@ -162,12 +175,4 @@ private function getDefaultPerPage(): int
162175

163176
return $this->query->getModel()->getPerPage();
164177
}
165-
166-
/**
167-
* Guess the key to use for the cursor.
168-
*/
169-
private function guessKey(): string
170-
{
171-
return $this->id?->key() ?? $this->query->getModel()->getKeyName();
172-
}
173178
}

0 commit comments

Comments
 (0)