Passed
Pull Request — master (#24)
by
unknown
02:14
created

KeysetPaginator::getFirst()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

86
        /** @scrutinizer ignore-call */ 
87
        $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

86
        /** @scrutinizer ignore-call */ 
87
        $dataReader = $this->dataReader->withLimit($this->pageSize + 1);
Loading history...
87
88 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\IterableDataReader. ( Ignorable by Annotation )

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

88
        /** @scrutinizer ignore-call */ 
89
        $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 Yiisoft\Data\Reader\IterableDataReader. ( Ignorable by Annotation )

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

88
        /** @scrutinizer ignore-call */ 
89
        $sort = $this->dataReader->getSort();
Loading history...
89 23
        $order = $sort->getOrder();
90
91 23
        if ($order === []) {
92 1
            throw new \RuntimeException('Data should be always sorted in order to work with keyset pagination');
93
        }
94
95 22
        $goingToPreviousPage = $this->firstValue !== null && $this->lastValue === null;
96 22
        $goingToNextPage = $this->firstValue === null && $this->lastValue !== null;
97
98 22
        if ($goingToPreviousPage) {
99
            // reverse sorting
100 4
            foreach ($order as &$sorting) {
101 4
                $sorting = $sorting === 'asc' ? 'desc' : 'asc';
102
            }
103 4
            unset($sorting);
104 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\IterableDataReader. ( Ignorable by Annotation )

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

104
            /** @scrutinizer ignore-call */ 
105
            $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 Yiisoft\Data\Reader\IterableDataReader. ( Ignorable by Annotation )

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

104
            /** @scrutinizer ignore-call */ 
105
            $dataReader = $dataReader->withSort($sort->withOrder($order));
Loading history...
105
        }
106
107
        // first order field is the field we are paging by
108 22
        $field = null;
109 22
        $sorting = null;
110 22
        foreach ($order as $field => $sorting) {
111 22
            break;
112
        }
113
114 22
        if ($goingToPreviousPage || $goingToNextPage) {
115 7
            $value = $goingToPreviousPage ? $this->firstValue : $this->lastValue;
116
117 7
            $filter = null;
118 7
            $reverseFilter = null;
119 7
            if ($sorting === 'asc') {
120 6
                $filter = new GreaterThan($field, $value);
121 6
                $reverseFilter = new LessThanOrEqual($field, $value);
122
            } else {
123 4
                $filter = new LessThan($field, $value);
124 4
                $reverseFilter = new GreaterThanOrEqual($field, $value);
125
            }
126
127 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 anonymous//tests/Paginat...ysetPaginatorTest.php$1 or Yiisoft\Data\Reader\IterableDataReader. ( Ignorable by Annotation )

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

127
            /** @scrutinizer ignore-call */ 
128
            $dataReader = $dataReader->withFilter($filter);
Loading history...
Bug introduced by
The method withFilter() 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

127
            /** @scrutinizer ignore-call */ 
128
            $dataReader = $dataReader->withFilter($filter);
Loading history...
128 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\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

128
            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\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

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