Passed
Push — master ( 4ca5b7...1dae24 )
by Alexander
01:47
created

KeysetPaginator::withNextPageToken()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 6
ccs 5
cts 5
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Data\Paginator;
6
7
use Yiisoft\Data\Reader\Filter\GreaterThan;
8
use Yiisoft\Data\Reader\Filter\GreaterThanOrEqual;
9
use Yiisoft\Data\Reader\Filter\LessThan;
10
use Yiisoft\Data\Reader\DataReaderInterface;
11
use Yiisoft\Data\Reader\Filter\LessThanOrEqual;
12
use Yiisoft\Data\Reader\FilterableDataInterface;
13
use Yiisoft\Data\Reader\SortableDataInterface;
14
15
/**
16
 * Keyset paginator
17
 *
18
 * - Equally fast for 1st and 1000nd page
19
 * - Total number of pages is not available
20
 * - Cannot get to specific page, only "next" and "previous"
21
 *
22
 * @link https://use-the-index-luke.com/no-offset
23
 */
24
class KeysetPaginator implements PaginatorInterface
25
{
26
    /**
27
     * @var FilterableDataInterface|DataReaderInterface|SortableDataInterface
28
     */
29
    private $dataReader;
30
    /**
31
     * @var int
32
     */
33
    private $pageSize = self::DEFAULT_PAGE_SIZE;
34
35
    private $firstValue;
36
    private $lastValue;
37
38
    private $currentFirstValue;
39
    private $currentLastValue;
40
41
    /**
42
     * @var bool Previous page has item indicator.
43
     */
44
    private $hasPreviousPageItem = false;
45
    /**
46
     * @var bool Next page has item indicator.
47
     */
48
    private $hasNextPageItem = false;
49
50
    /**
51
     * @var array|null Reader cache against repeated scans.
52
     *
53
     * See more {@see __clone()} and {@see initializeInternal()}.
54
     */
55
    private $readCache;
56
57 28
    public function __construct(DataReaderInterface $dataReader)
58
    {
59 28
        if (!$dataReader instanceof FilterableDataInterface) {
60 1
            throw new \InvalidArgumentException('Data reader should implement FilterableDataInterface in order to be used with keyset paginator');
61
        }
62
63 27
        if (!$dataReader instanceof SortableDataInterface) {
64 1
            throw new \InvalidArgumentException('Data reader should implement SortableDataInterface in order to be used with keyset paginator');
65
        }
66
67 26
        if ($dataReader->getSort() === null) {
68 1
            throw new \RuntimeException('Data sorting should be configured in order to work with keyset pagination');
69
        }
70
71 25
        $this->dataReader = $dataReader;
72
    }
73
74
    /**
75
     * Reads items of the page
76
     *
77
     * This method uses the read cache to prevent duplicate reads from the data source. See more {@see resetInternal()}
78
     *
79
     * @return iterable
80
     */
81 23
    public function read(): iterable
82
    {
83 23
        if ($this->readCache) {
84 1
            return $this->readCache;
85
        }
86
87 23
        $dataReader = $this->dataReader->withLimit($this->pageSize + 1);
0 ignored issues
show
Bug introduced by
The method withLimit() does not exist on Yiisoft\Data\Reader\SortableDataInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Data\Reader\SortableDataInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

87
        /** @scrutinizer ignore-call */ 
88
        $dataReader = $this->dataReader->withLimit($this->pageSize + 1);
Loading history...
Bug introduced by
The method withLimit() does not exist on Yiisoft\Data\Reader\FilterableDataInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Data\Reader\FilterableDataInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

87
        /** @scrutinizer ignore-call */ 
88
        $dataReader = $this->dataReader->withLimit($this->pageSize + 1);
Loading history...
88
89 23
        $sort = $this->dataReader->getSort();
0 ignored issues
show
Bug introduced by
The method getSort() does not exist on Yiisoft\Data\Reader\FilterableDataInterface. It seems like you code against a sub-type of Yiisoft\Data\Reader\FilterableDataInterface such as Yiisoft\Data\Reader\Iterable\IterableDataReader. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

89
        /** @scrutinizer ignore-call */ 
90
        $sort = $this->dataReader->getSort();
Loading history...
Bug introduced by
The method getSort() does not exist on Yiisoft\Data\Reader\DataReaderInterface. It seems like you code against a sub-type of Yiisoft\Data\Reader\DataReaderInterface such as anonymous//tests/Paginat...ysetPaginatorTest.php$0 or Yiisoft\Data\Reader\Iterable\IterableDataReader. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

89
        /** @scrutinizer ignore-call */ 
90
        $sort = $this->dataReader->getSort();
Loading history...
90 23
        $order = $sort->getOrder();
91
92 23
        if ($order === []) {
93 1
            throw new \RuntimeException('Data should be always sorted in order to work with keyset pagination');
94
        }
95
96 22
        $goingToPreviousPage = $this->firstValue !== null && $this->lastValue === null;
97 22
        $goingToNextPage = $this->firstValue === null && $this->lastValue !== null;
98
99 22
        if ($goingToPreviousPage) {
100
            // reverse sorting
101 4
            foreach ($order as &$sorting) {
102 4
                $sorting = $sorting === 'asc' ? 'desc' : 'asc';
103
            }
104 4
            unset($sorting);
105 4
            $dataReader = $dataReader->withSort($sort->withOrder($order));
0 ignored issues
show
Bug introduced by
The method withSort() does not exist on Yiisoft\Data\Reader\FilterableDataInterface. It seems like you code against a sub-type of Yiisoft\Data\Reader\FilterableDataInterface such as Yiisoft\Data\Reader\Iterable\IterableDataReader. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

105
            /** @scrutinizer ignore-call */ 
106
            $dataReader = $dataReader->withSort($sort->withOrder($order));
Loading history...
Bug introduced by
The method withSort() does not exist on Yiisoft\Data\Reader\DataReaderInterface. It seems like you code against a sub-type of Yiisoft\Data\Reader\DataReaderInterface such as anonymous//tests/Paginat...ysetPaginatorTest.php$0 or Yiisoft\Data\Reader\Iterable\IterableDataReader. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

105
            /** @scrutinizer ignore-call */ 
106
            $dataReader = $dataReader->withSort($sort->withOrder($order));
Loading history...
106
        }
107
108
        // first order field is the field we are paging by
109 22
        $field = null;
110 22
        $sorting = null;
111 22
        foreach ($order as $field => $sorting) {
112 22
            break;
113
        }
114
115 22
        if ($goingToPreviousPage || $goingToNextPage) {
116 7
            $value = $goingToPreviousPage ? $this->firstValue : $this->lastValue;
117
118 7
            $filter = null;
119 7
            $reverseFilter = null;
120 7
            if ($sorting === 'asc') {
121 6
                $filter = new GreaterThan($field, $value);
122 6
                $reverseFilter = new LessThanOrEqual($field, $value);
123
            } else {
124 4
                $filter = new LessThan($field, $value);
125 4
                $reverseFilter = new GreaterThanOrEqual($field, $value);
126
            }
127
128 7
            $dataReader = $dataReader->withFilter($filter);
0 ignored issues
show
Bug introduced by
The method withFilter() does not exist on Yiisoft\Data\Reader\DataReaderInterface. It seems like you code against a sub-type of Yiisoft\Data\Reader\DataReaderInterface such as Yiisoft\Data\Reader\Iterable\IterableDataReader or anonymous//tests/Paginat...ysetPaginatorTest.php$1. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

128
            /** @scrutinizer ignore-call */ 
129
            $dataReader = $dataReader->withFilter($filter);
Loading history...
Bug introduced by
The method withFilter() does not exist on Yiisoft\Data\Reader\SortableDataInterface. It seems like you code against a sub-type of Yiisoft\Data\Reader\SortableDataInterface such as Yiisoft\Data\Reader\Iterable\IterableDataReader. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

128
            /** @scrutinizer ignore-call */ 
129
            $dataReader = $dataReader->withFilter($filter);
Loading history...
129 7
            foreach ($dataReader->withFilter($reverseFilter)->withLimit(1)->read() as $void) {
0 ignored issues
show
Bug introduced by
The method read() does not exist on Yiisoft\Data\Reader\SortableDataInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Data\Reader\SortableDataInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

129
            foreach ($dataReader->withFilter($reverseFilter)->withLimit(1)->/** @scrutinizer ignore-call */ read() as $void) {
Loading history...
Bug introduced by
The method read() does not exist on Yiisoft\Data\Reader\FilterableDataInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Data\Reader\FilterableDataInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

129
            foreach ($dataReader->withFilter($reverseFilter)->withLimit(1)->/** @scrutinizer ignore-call */ read() as $void) {
Loading history...
130 7
                $this->hasPreviousPageItem = true;
131
            }
132
        }
133
134 22
        $data = [];
135 22
        foreach ($dataReader->read() as $item) {
136 19
            if ($this->currentFirstValue === null) {
137 19
                $this->currentFirstValue = $item[$field];
138
            }
139 19
            if (count($data) === $this->pageSize) {
140 9
                $this->hasNextPageItem = true;
141
            } else {
142 19
                $this->currentLastValue = $item[$field];
143 19
                $data[] = $item;
144
            }
145
        }
146
147 22
        if ($goingToPreviousPage) {
148 4
            [$this->currentFirstValue, $this->currentLastValue] = [$this->currentLastValue, $this->currentFirstValue];
149 4
            [$this->hasPreviousPageItem, $this->hasNextPageItem] = [$this->hasNextPageItem, $this->hasPreviousPageItem];
150 4
            $data = array_reverse($data);
151
        }
152
153 22
        return $this->readCache = $data;
154
    }
155
156 4
    public function withPreviousPageToken(?string $value): self
157
    {
158 4
        $new = clone $this;
159 4
        $new->firstValue = $value;
160 4
        $new->lastValue = null;
161 4
        return $new;
162
    }
163
164 7
    public function withNextPageToken(?string $value): self
165
    {
166 7
        $new = clone $this;
167 7
        $new->firstValue = null;
168 7
        $new->lastValue = $value;
169 7
        return $new;
170
    }
171
172 3
    public function getPreviousPageToken(): ?string
173
    {
174 3
        if ($this->isOnFirstPage()) {
175 2
            return null;
176
        }
177 3
        return (string)$this->currentFirstValue;
178
    }
179
180 6
    public function getNextPageToken(): ?string
181
    {
182 6
        if ($this->isOnLastPage()) {
183 1
            return null;
184
        }
185 6
        return (string)$this->currentLastValue;
186
    }
187
188 25
    public function withPageSize(int $pageSize): self
189
    {
190 25
        if ($pageSize < 1) {
191 1
            throw new \InvalidArgumentException('Page size should be at least 1');
192
        }
193
194 24
        $new = clone $this;
195 24
        $new->pageSize = $pageSize;
196 24
        return $new;
197
    }
198
199 21
    public function isOnLastPage(): bool
200
    {
201 21
        $this->initializeInternal();
202 21
        return !$this->hasNextPageItem;
203
    }
204
205 19
    public function isOnFirstPage(): bool
206
    {
207 19
        if ($this->lastValue === null && $this->firstValue === null) {
208
            // Initial state, no values.
209 16
            return true;
210
        }
211 3
        $this->initializeInternal();
212 3
        return !$this->hasPreviousPageItem;
213
    }
214
215
    public function getPageSize(): int
216
    {
217
        return $this->pageSize;
218
    }
219
220 2
    public function getCurrentPageSize(): int
221
    {
222 2
        $this->initializeInternal();
223 2
        return count($this->readCache);
224
    }
225
226 24
    public function __clone()
227
    {
228 24
        $this->readCache = null;
229 24
        $this->hasPreviousPageItem = false;
230 24
        $this->hasNextPageItem = false;
231 24
        $this->currentFirstValue = null;
232 24
        $this->currentLastValue = null;
233
    }
234
235 22
    protected function initializeInternal(): void
236
    {
237 22
        if ($this->readCache !== null) {
238 7
            return;
239
        }
240 17
        $cache = [];
241 17
        foreach ($this->read() as $value) {
242 14
            $cache[] = $value;
243
        }
244 17
        $this->readCache = $cache;
245
    }
246
247
    public function isRequired(): bool
248
    {
249
        return !$this->isOnFirstPage() || !$this->isOnLastPage();
250
    }
251
}
252