Passed
Push — master ( e2fd7e...408742 )
by Sergei
02:55
created

OffsetPaginator   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 215
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 60
dl 0
loc 215
ccs 80
cts 80
cp 1
rs 9.76
c 1
b 0
f 0
wmc 33

20 Methods

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

191
        return $this->dataReader->/** @scrutinizer ignore-call */ count();
Loading history...
192
    }
193
194
    /**
195
     * Get total number of pages in a data reader being paginated.
196
     *
197
     * @return int Total pages number.
198
     */
199 18
    public function getTotalPages(): int
200
    {
201 18
        return (int) ceil($this->getTotalItems() / $this->pageSize);
202
    }
203
204 1
    public function getSort(): ?Sort
205
    {
206 1
        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. ( Ignorable by Annotation )

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

206
        return $this->dataReader instanceof SortableDataInterface ? $this->dataReader->/** @scrutinizer ignore-call */ getSort() : null;
Loading history...
207
    }
208
209
    /**
210
     * @psalm-return Generator<TKey, TValue, mixed, null>
211
     */
212 5
    public function read(): iterable
213
    {
214 5
        if ($this->currentPage > $this->getInternalTotalPages()) {
215 1
            throw new PaginatorException('Page not found.');
216
        }
217
218 4
        yield from $this->dataReader
219 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 Yiisoft\Data\Reader\ReadableDataInterface such as anonymous//tests/Paginat...ysetPaginatorTest.php$3 or anonymous//tests/Paginat...fsetPaginatorTest.php$1 or anonymous//tests/Paginat...ysetPaginatorTest.php$2 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

219
            ->/** @scrutinizer ignore-call */ withLimit($this->pageSize)
Loading history...
220 4
            ->withOffset($this->getOffset())
221 4
            ->read();
222
    }
223
224 2
    public function readOne(): array|object|null
225
    {
226 2
        return $this->dataReader
227 2
            ->withLimit(1)
228 2
            ->withOffset($this->getOffset())
229 2
            ->readOne();
230
    }
231
232 5
    public function isOnFirstPage(): bool
233
    {
234 5
        return $this->currentPage === 1;
235
    }
236
237 5
    public function isOnLastPage(): bool
238
    {
239 5
        if ($this->currentPage > $this->getInternalTotalPages()) {
240 1
            throw new PaginatorException('Page not found.');
241
        }
242
243 4
        return $this->currentPage === $this->getInternalTotalPages();
244
    }
245
246 3
    public function isPaginationRequired(): bool
247
    {
248 3
        return $this->getTotalPages() > 1;
249
    }
250
251 14
    private function getInternalTotalPages(): int
252
    {
253 14
        return max(1, $this->getTotalPages());
254
    }
255
}
256