OffsetPaginator::getOffset()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Data\Paginator;
6
7
use Generator;
8
use InvalidArgumentException;
9
use LogicException;
10
use Yiisoft\Data\Reader\CountableDataInterface;
11
use Yiisoft\Data\Reader\FilterableDataInterface;
12
use Yiisoft\Data\Reader\FilterInterface;
13
use Yiisoft\Data\Reader\LimitableDataInterface;
14
use Yiisoft\Data\Reader\OffsetableDataInterface;
15
use Yiisoft\Data\Reader\ReadableDataInterface;
16
use Yiisoft\Data\Reader\Sort;
17
use Yiisoft\Data\Reader\SortableDataInterface;
18
19
use function ceil;
20
use function max;
21
use function sprintf;
22
23
/**
24
 * Offset paginator.
25
 *
26
 * Advantages:
27
 *
28
 * - Total number of pages is available
29
 * - Can get to specific page
30
 * - Data can be unordered
31
 *
32
 * Disadvantages:
33
 *
34
 * - Performance degrades with page number increase
35
 * - Insertions or deletions in the middle of the data are making results inconsistent
36
 *
37
 * @template TKey as array-key
38
 * @template TValue as array|object
39
 *
40
 * @implements PaginatorInterface<TKey, TValue>
41
 */
