Passed
Push — master ( b45d6c...0b4471 )
by Eric
01:56
created

Paginator::prepareBeforeQueryCallback()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Pagination - Simple, lightweight and universal service that implements pagination on collections of things.
7
 *
8
 * @author    Eric Sizemore <[email protected]>
9
 * @version   2.0.0
10
 * @copyright (C) 2024 Eric Sizemore
11
 * @license   The MIT License (MIT)
12
 *
13
 * Copyright (C) 2024 Eric Sizemore<https://www.secondversion.com/>.
14
 * Copyright (c) 2015-2019 Ashley Dawson<[email protected]>
15
 *
16
 * Permission is hereby granted, free of charge, to any person obtaining a copy
17
 * of this software and associated documentation files (the "Software"), to
18
 * deal in the Software without restriction, including without limitation the
19
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
20
 * sell copies of the Software, and to permit persons to whom the Software is
21
 * furnished to do so, subject to the following conditions:
22
 *
23
 * The above copyright notice and this permission notice shall be included in
24
 * all copies or substantial portions of the Software.
25
 *
26
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
32
 * THE SOFTWARE.
33
 */
34
35
/**
36
 * Esi\Pagination is a fork of AshleyDawson\SimplePagination (https://github.com/AshleyDawson/SimplePagination) which is:
37
 *     Copyright (c) 2015-2019 Ashley Dawson
38
 *
39
 * For a list of changes made in Esi\Pagination in comparison to the original library {@see CHANGELOG.md}.
40
 */
41
42
namespace Esi\Pagination;
43
44
use Closure;
45
use Esi\Pagination\Exception\CallbackNotFoundException;
46
use Esi\Pagination\Exception\InvalidPageNumberException;
47
use Iterator;
48
49
use function ceil;
50
use function iterator_to_array;
51
use function max;
52
use function min;
53
use function range;
54
use function sprintf;
55
56
/**
57
 * Class Paginator
58
 * @see \Esi\Pagination\Tests\PaginatorTest
59
 */
