Passed
Push — master ( bef4fd...91295c )
by Sergei
02:35
created

OffsetPaginator::withSort()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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

192
        return $this->dataReader->/** @scrutinizer ignore-call */ count();
Loading history...
193
    }
194
195
    /**
196
     * Get total number of pages in a data reader being paginated.
197
     *
198
     * @return int Total pages number.
199
     */
200 18
    public function getTotalPages(): int
201
    {
202 18
        return (int) ceil($this->getTotalItems() / $this->pageSize);
203
    }
204
205 2
    public function isSortable(): bool
206
    {
207 2
        return $this->dataReader instanceof SortableDataInterface;
208
    }
209
210 2
    public function withSort(?Sort $sort): static
211
    {
212 2
        if (!$this->dataReader instanceof SortableDataInterface) {
213 1
            throw new LogicException('Data reader does not support sorting.');
214
        }
215
216 1
        $new = clone $this;
217 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

217
        /** @scrutinizer ignore-call */ 
218
        $new->dataReader = $this->dataReader->withSort($sort);
Loading history...
218 1
        return $new;
219
    }
220
221 2
    public function getSort(): ?Sort
222
    {
223 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

223
        return $this->dataReader instanceof SortableDataInterface ? $this->dataReader->/** @scrutinizer ignore-call */ getSort() : null;
Loading history...
224
    }
225
226
    /**
227
     * @psalm-return Generator<TKey, TValue, mixed, null>
228
     */
229 5
    public function read(): iterable
230
    {
231 5
        if ($this->currentPage > $this->getInternalTotalPages()) {
232 1
            throw new PaginatorException('Page not found.');
233
        }
234
235 4
        yield from $this->dataReader
236 4
            ->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

236
            ->/** @scrutinizer ignore-call */ withLimit($this->pageSize)
Loading history...
237 4
            ->withOffset($this->getOffset())
238 4
            ->read();
239
    }
240
241 2
    public function readOne(): array|object|null
242
    {
243 2
        return $this->dataReader
244 2
            ->withLimit(1)
245 2
            ->withOffset($this->getOffset())
246 2
            ->readOne();
247
    }
248
249 5
    public function isOnFirstPage(): bool
250
    {
251 5
        return $this->currentPage === 1;
252
    }
253
254 5
    public function isOnLastPage(): bool
255
    {
256 5
        if ($this->currentPage > $this->getInternalTotalPages()) {
257 1
            throw new PaginatorException('Page not found.');
258
        }
259
260 4
        return $this->currentPage === $this->getInternalTotalPages();
261
    }
262
263 3
    public function isPaginationRequired(): bool
264
    {
265 3
        return $this->getTotalPages() > 1;
266
    }
267
268 14
    private function getInternalTotalPages(): int
269
    {
270 14
        return max(1, $this->getTotalPages());
271
    }
272
}
273