Passed
Push — master ( e14ca2...72264b )
by Eric
01:53
created

Paginator::validateConfig()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 9
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 16
ccs 11
cts 11
cp 1
crap 2
rs 9.9666
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.2
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
 * Esi\Pagination is a fork of AshleyDawson\SimplePagination (https://github.com/AshleyDawson/SimplePagination) which is:
36
 *     Copyright (c) 2015-2019 Ashley Dawson
37
 *
38
 * For a list of changes made in Esi\Pagination in comparison to the original library {@see CHANGELOG.md}.
39
 */
40
41
namespace Esi\Pagination;
42
43
use Closure;
44
use Esi\Pagination\Exception\CallbackNotFoundException;
45
use Esi\Pagination\Exception\InvalidPageNumberException;
46
use Iterator;
47
48
use function array_filter;
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
use const ARRAY_FILTER_USE_BOTH;
57
58
/**
59
 * Main Paginator Class.
60
 *
61
 * @see Tests\PaginatorTest
62
 */
63
class Paginator implements PaginatorInterface
64
{
65
    /**
66
     * A callback to run after the count and slice queries.
67
     */
68
    private ?Closure $afterQueryCallback = null;
69
70
    /**
71
     * A callback to run before the count and slice queries.
72
     */
73
    private ?Closure $beforeQueryCallback = null;
74
75
    /**
76
     * Number of items to include per page.
77
     */
78
    private int $itemsPerPage = 10;
79
    /**
80
     * A callback that is used to determine the total number of items in your collection (returned as an integer).
81
     */
82
    private ?Closure $itemTotalCallback = null;
83
84
    /**
85
     * Number of pages in range.
86
     */
87
    private int $pagesInRange = 5;
88
89
    /**
90
     * A callback to slice your collection given an offset and length argument.
91
     */
92
    private ?Closure $sliceCallback = null;
93
94
    /**
95
     * Constructor - passing optional configuration.
96
     *
97
     * <code>
98
     * $paginator = new Paginator([
99
     *     'itemTotalCallback' => function () {
100
     *         // ...
101
     *     },
102
     *     'sliceCallback' => function (int $offset, int $length) {
103
     *         // ...
104
     *     },
105
     *     'itemsPerPage' => 10,
106
     *     'pagesInRange' => 5,
107
     * ]);
108
     * </code>
109
     *
110
     * @param null|array{}|array{
111
     *     itemTotalCallback: Closure,
112
     *     sliceCallback: Closure,
113
     *     itemsPerPage: int,
114
     *     pagesInRange: int
115
     * } $config
116
     */
117 17
    public function __construct(?array $config = null)
118
    {
119 17
        $config = self::validateConfig($config);
120
121 17
        if ($config === []) {
122 17
            return;
123
        }
124
125 5
        $this->setItemTotalCallback($config['itemTotalCallback']);
126 5
        $this->setSliceCallback($config['sliceCallback']);
127 5
        $this->setItemsPerPage($config['itemsPerPage']);
128 5
        $this->setPagesInRange($config['pagesInRange']);
129
    }
130
131
    /**
132
     * @inheritDoc
133
     */
134 1
    #[\Override]
135
    public function getAfterQueryCallback(): ?Closure
136
    {
137 1
        return $this->afterQueryCallback;
138
    }
139
140
    /**
141
     * @inheritDoc
142
     */
143 1
    #[\Override]
144
    public function getBeforeQueryCallback(): ?Closure
145
    {
146 1
        return $this->beforeQueryCallback;
147
    }
148
149
    /**
150
     * @inheritDoc
151
     */
152 2
    #[\Override]
153
    public function getItemsPerPage(): int
154
    {
155 2
        return $this->itemsPerPage;
156
    }
157
158
    /**
159
     * @inheritDoc
160
     */
161 2
    #[\Override]
162
    public function getItemTotalCallback(): ?Closure
163
    {
164 2
        return $this->itemTotalCallback;
165
    }
166
167
    /**
168
     * @inheritDoc
169
     */
170 2
    #[\Override]
171
    public function getPagesInRange(): int
172
    {
173 2
        return $this->pagesInRange;
174
    }
175
176
    /**
177
     * @inheritDoc
178
     */
179 2
    #[\Override]
180
    public function getSliceCallback(): ?Closure
181
    {
182 2
        return $this->sliceCallback;
183
    }
184
185
    /**
186
     * @inheritDoc
187
     */
188 12
    #[\Override]
189
    public function paginate(int $currentPageNumber = 1): Pagination
190
    {
191 12
        if ($this->itemTotalCallback === null) {
192 2
            throw new CallbackNotFoundException(
193 2
                'Item total callback not found, set it using Paginator::setItemTotalCallback()'
194 2
            );
195
        }
196
197 10
        if ($this->sliceCallback === null) {
198 1
            throw new CallbackNotFoundException(
199 1
                'Slice callback not found, set it using Paginator::setSliceCallback()'
200 1
            );
201
        }
202
203 9
        if ($currentPageNumber <= 0) {
204 1
            throw new InvalidPageNumberException(
205 1
                sprintf('Current page number must have a value of 1 or more, %s given', $currentPageNumber)
206 1
            );
207
        }
208
209 8
        $sliceCallback       = $this->sliceCallback;
210 8
        $itemTotalCallback   = $this->itemTotalCallback;
211 8
        $beforeQueryCallback = $this->prepareBeforeQueryCallback();
212 8
        $afterQueryCallback  = $this->prepareAfterQueryCallback();
213
214 8
        $pagination = new Pagination();
215
216 8
        $beforeQueryCallback($this, $pagination);
217 8
        $totalNumberOfItems = (int) $itemTotalCallback($pagination);
218 8
        $afterQueryCallback($this, $pagination);
219
220 8
        $numberOfPages = (int) ceil($totalNumberOfItems / $this->itemsPerPage);
221 8
        $pagesInRange  = min($this->pagesInRange, $numberOfPages);
222 8
        $pages         = self::determinePageRange($currentPageNumber, $pagesInRange, $numberOfPages);
223 8
        $offset        = ($currentPageNumber - 1) * $this->itemsPerPage;
224
225 8
        $beforeQueryCallback($this, $pagination);
226
227 8
        if (-1 === $this->itemsPerPage) {
228 1
            $items = $sliceCallback(0, 999_999_999, $pagination);
1 ignored issue
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_STRING, expecting ',' or ')' on line 228 at column 42
Loading history...
229
        } else {
230 7
            $items = $sliceCallback($offset, $this->itemsPerPage, $pagination);
231
        }
232
233 8
        if ($items instanceof Iterator) {
234 1
            $items = iterator_to_array($items);
235
        }
236
237 8
        $afterQueryCallback($this, $pagination);
238
239 8
        $previousPageNumber = self::determinePreviousPageNumber($currentPageNumber);
240 8
        $nextPageNumber     = self::determineNextPageNumber($currentPageNumber, $numberOfPages);
241
242
        /** @var non-empty-array<int> $pages * */
243 8
        $pagination
244 8
            ->setItems($items)
245 8
            ->setPages($pages)
246 8
            ->setTotalNumberOfPages($numberOfPages)
247 8
            ->setCurrentPageNumber($currentPageNumber)
248 8
            ->setFirstPageNumber(1)
249 8
            ->setLastPageNumber($numberOfPages)
250 8
            ->setPreviousPageNumber($previousPageNumber)
251 8
            ->setNextPageNumber($nextPageNumber)
252 8
            ->setItemsPerPage($this->itemsPerPage)
253 8
            ->setTotalNumberOfItems($totalNumberOfItems)
254 8
            ->setFirstPageNumberInRange(min($pages))
255 8
            ->setLastPageNumberInRange(max($pages))
256 8
        ;
257
258 8
        return $pagination;
259
    }
260
261
    /**
262
     * @inheritDoc
263
     */
264 2
    #[\Override]
265
    public function setAfterQueryCallback(?Closure $afterQueryCallback): static
266
    {
267 2
        $this->afterQueryCallback = $afterQueryCallback;
268
269 2
        return $this;
270
    }
271
272
    /**
273
     * @inheritDoc
274
     */
275 2
    #[\Override]
276
    public function setBeforeQueryCallback(?Closure $beforeQueryCallback): static
277
    {
278 2
        $this->beforeQueryCallback = $beforeQueryCallback;
279
280 2
        return $this;
281
    }
282
283
    /**
284
     * @inheritDoc
285
     */
286 12
    #[\Override]
287
    public function setItemsPerPage(int $itemsPerPage): static
288
    {
289 12
        $this->itemsPerPage = $itemsPerPage;
290
291 12
        return $this;
292
    }
293
294
    /**
295
     * @inheritDoc
296
     */
297 12
    #[\Override]
298
    public function setItemTotalCallback(?Closure $itemTotalCallback): static
299
    {
300 12
        $this->itemTotalCallback = $itemTotalCallback;
301
302 12
        return $this;
303
    }
304
305
    /**
306
     * @inheritDoc
307
     */
308 12
    #[\Override]
309
    public function setPagesInRange(int $pagesInRange): static
310
    {
311 12
        $this->pagesInRange = $pagesInRange;
312
313 12
        return $this;
314
    }
315
316
    /**
317
     * @inheritDoc
318
     */
319 12
    #[\Override]
320
    public function setSliceCallback(?Closure $sliceCallback): static
321
    {
322 12
        $this->sliceCallback = $sliceCallback;
323
324 12
        return $this;
325
    }
326
327
    /**
328
     * A helper function to {@see self::paginate()}.
329
     *
330
     * Ensures the afterQueryCallback is a valid Closure. If the currently set
331
     * afterQueryCallback is null, it will return an empty Closure object.
332
     */
333 8
    protected function prepareAfterQueryCallback(): Closure
334
    {
335 8
        if ($this->afterQueryCallback instanceof Closure) {
336 1
            return $this->afterQueryCallback;
337
        }
338
339 7
        return static function (): void {};
340
    }
341
342
    /**
343
     * A helper function to {@see self::paginate()}.
344
     *
345
     * Ensures the beforeQueryCallback is a valid Closure. If the currently set
346
     * beforeQueryCallback is null, it will return an empty Closure object.
347
     */
348 8
    protected function prepareBeforeQueryCallback(): Closure
349
    {
350 8
        if ($this->beforeQueryCallback instanceof Closure) {
351 1
            return $this->beforeQueryCallback;
352
        }
353
354 7
        return static function (): void {};
355
    }
356
357
    /**
358
     * A helper function to {@see self::paginate()}.
359
     *
360
     * Determines the next page number based on the current page number.
361
     */
362 8
    protected static function determineNextPageNumber(int $currentPageNumber, int $numberOfPages): ?int
363
    {
364 8
        if (($currentPageNumber + 1) <= $numberOfPages) {
365 7
            return $currentPageNumber + 1;
366
        }
367
368 5
        return null;
369
    }
370
371
    /**
372
     * A helper function to {@see self::paginate()}.
373
     *
374
     * Determines the number of pages in range given the current page number, currently
375
     * set pages in range, and total number of pages.
376
     *
377
     * @return array<int>
378
     */
379 8
    protected static function determinePageRange(int $currentPageNumber, int $pagesInRange, int $numberOfPages): array
380
    {
381 8
        $change = (int) ceil($pagesInRange / 2);
382
383 8
        if (($currentPageNumber - $change) > ($numberOfPages - $pagesInRange)) {
384 5
            $pages = range(($numberOfPages - $pagesInRange) + 1, $numberOfPages);
385
        } else {
386 7
            if (($currentPageNumber - $change) < 0) {
387 5
                $change = $currentPageNumber;
388
            }
389
390 7
            $offset = $currentPageNumber - $change;
391 7
            $pages  = range(($offset + 1), $offset + $pagesInRange);
392
        }
393
394 8
        return $pages;
395
    }
396
397
    /**
398
     * A helper function to {@see self::paginate()}.
399
     *
400
     * Determines the previous page number based on the current page number.
401
     */
402 8
    protected static function determinePreviousPageNumber(int $currentPageNumber): ?int
403
    {
404 8
        if (($currentPageNumber - 1) > 0) {
405 5
            return $currentPageNumber - 1;
406
        }
407
408 8
        return null;
409
    }
410
411
    /**
412
     * Helper function for __construct() to validate the passed $config.
413
     *
414
     * @param null|array{}|array{
415
     *     itemTotalCallback: Closure,
416
     *     sliceCallback: Closure,
417
     *     itemsPerPage: int,
418
     *     pagesInRange: int
419
     * } $config Expected array signature.
420
     *
421
     * @return array{}|array{
422
     *     itemTotalCallback: Closure,
423
     *     sliceCallback: Closure,
424
     *     itemsPerPage: int,
425
     *     pagesInRange: int
426
     * }
427
     */
428 17
    protected static function validateConfig(?array $config = null): array
429
    {
430 17
        static $validKeys = ['itemTotalCallback', 'sliceCallback', 'itemsPerPage', 'pagesInRange'];
431
432 17
        $config ??= [];
433
434 17
        return array_filter($config, static function (mixed $value, string $key) use ($validKeys): bool {
435 6
            if (!\in_array($key, $validKeys, true)) {
436 1
                return false;
437
            }
438
439 6
            return match($key) {
440 6
                'itemTotalCallback', 'sliceCallback' => $value instanceof Closure,
441 6
                default => \is_int($value)
442 6
            };
443 17
        }, ARRAY_FILTER_USE_BOTH);
444
445
    }
446
}
447