60
class Paginator implements PaginatorInterface
61
{
62
    /**
63
     * A callback that is used to determine the total number of items in your collection (returned as an integer).
64
     */
65
    private ?Closure $itemTotalCallback   = null;
66
67
    /**
68
     * A callback to slice your collection given an offset and length argument.
69
     */
70
    private ?Closure $sliceCallback       = null;
71
72
    /**
73
     * A callback to run before the count and slice queries.
74
     */
75
    private ?Closure $beforeQueryCallback = null;
76
77
    /**
78
     * A callback to run after the count and slice queries.
79
     */
80
    private ?Closure $afterQueryCallback  = null;
81
82
    /**
83
     * Number of items to include per page.
84
     */
85
    private int $itemsPerPage = 10;
86
87
    /**
88
     */
89
    private int $pagesInRange = 5;
90
91
    /**
92
     * Constructor - passing optional configuration
93
     *
94
     * <code>
95
     * $paginator = new Paginator(array(
96
     *     'itemTotalCallback' => function () {
97
     *         // ...
98
     *     },
99
     *     'sliceCallback' => function (int $offset, int $length) {
100
     *         // ...
101
     *     },
102
     *     'itemsPerPage' => 10,
103
     *     'pagesInRange' => 5
104
     * ));
105
     * </code>
106
     *
107
     * @param null|array{}|array{
108
     *     itemTotalCallback: Closure,
109
     *     sliceCallback: Closure,
110
     *     itemsPerPage: int,
111
     *     pagesInRange: int
112
     * } $config
113
     */
114 16
    public function __construct(?array $config = null)
115
    {
116 16
        if ($config !== null && $config !== []) {
117 5
            $this->setItemTotalCallback($config['itemTotalCallback']);
118 5
            $this->setSliceCallback($config['sliceCallback']);
119 5
            $this->setItemsPerPage($config['itemsPerPage']);
120 5
            $this->setPagesInRange($config['pagesInRange']);
121
        }
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 12
    public function paginate(int $currentPageNumber = 1): Pagination
128
    {
129 12
        if ($this->itemTotalCallback === null) {
130 2
            throw new CallbackNotFoundException(
131 2
                'Item total callback not found, set it using Paginator::setItemTotalCallback()'
132 2
            );
133
        }
134
135 10
        if ($this->sliceCallback === null) {
136 1
            throw new CallbackNotFoundException(
137 1
                'Slice callback not found, set it using Paginator::setSliceCallback()'
138 1
            );
139
        }
140
141 9
        if ($currentPageNumber <= 0) {
142 1
            throw new InvalidPageNumberException(
143 1
                sprintf('Current page number must have a value of 1 or more, %s given', $currentPageNumber)
144 1
            );
145
        }
146
147 8
        $sliceCallback       = $this->sliceCallback;
148 8
        $itemTotalCallback   = $this->itemTotalCallback;
149
        /** @var Closure $beforeQueryCallback */
150 8
        $beforeQueryCallback = $this->prepareBeforeQueryCallback();
151
        /** @var Closure $afterQueryCallback  */
152 8
        $afterQueryCallback  = $this->prepareAfterQueryCallback();
153
154 8
        $pagination = new Pagination();
155
156 8
        $beforeQueryCallback($this, $pagination);
157 8
        $totalNumberOfItems = (int) $itemTotalCallback($pagination);
158 8
        $afterQueryCallback($this, $pagination);
159
160 8
        $numberOfPages = (int) ceil($totalNumberOfItems / $this->itemsPerPage);
161 8
        $pagesInRange  = $this->pagesInRange > $numberOfPages ? $numberOfPages : $this->pagesInRange;
162 8
        $pages         = $this->determinePageRange($currentPageNumber, $pagesInRange, $numberOfPages);
163 8
        $offset        = ($currentPageNumber - 1) * $this->itemsPerPage;
164
165 8
        $beforeQueryCallback($this, $pagination);
166
167 8
        if (-1 === $this->itemsPerPage) {
168 1
            $items = $sliceCallback(0, 999999999, $pagination);
169
        } else {
170 7
            $items = $sliceCallback($offset, $this->itemsPerPage, $pagination);
171
        }
172
173 8
        if ($items instanceof Iterator) {
174 1
            $items = iterator_to_array($items);
175
        }
176
177 8
        $afterQueryCallback($this, $pagination);
178
179 8
        $previousPageNumber = $this->determinePreviousPageNumber($currentPageNumber);
180 8
        $nextPageNumber     = $this->determineNextPageNumber($currentPageNumber, $numberOfPages);
181
182
        /** @var non-empty-array<int> $pages **/
183 8
        $pagination
184 8
            ->setItems($items)
185 8
            ->setPages($pages)
186 8
            ->setTotalNumberOfPages($numberOfPages)
187 8
            ->setCurrentPageNumber($currentPageNumber)
188 8
            ->setFirstPageNumber(1)
189 8
            ->setLastPageNumber($numberOfPages)
190 8
            ->setPreviousPageNumber($previousPageNumber)
191 8
            ->setNextPageNumber($nextPageNumber)
192 8
            ->setItemsPerPage($this->itemsPerPage)
193 8
            ->setTotalNumberOfItems($totalNumberOfItems)
194 8
            ->setFirstPageNumberInRange(min($pages))
195 8
            ->setLastPageNumberInRange(max($pages))
196 8
        ;
197
198 8
        return $pagination;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204 1
    public function getSliceCallback(): ?Closure
205
    {
206 1
        return $this->sliceCallback;
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212 12
    public function setSliceCallback(?Closure $sliceCallback): static
213
    {
214 12
        $this->sliceCallback = $sliceCallback;
215
216 12
        return $this;
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222 1
    public function getItemTotalCallback(): ?Closure
223
    {
224 1
        return $this->itemTotalCallback;
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230 1
    public function getBeforeQueryCallback(): ?Closure
231
    {
232 1
        return $this->beforeQueryCallback;
233
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238 2
    public function setBeforeQueryCallback(?Closure $beforeQueryCallback): static
239
    {
240 2
        $this->beforeQueryCallback = $beforeQueryCallback;
241
242 2
        return $this;
243
    }
244
245
    /**
246
     * {@inheritdoc}
247
     */
248 1
    public function getAfterQueryCallback(): ?Closure
249
    {
250 1
        return $this->afterQueryCallback;
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     */
256 2
    public function setAfterQueryCallback(?Closure $afterQueryCallback): static
257
    {
258 2
        $this->afterQueryCallback = $afterQueryCallback;
259
260 2
        return $this;
261
    }
262
263
    /**
264
     * {@inheritdoc}
265
     */
266 12
    public function setItemTotalCallback(?Closure $itemTotalCallback): static
267
    {
268 12
        $this->itemTotalCallback = $itemTotalCallback;
269
270 12
        return $this;
271
    }
272
273
    /**
274
     * {@inheritdoc}
275
     */
276 1
    public function getItemsPerPage(): int
277
    {
278 1
        return $this->itemsPerPage;
279
    }
280
281
    /**
282
     * {@inheritdoc}
283
     */
284 12
    public function setItemsPerPage(int $itemsPerPage): static
285
    {
286 12
        $this->itemsPerPage = $itemsPerPage;
287
288 12
        return $this;
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     */
294 1
    public function getPagesInRange(): int
295
    {
296 1
        return $this->pagesInRange;
297
    }
298
299
    /**
300
     * {@inheritdoc}
301
     */
302 12
    public function setPagesInRange(int $pagesInRange): static
303
    {
304 12
        $this->pagesInRange = $pagesInRange;
305
306 12
        return $this;
307
    }
308
309
    // Helper functions to the main paginate() function.
310
311
    /**
312
     */
313 8
    protected function prepareBeforeQueryCallback(): Closure
314
    {
315 8
        if ($this->beforeQueryCallback instanceof Closure) {
316 1
            return $this->beforeQueryCallback;
317
        }
318
319 7
        return static function (): void {};
320
    }
321
322
    /**
323
     */
324 8
    protected function prepareAfterQueryCallback(): Closure
325
    {
326 8
        if ($this->afterQueryCallback instanceof Closure) {
327 1
            return $this->afterQueryCallback;
328
        }
329
330 7
        return static function (): void {};
331
    }
332
333
    /**
334
     * @return array<int>
335
     */
336 8
    protected function determinePageRange(int $currentPageNumber, int $pagesInRange, int $numberOfPages): array
337
    {
338 8
        $change = (int) ceil($pagesInRange / 2);
339
340 8
        if (($currentPageNumber - $change) > ($numberOfPages - $pagesInRange)) {
341 5
            $pages = range(($numberOfPages - $pagesInRange) + 1, $numberOfPages);
342
        } else {
343 7
            if (($currentPageNumber - $change) < 0) {
344 5
                $change = $currentPageNumber;
345
            }
346
347 7
            $offset = $currentPageNumber - $change;
348 7
            $pages  = range(($offset + 1), $offset + $pagesInRange);
349
        }
350
351 8
        return $pages;
352
    }
353
354
    /**
355
     */
356 8
    protected function determinePreviousPageNumber(int $currentPageNumber): ?int
357
    {
358 8
        $previousPageNumber = null;
359
360 8
        if (($currentPageNumber - 1) > 0) {
361 5
            $previousPageNumber = $currentPageNumber - 1;
362
        }
363
364 8
        return $previousPageNumber;
365
    }
366
367
    /**
368
     */
369 8
    protected function determineNextPageNumber(int $currentPageNumber, int $numberOfPages): ?int
370
    {
371 8
        $nextPageNumber = null;
372
373 8
        if (($currentPageNumber + 1) <= $numberOfPages) {
374 7
            $nextPageNumber = $currentPageNumber + 1;
375
        }
376
377 8
        return $nextPageNumber;
378
    }
379
}
380