42
final class OffsetPaginator implements PaginatorInterface
43
{
44
    /**
45
     * @var PageToken Current page token
46
     */
47
    private PageToken $token;
48
49
    /**
50
     * @var int Maximum number of items per page.
51
     * @psalm-var positive-int
52
     */
53
    private int $pageSize = self::DEFAULT_PAGE_SIZE;
54
55
    /**
56
     * Data reader being paginated.
57
     *
58
     * @psalm-var ReadableDataInterface<TKey, TValue>&LimitableDataInterface&OffsetableDataInterface&CountableDataInterface
59
     */
60
    private ReadableDataInterface $dataReader;
61
62
    /**
63
     * @param ReadableDataInterface $dataReader Data reader being paginated.
64
     * @psalm-param ReadableDataInterface<TKey, TValue>&LimitableDataInterface&OffsetableDataInterface&CountableDataInterface $dataReader
65
     * @psalm-suppress DocblockTypeContradiction Needed to allow validating `$dataReader`
66
     */
67 40
    public function __construct(ReadableDataInterface $dataReader)
68
    {
69 40
        if (!$dataReader instanceof OffsetableDataInterface) {
70 1
            throw new InvalidArgumentException(sprintf(
71 1
                'Data reader should implement "%s" in order to be used with offset paginator.',
72 1
                OffsetableDataInterface::class,
73 1
            ));
74
        }
75
76 39
        if (!$dataReader instanceof CountableDataInterface) {
77 1
            throw new InvalidArgumentException(sprintf(
78 1
                'Data reader should implement "%s" in order to be used with offset paginator.',
79 1
                CountableDataInterface::class,
80 1
            ));
81
        }
82
83 38
        if (!$dataReader instanceof LimitableDataInterface) {
84 1
            throw new InvalidArgumentException(sprintf(
85 1
                'Data reader should implement "%s" in order to be used with offset paginator.',
86 1
                LimitableDataInterface::class,
87 1
            ));
88
        }
89
90 37
        $this->dataReader = $dataReader;
91 37
        $this->token = PageToken::next('1');
92
    }
93
94 4
    public function withToken(?PageToken $token): static
95
    {
96 4
        return $this->withCurrentPage($token === null ? 1 : (int)$token->value);
97
    }
98
99 20
    public function withPageSize(int $pageSize): static
100
    {
101 20
        if ($pageSize < 1) {
102 1
            throw new PaginatorException('Page size should be at least 1.');
103
        }
104
105 19
        $new = clone $this;
106 19
        $new->pageSize = $pageSize;
107 19
        return $new;
108
    }
109
110
    /**
111
     * Get a new instance with the given current page number set.
112
     *
113
     * @param int $page Page number.
114
     *
115
     * @throws PaginatorException If current page is set incorrectly.
116
     *
117
     * @return self New instance.
118
     */
119 18
    public function withCurrentPage(int $page): self
120
    {
121 18
        if ($page < 1) {
122 1
            throw new PaginatorException('Current page should be at least 1.');
123
        }
124
125 17
        $new = clone $this;
126 17
        $new->token = PageToken::next((string) $page);
127 17
        return $new;
128
    }
129
130 1
    public function getToken(): PageToken
131
    {
132 1
        return $this->token;
133
    }
134
135 1
    public function getNextToken(): ?PageToken
136
    {
137 1
        return $this->isOnLastPage() ? null : PageToken::next((string) ($this->getCurrentPage() + 1));
138
    }
139
140 1
    public function getPreviousToken(): ?PageToken
141
    {
142 1
        return $this->isOnFirstPage() ? null : PageToken::next((string) ($this->getCurrentPage() - 1));
143
    }
144
145 1
    public function getPageSize(): int
146
    {
147 1
        return $this->pageSize;
148
    }
149
150
    /**
151
     * Get current page number.
152
     *
153
     * @return int Current page number.
154
     */
155 18
    public function getCurrentPage(): int
156
    {
157 18
        return (int) $this->token->value;
158
    }
159
160 6
    public function getCurrentPageSize(): int
161
    {
162 6
        $pages = $this->getInternalTotalPages();
163
164 6
        if ($pages === 1) {
165 3
            return $this->getTotalItems();
166
        }
167
168 3
        $currentPage = $this->getCurrentPage();
169
170 3
        if ($currentPage < $pages) {
171 1
            return $this->pageSize;
172
        }
173
174 2
        if ($currentPage === $pages) {
175
            /** @psalm-var positive-int Because total items is more than offset */
176 1
            return $this->getTotalItems() - $this->getOffset();
177
        }
178
179 1
        return 0;
180
    }
181
182
    /**
183
     * Get offset for the current page i.e. the number of items to skip before the current page is reached.
184
     *
185
     * @return int Offset.
186
     */
187 9
    public function getOffset(): int
188
    {
189 9
        return $this->pageSize * ($this->getCurrentPage() - 1);
190
    }
191
192
    /**
193
     * Get total number of items in the whole data reader being paginated.
194
     *
195
     * @return int Total items number.
196
     *
197
     * @psalm-return non-negative-int
198
     */
199 20
    public function getTotalItems(): int
200
    {
201 20
        return $this->dataReader->count();
0 ignored issues
show
Bug introduced by
The method count() does not exist on Yiisoft\Data\Reader\ReadableDataInterface. It seems like you code against a sub-type of Yiisoft\Data\Reader\ReadableDataInterface such as anonymous//tests/Paginat...fsetPaginatorTest.php$2 or Yiisoft\Data\Tests\Support\StubOffsetData or anonymous//tests/Paginat...fsetPaginatorTest.php$1 or Yiisoft\Data\Reader\DataReaderInterface or anonymous//tests/Paginat...fsetPaginatorTest.php$0. ( Ignorable by Annotation )

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

201
        return $this->dataReader->/** @scrutinizer ignore-call */ count();
Loading history...
202
    }
203
204
    /**
205
     * Get total number of pages in a data reader being paginated.
206
     *
207
     * @return int Total pages number.
208
     */
209 19
    public function getTotalPages(): int
210
    {
211 19
        return (int) ceil($this->getTotalItems() / $this->pageSize);
212
    }
213
214 2
    public function isSortable(): bool
215
    {
216 2
        return $this->dataReader instanceof SortableDataInterface;
217
    }
218
219 2
    public function withSort(?Sort $sort): static
220
    {
221 2
        if (!$this->dataReader instanceof SortableDataInterface) {
222 1
            throw new LogicException('Data reader does not support sorting.');
223
        }
224
225 1
        $new = clone $this;
226 1
        $new->dataReader = $this->dataReader->withSort($sort);
0 ignored issues
show
Bug introduced by
The method withSort() does not exist on Yiisoft\Data\Reader\ReadableDataInterface. It seems like you code against a sub-type of Yiisoft\Data\Reader\ReadableDataInterface such as Yiisoft\Data\Paginator\PaginatorInterface or anonymous//tests/Paginat...ysetPaginatorTest.php$0 or anonymous//tests/Paginat...ysetPaginatorTest.php$3 or Yiisoft\Data\Reader\DataReaderInterface or Yiisoft\Data\Tests\Support\MutationDataReader. ( Ignorable by Annotation )

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

226
        /** @scrutinizer ignore-call */ 
227
        $new->dataReader = $this->dataReader->withSort($sort);
Loading history...
227 1
        return $new;
228
    }
229
230 2
    public function getSort(): ?Sort
231
    {
232 2
        return $this->dataReader instanceof SortableDataInterface ? $this->dataReader->getSort() : null;
0 ignored issues
show
Bug introduced by
The method getSort() does not exist on Yiisoft\Data\Reader\ReadableDataInterface. It seems like you code against a sub-type of Yiisoft\Data\Reader\ReadableDataInterface such as Yiisoft\Data\Paginator\PaginatorInterface or anonymous//tests/Paginat...ysetPaginatorTest.php$0 or anonymous//tests/Paginat...ysetPaginatorTest.php$3 or Yiisoft\Data\Reader\DataReaderInterface or Yiisoft\Data\Tests\Support\MutationDataReader. ( Ignorable by Annotation )

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

232
        return $this->dataReader instanceof SortableDataInterface ? $this->dataReader->/** @scrutinizer ignore-call */ getSort() : null;
Loading history...
233
    }
234
235 2
    public function isFilterable(): bool
236
    {
237 2
        return $this->dataReader instanceof FilterableDataInterface;
238
    }
239
240 2
    public function withFilter(FilterInterface $filter): static
241
    {
242 2
        if (!$this->dataReader instanceof FilterableDataInterface) {
243 1
            throw new LogicException('Data reader does not support filtering.');
244
        }
245
246 1
        $new = clone $this;
247 1
        $new->dataReader = $this->dataReader->withFilter($filter);
0 ignored issues
show
Bug introduced by
The method withFilter() does not exist on Yiisoft\Data\Reader\ReadableDataInterface. It seems like you code against a sub-type of Yiisoft\Data\Reader\ReadableDataInterface such as Yiisoft\Data\Paginator\PaginatorInterface or anonymous//tests/Paginat...ysetPaginatorTest.php$0 or anonymous//tests/Paginat...ysetPaginatorTest.php$2 or Yiisoft\Data\Reader\DataReaderInterface or Yiisoft\Data\Tests\Support\MutationDataReader. ( Ignorable by Annotation )

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

247
        /** @scrutinizer ignore-call */ 
248
        $new->dataReader = $this->dataReader->withFilter($filter);
Loading history...
248 1
        return $new;
249
    }
250
251
    /**
252
     * @psalm-return Generator<TKey, TValue, mixed, null>
253
     */
254 6
    public function read(): iterable
255
    {
256 6
        if ($this->getCurrentPage() > $this->getInternalTotalPages()) {
257 1
            throw new PageNotFoundException();
258
        }
259
260 5
        yield from $this->dataReader
261 5
            ->withLimit($this->pageSize)
0 ignored issues
show
Bug introduced by
The method withLimit() does not exist on Yiisoft\Data\Reader\ReadableDataInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Yiisoft\Data\Paginator\PaginatorInterface or anonymous//tests/Paginat...ysetPaginatorTest.php$0 or anonymous//tests/Paginat...fsetPaginatorTest.php$2 or Yiisoft\Data\Paginator\OffsetPaginator or Yiisoft\Data\Paginator\KeysetPaginator. Are you sure you never get one of those? ( Ignorable by Annotation )

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

261
            ->/** @scrutinizer ignore-call */ withLimit($this->pageSize)
Loading history...
262 5
            ->withOffset($this->getOffset())
263 5
            ->read();
264
    }
265
266 2
    public function readOne(): array|object|null
267
    {
268 2
        return $this->dataReader
269 2
            ->withLimit(1)
270 2
            ->withOffset($this->getOffset())
271 2
            ->readOne();
272
    }
273
274 5
    public function isOnFirstPage(): bool
275
    {
276 5
        return $this->token->value === '1';
277
    }
278
279 5
    public function isOnLastPage(): bool
280
    {
281 5
        if ($this->getCurrentPage() > $this->getInternalTotalPages()) {
282 1
            throw new PageNotFoundException();
283
        }
284
285 4
        return $this->getCurrentPage() === $this->getInternalTotalPages();
286
    }
287
288 3
    public function isPaginationRequired(): bool
289
    {
290 3
        return $this->getTotalPages() > 1;
291
    }
292
293
    /**
294
     * @psalm-return non-negative-int
295
     */
296 15
    private function getInternalTotalPages(): int
297
    {
298 15
        return max(1, $this->getTotalPages());
299
    }
300
}
301