Paginator::renderLinks()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 0
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
The MIT License (MIT)
5
6
Copyright (c) 2014 Jason Grimes
7
8
Permission is hereby granted, free of charge, to any person obtaining a copy
9
of this software and associated documentation files (the "Software"), to deal
10
in the Software without restriction, including without limitation the rights
11
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
copies of the Software, and to permit persons to whom the Software is
13
furnished to do so, subject to the following conditions:
14
15
The above copyright notice and this permission notice shall be included in all
16
copies or substantial portions of the Software.
17
18
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
SOFTWARE.
25
*/
26
27
/**
28
 * Paginator.php - Jaxon Paginator
29
 *
30
 * Create pagination links from a Jaxon request and a data array.
31
 *
32
 * @package jaxon-core
33
 * @author Jason Grimes
34
 * @author Thierry Feuzeu
35
 * @copyright 2014 Jason Grimes
36
 * @copyright 2016 Thierry Feuzeu
37
 * @license https://opensource.org/licenses/MIT MIT License
38
 * @link https://github.com/jaxon-php/jaxon-core
39
 */
40
41
namespace Jaxon\App\Pagination;
42
43
use Jaxon\App\Pagination\Page;
44
use Jaxon\Response\NodeResponse;
45
use Jaxon\Response\Response;
46
use Jaxon\Plugin\Response\Pagination\PaginatorPlugin;
47
use Jaxon\Script\JsExpr;
48
use Closure;
49
50
use function array_pop;
51
use function array_map;
52
use function array_shift;
53
use function ceil;
54
use function count;
55
use function is_a;
56
use function max;
57
use function range;
58
use function trim;
59
60
class Paginator
61
{
62
    /**
63
     * @var integer
64
     */
65
    protected $nPagesCount = 0;
66
67
    /**
68
     * @var integer
69
     */
70
    protected $nMaxPages = 9;
71
72
    /**
73
     * @var string
74
     */
75
    protected $sPreviousText = '&laquo;';
76
77
    /**
78
     * @var string
79
     */
80
    protected $sNextText = '&raquo;';
81
82
    /**
83
     * @var string
84
     */
85
    protected $sEllipsysText = '...';
86
87
    /**
88
     * The constructor.
89
     *
90
     * @param PaginatorPlugin $xPlugin
91
     * @param int $nPageNumber     The current page number
92
     * @param int $nItemsPerPage    The number of items per page
93
     * @param int $nItemsCount      The total number of items
94
     */
95
    public function __construct(private PaginatorPlugin $xPlugin, protected int $nPageNumber,
96
        protected int $nItemsPerPage, protected int $nItemsCount)
97
    {
98
        $this->updatePagesCount();
99
    }
100
101
    /**
102
     * Update the number of pages
103
     *
104
     * @return Paginator
105
     */
106
    private function updatePagesCount(): Paginator
107
    {
108
        $this->nItemsPerPage = $this->nItemsPerPage > 0 ? $this->nItemsPerPage : 0;
109
        // $this->nItemsCount = $this->nItemsCount > 0 ? $this->nItemsCount : 0;
110
        $this->nPageNumber = $this->nPageNumber < 1 ? 1 : $this->nPageNumber;
111
112
        if($this->nItemsCount >= 0)
113
        {
114
            $this->nPagesCount = ($this->nItemsPerPage === 0 ? 0 :
115
                (int)ceil($this->nItemsCount / $this->nItemsPerPage));
116
            if($this->nPageNumber > $this->nPagesCount)
117
            {
118
                $this->nPageNumber = $this->nPagesCount;
119
            }
120
        }
121
        return $this;
122
    }
123
124
    /**
125
     * Set the text for the previous page link
126
     *
127
     * @param string $sText    The text for the previous page link
128
     *
129
     * @return Paginator
130
     */
131
    public function setPreviousText(string $sText): Paginator
132
    {
133
        $this->sPreviousText = $sText;
134
        return $this;
135
    }
136
137
    /**
138
     * Set the text for the next page link
139
     *
140
     * @param string $sText    The text for the previous page link
141
     *
142
     * @return Paginator
143
     */
144
    public function setNextText(string $sText): Paginator
145
    {
146
        $this->sNextText = $sText;
147
        return $this;
148
    }
149
150
    /**
151
     * Set the max number of pages to show
152
     *
153
     * @param int $nMaxPages    The max number of pages to show
154
     *
155
     * @return Paginator
156
     */
157
    public function setMaxPages(int $nMaxPages): Paginator
158
    {
159
        // Make sure the max number of pages is odd and greater than 5.
160
        $this->nMaxPages = max((int)(($nMaxPages - 1) / 2) * 2 + 1, 5);
161
        return $this;
162
    }
163
164
    /**
165
     * Get the previous page data.
166
     *
167
     * @return Page
168
     */
169
    protected function getPrevPage(): Page
170
    {
171
        return $this->nPageNumber <= 1 ?
172
            new Page('disabled', $this->sPreviousText, 0) :
173
            new Page('enabled', $this->sPreviousText, $this->nPageNumber - 1);
174
    }
175
176
    /**
177
     * Get the next page data.
178
     *
179
     * @return Page
180
     */
181
    protected function getNextPage(): Page
182
    {
183
        // The next page link is always active when the total number of items is not privided.
184
        return $this->nItemsCount >= 0 && $this->nPageNumber >= $this->nPagesCount ?
185
            new Page('disabled', $this->sNextText, 0) :
186
            new Page('enabled', $this->sNextText, $this->nPageNumber + 1);
187
    }
188
189
    /**
190
     * Get a page data.
191
     *
192
     * @param integer $nNumber    The page number
193
     *
194
     * @return Page
195
     */
196
    protected function getPage(int $nNumber): Page
197
    {
198
        if($nNumber < 1)
199
        {
200
            return new Page('disabled', $this->sEllipsysText, 0);
201
        }
202
        $sType = ($nNumber === $this->nPageNumber ? 'current' : 'enabled');
203
        return new Page($sType, "$nNumber", $nNumber);
204
    }
205
206
    /**
207
     * Get the array of page numbers to be printed.
208
     *
209
     * Example: [1, 2, 3, 4, 5, 6, 7]
210
     *
211
     * @return array
212
     */
213
    protected function getAllPageNumbers(): array
214
    {
215
        return range(1, $this->nPagesCount);
216
    }
217
218
    /**
219
     * Get the array of page numbers to be printed, when the total number of items is not provided.
220
     *
221
     * Example: [1, 0, 4, 5, 6, 0, 10]
222
     *
223
     * @return array
224
     */
225
    protected function getPageNumbersWithoutTotal(): array
226
    {
227
        $aPageNumbers = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $aPageNumbers is dead and can be removed.
Loading history...
228
229
        // Determine the sliding range, centered around the current page.
230
        $nNumAdjacents = ($this->nMaxPages - 1) / 2;
231
232
        $nSlidingStart = 1;
233
        $nSlidingStartThreshold = $nNumAdjacents;
234
        $nSlidingEnd = $this->nPageNumber + $nNumAdjacents - 1;
235
236
        if($this->nPageNumber > $nNumAdjacents + 1)
237
        {
238
            $nSlidingStart = $this->nPageNumber - $nNumAdjacents + 2;
239
        }
240
        if($this->nPageNumber <= $nSlidingStartThreshold)
241
        {
242
            $nSlidingEnd += $nSlidingStartThreshold - $this->nPageNumber + 1;
243
        }
244
245
        // Build the list of page numbers. Pages with 0 as number are ellipsys.
246
        $aStartPages = $nSlidingStart > 1 ? [1, 0] : [];
247
        $aPageNumbers = range($nSlidingStart, $nSlidingEnd);
248
        // Ellipsys are always added at the end of the list.
249
        return [...$aStartPages, ...$aPageNumbers, 0];
250
    }
251
252
    /**
253
     * Get the array of page numbers to be printed, when the total number of items is provided.
254
     *
255
     * Example: [1, 0, 4, 5, 6, 0, 10]
256
     *
257
     * @return array
258
     */
259
    protected function getPageNumbersWithTotal(): array
260
    {
261
        $aPageNumbers = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $aPageNumbers is dead and can be removed.
Loading history...
262
263
        // Determine the sliding range, centered around the current page.
264
        $nNumAdjacents = ($this->nMaxPages - 1) / 2;
265
266
        $nSlidingStart = 1;
267
        $nSlidingStartThreshold = $nNumAdjacents;
268
        $nSlidingEnd = $this->nPagesCount;
269
        $nSlidingEndThreshold = $this->nPagesCount - $nNumAdjacents;
270
271
        if($this->nPageNumber > $nNumAdjacents + 1)
272
        {
273
            $nSlidingStart = $this->nPageNumber - $nNumAdjacents + 2;
274
        }
275
        if($this->nPageNumber > $nSlidingEndThreshold)
276
        {
277
            $nSlidingStart -= $this->nPageNumber - $nSlidingEndThreshold;
278
        }
279
280
        if($this->nPageNumber < $this->nPagesCount - $nNumAdjacents)
281
        {
282
            $nSlidingEnd = $this->nPageNumber + $nNumAdjacents - 2;
283
        }
284
        if($this->nPageNumber <= $nSlidingStartThreshold)
285
        {
286
            $nSlidingEnd += $nSlidingStartThreshold - $this->nPageNumber + 1;
287
        }
288
289
        // Build the list of page numbers. Pages with 0 as number are ellipsys.
290
        $aStartPages = $nSlidingStart > 1 ? [1, 0] : [];
291
        $aEndPages = $nSlidingEnd < $this->nPagesCount ? [0, $this->nPagesCount] : [];
292
        $aPageNumbers = range($nSlidingStart, $nSlidingEnd);
293
        return [...$aStartPages, ...$aPageNumbers, ...$aEndPages];
294
    }
295
296
    /**
297
     * Get the current page number.
298
     *
299
     * @return int
300
     */
301
    public function currentPage(): int
302
    {
303
        return $this->nPageNumber;
304
    }
305
306
    /**
307
     * Get the links (pages raw data).
308
     *
309
     * @return array<Page>
310
     */
311
    public function pages(): array
312
    {
313
        $aPageNumbers = match(true) {
314
            $this->nItemsCount < 0 => $this->getPageNumbersWithoutTotal(),
315
            $this->nPagesCount < 2 => [],
316
            $this->nPagesCount <= $this->nMaxPages => $this->getAllPageNumbers(),
317
            default => $this->getPageNumbersWithTotal(),
318
        };
319
        if(count($aPageNumbers) === 0)
320
        {
321
            return [];
322
        }
323
324
        $aPages = array_map($this->getPage(...), $aPageNumbers);
325
        return [$this->getPrevPage(), ...$aPages, $this->getNextPage()];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array($this->getP..., $this->getNextPage()) returns an array which contains values of type array which are incompatible with the documented value type Jaxon\App\Pagination\Page.
Loading history...
326
    }
327
328
    /**
329
     * Call a closure that will receive the page number as parameter.
330
     *
331
     * @param Closure $fPageCallback
332
     *
333
     * @return Paginator
334
     */
335
    public function page(Closure $fPageCallback): Paginator
336
    {
337
        $fPageCallback($this->nPageNumber);
338
339
        return $this;
340
    }
341
342
    /**
343
     * Call a closure that will receive the pagination offset as parameter.
344
     *
345
     * @param Closure $fOffsetCallback
346
     *
347
     * @return Paginator
348
     */
349
    public function offset(Closure $fOffsetCallback): Paginator
350
    {
351
        $fOffsetCallback(($this->nPageNumber - 1) * $this->nItemsPerPage);
352
353
        return $this;
354
    }
355
356
    /**
357
     * Show the pagination links
358
     *
359
     * @return string
360
     */
361
    private function renderLinks(): string
362
    {
363
        $aPages = $this->pages();
364
        if(count($aPages) === 0)
365
        {
366
            return '';
367
        }
368
369
        $xPrevPage = array_shift($aPages); // The first entry in the array
370
        $xNextPage = array_pop($aPages); // The last entry in the array
371
        return $this->xPlugin->renderer()->render($aPages, $xPrevPage, $xNextPage);
372
    }
373
374
    /**
375
     * Show the pagination links
376
     *
377
     * @param string $sWrapperId
378
     *
379
     * @return array|null
380
     */
381
    private function showLinks(string $sWrapperId): ?array
382
    {
383
        $sHtml = $this->renderLinks();
384
        // The HTML code must always be displayed, even if it is empty.
385
        if(is_a($this->xPlugin->response(), Response::class))
386
        {
387
            /** @var Response */
388
            $xResponse = $this->xPlugin->response();
389
            $xResponse->html($sWrapperId, $sHtml);
390
            return !$sHtml ? null : ['id' => $sWrapperId];
391
        }
392
393
        // The wrapper id is not needed for the NodeResponse
394
        /** @var NodeResponse */
395
        $xResponse = $this->xPlugin->response();
396
        $xResponse->html($sHtml);
0 ignored issues
show
Bug introduced by
The method html() does not exist on Jaxon\Response\AbstractResponse. It seems like you code against a sub-type of said class. However, the method does not exist in Jaxon\Response\AjaxResponse. 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

396
        $xResponse->/** @scrutinizer ignore-call */ 
397
                    html($sHtml);
Loading history...
397
        return !$sHtml ? null : [];
398
    }
399
400
    /**
401
     * @param JsExpr $xCall
402
     * @param string $sWrapperId
403
     *
404
     * @return void
405
     */
406
    public function render(JsExpr $xCall, string $sWrapperId = ''): void
407
    {
408
        if(($xFunc = $xCall->func()) === null)
409
        {
410
            return;
411
        }
412
413
        $aParams = $this->showLinks(trim($sWrapperId));
414
        if($aParams !== null)
415
        {
416
            // Set click handlers on the pagination links
417
            $aParams['func'] = $xFunc->withPage()->jsonSerialize();
418
            $this->xPlugin->addCommand('pg.paginate', $aParams);
419
        }
420
    }
421
}
422