Passed
Push — master ( 91295c...febe3e )
by Sergei
02:41
created

OffsetPaginator::withToken()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
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 PageToken Current page token
44
     */
45
    private PageToken $token;
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 36
    public function __construct(ReadableDataInterface $dataReader)
65
    {
66 36
        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 35
        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 34
        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 33
        $this->dataReader = $dataReader;
88 33
        $this->token = PageToken::next('1');
89
    }
90
91 4
    public function withToken(?PageToken $token): static
92
    {
93 4
        return $this->withCurrentPage($token === null ? 1 : (int)$token->value);
94
    }
95
96 20
    public function withPageSize(int $pageSize): static
97
    {
98 20
        if ($pageSize < 1) {
99 1
            throw new PaginatorException('Page size should be at least 1.');
100
        }
101
102 19
        $new = clone $this;
103 19
        $new->pageSize = $pageSize;
104 19
        return $new;
105
    }
106
107
    /**
108
     * Get a new instance with the given current page number set.
109
     *
110
     * @param int $page Page number.
111
     *
112
     * @throws PaginatorException If current page is set incorrectly.
113
     *
114
     * @return self New instance.
115
     */
116 18
    public function withCurrentPage(int $page): self
117
    {
118 18
        if ($page < 1) {
119 1
            throw new PaginatorException('Current page should be at least 1.');
120
        }
121
122 17
        $new = clone $this;
123 17
        $new->token = PageToken::next((string) $page);
124 17
        return $new;
125
    }
126
127 1
    public function getToken(): PageToken
128
    {
129 1
        return $this->token;
130
    }
131
132 1
    public function getNextToken(): ?PageToken
133
    {
134 1
        return $this->isOnLastPage() ? null : PageToken::next((string) ($this->getCurrentPage() + 1));
135
    }
136
137 1
    public function getPreviousToken(): ?PageToken
138
    {
139 1
        return $this->isOnFirstPage() ? null : PageToken::next((string) ($this->getCurrentPage() - 1));
140
    }
141
142 1
    public function getPageSize(): int
143
    {
144 1
        return $this->pageSize;
145
    }
146
147
    /**
148
     * Get current page number.
149
     *
150
     * @return int Current page number.
151
     */
152 17
    public function getCurrentPage(): int
153
    {
154 17
        return (int) $this->token->value;
155
    }
156
157 6
    public function getCurrentPageSize(): int
158
    {
159 6
        $pages = $this->getInternalTotalPages();
160
161 6
        if ($pages === 1) {
162 3
            return $this->getTotalItems();
163
        }
164
165 3
        $currentPage = $this->getCurrentPage();
166
167 3
        if ($currentPage < $pages) {
168 1
            return $this->pageSize;
169
        }
170
171 2
        if ($currentPage === $pages) {
172 1
            return $this->getTotalItems() - $this->getOffset();
173
        }
174
175 1
        throw new PaginatorException('Page not found.');
176
    }
177
178
    /**
179
     * Get offset for the current page i.e. the number of items to skip before the current page is reached.
180
     *
181
     * @return int Offset.
182
     */
183 8
    public function getOffset(): int
184
    {
185 8
        return $this->pageSize * ($this->getCurrentPage() - 1);
186
    }
187
188
    /**
189
     * Get total number of items in the whole data reader being paginated.
190
     *
191
     * @return int Total items number.
192
     */
193 19
    public function getTotalItems(): int
194
    {
195 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

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

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

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

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