Passed
Push — master ( 6c8383...fe7e74 )
by Alexander
04:38 queued 02:11
created

OffsetPaginator   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 200
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 52
c 1
b 0
f 0
dl 0
loc 200
ccs 70
cts 70
cp 1
rs 9.92
wmc 31

19 Methods

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

183
        return $this->dataReader->/** @scrutinizer ignore-call */ count();
Loading history...
184
    }
185
186
    /**
187
     * Get total number of pages in a data reader being paginated.
188
     *
189
     * @return int Total pages number.
190
     */
191 18
    public function getTotalPages(): int
192
    {
193 18
        return (int) ceil($this->getTotalItems() / $this->pageSize);
194
    }
195
196 1
    public function getSort(): ?Sort
197
    {
198 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 anonymous//tests/Paginat...ysetPaginatorTest.php$2 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

198
        return $this->dataReader instanceof SortableDataInterface ? $this->dataReader->/** @scrutinizer ignore-call */ getSort() : null;
Loading history...
199
    }
200
201
    /**
202
     * @psalm-return Generator<TKey, TValue, mixed, null>
203
     */
204 5
    public function read(): iterable
205
    {
206 5
        if ($this->currentPage > $this->getInternalTotalPages()) {
207 1
            throw new PaginatorException('Page not found.');
208
        }
209
210 4
        yield from $this->dataReader
211 4
            ->withLimit($this->pageSize)
212 4
            ->withOffset($this->getOffset())
0 ignored issues
show
Bug introduced by
The method withOffset() 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$1 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

212
            ->/** @scrutinizer ignore-call */ withOffset($this->getOffset())
Loading history...
213 4
            ->read();
214
    }
215
216 5
    public function isOnFirstPage(): bool
217
    {
218 5
        return $this->currentPage === 1;
219
    }
220
221 5
    public function isOnLastPage(): bool
222
    {
223 5
        if ($this->currentPage > $this->getInternalTotalPages()) {
224 1
            throw new PaginatorException('Page not found.');
225
        }
226
227 4
        return $this->currentPage === $this->getInternalTotalPages();
228
    }
229
230 3
    public function isRequired(): bool
231
    {
232 3
        return $this->getTotalPages() > 1;
233
    }
234
235 14
    private function getInternalTotalPages(): int
236
    {
237 14
        return max(1, $this->getTotalPages());
238
    }
239
}
240