Passed
Push — 4 ( e217a3...20134e )
by
unknown
06:42
created

PaginatedList::LastPage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use SilverStripe\Control\HTTP;
6
use SilverStripe\Control\HTTPRequest;
7
use SilverStripe\ORM\Queries\SQLSelect;
8
use SilverStripe\View\ArrayData;
9
use ArrayAccess;
10
use Exception;
11
use IteratorIterator;
12
13
/**
14
 * A decorator that wraps around a data list in order to provide pagination.
15
 */
16
class PaginatedList extends ListDecorator
17
{
18
19
    protected $request;
20
    protected $getVar = 'start';
21
22
    protected $pageLength = 10;
23
    protected $pageStart;
24
    protected $totalItems;
25
    protected $limitItems = true;
26
27
    /**
28
     * Constructs a new paginated list instance around a list.
29
     *
30
     * @param SS_List $list The list to paginate. The getRange method will
31
     *        be used to get the subset of objects to show.
32
     * @param array|ArrayAccess $request Either a map of request parameters or
33
     *        request object that the pagination offset is read from.
34
     * @throws Exception
35
     */
36
    public function __construct(SS_List $list, $request = [])
37
    {
38
        if (!is_array($request) && !$request instanceof ArrayAccess) {
0 ignored issues
show
introduced by
$request is always a sub-type of ArrayAccess.
Loading history...
39
            throw new Exception('The request must be readable as an array.');
40
        }
41
42
        $this->setRequest($request);
0 ignored issues
show
Bug introduced by
It seems like $request can also be of type array; however, parameter $request of SilverStripe\ORM\PaginatedList::setRequest() does only seem to accept ArrayAccess|SilverStripe\Control\HTTPRequest, maybe add an additional type check? ( Ignorable by Annotation )

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

42
        $this->setRequest(/** @scrutinizer ignore-type */ $request);
Loading history...
43
        parent::__construct($list);
44
    }
45
46
    /**
47
     * Returns the GET var that is used to set the page start. This defaults
48
     * to "start".
49
     *
50
     * If there is more than one paginated list on a page, it is neccesary to
51
     * set a different get var for each using {@link setPaginationGetVar()}.
52
     *
53
     * @return string
54
     */
55
    public function getPaginationGetVar()
56
    {
57
        return $this->getVar;
58
    }
59
60
    /**
61
     * Sets the GET var used to set the page start.
62
     *
63
     * @param string $var
64
     * @return $this
65
     */
66
    public function setPaginationGetVar($var)
67
    {
68
        $this->getVar = $var;
69
        return $this;
70
    }
71
72
    /**
73
     * Returns the number of items displayed per page. This defaults to 10.
74
     *
75
     * @return int
76
     */
77
    public function getPageLength()
78
    {
79
        return $this->pageLength;
80
    }
81
82
    /**
83
     * Set the number of items displayed per page. Set to zero to disable paging.
84
     *
85
     * @param int $length
86
     * @return $this
87
     */
88
    public function setPageLength($length)
89
    {
90
        $this->pageLength = (int)$length;
91
        return $this;
92
    }
93
94
    /**
95
     * Sets the current page.
96
     *
97
     * @param int $page Page index beginning with 1
98
     * @return $this
99
     */
100
    public function setCurrentPage($page)
101
    {
102
        $this->pageStart = ((int)$page - 1) * $this->getPageLength();
103
        return $this;
104
    }
105
106
    /**
107
     * Returns the offset of the item the current page starts at.
108
     *
109
     * @return int
110
     */
111
    public function getPageStart()
112
    {
113
        $request = $this->getRequest();
114
        if ($this->pageStart === null) {
115
            if ($request
116
                && isset($request[$this->getPaginationGetVar()])
117
                && $request[$this->getPaginationGetVar()] > 0
118
            ) {
119
                $this->pageStart = (int)$request[$this->getPaginationGetVar()];
120
            } else {
121
                $this->pageStart = 0;
122
            }
123
        }
124
125
        return $this->pageStart;
126
    }
127
128
    /**
129
     * Sets the offset of the item that current page starts at. This should be
130
     * a multiple of the page length.
131
     *
132
     * @param int $start
133
     * @return $this
134
     */
135
    public function setPageStart($start)
136
    {
137
        $this->pageStart = (int)$start;
138
        return $this;
139
    }
140
141
    /**
142
     * Returns the total number of items in the unpaginated list.
143
     *
144
     * @return int
145
     */
146
    public function getTotalItems()
147
    {
148
        if ($this->totalItems === null) {
149
            $this->totalItems = count($this->list);
150
        }
151
152
        return $this->totalItems;
153
    }
154
155
    /**
156
     * Sets the total number of items in the list. This is useful when doing
157
     * custom pagination.
158
     *
159
     * @param int $items
160
     * @return $this
161
     */
162
    public function setTotalItems($items)
163
    {
164
        $this->totalItems = (int)$items;
165
        return $this;
166
    }
167
168
    /**
169
     * Sets the page length, page start and total items from a query object's
170
     * limit, offset and unlimited count. The query MUST have a limit clause.
171
     *
172
     * @param SQLSelect $query
173
     * @return $this
174
     */
175
    public function setPaginationFromQuery(SQLSelect $query)
176
    {
177
        if ($limit = $query->getLimit()) {
178
            $this->setPageLength($limit['limit']);
179
            $this->setPageStart($limit['start']);
180
            $this->setTotalItems($query->unlimitedRowCount());
181
        }
182
        return $this;
183
    }
184
185
    /**
186
     * Returns whether or not the underlying list is limited to the current
187
     * pagination range when iterating.
188
     *
189
     * By default the limit method will be called on the underlying list to
190
     * extract the subset for the current page. In some situations, if the list
191
     * is custom generated and already paginated you don't want to additionally
192
     * limit the list. You can use {@link setLimitItems} to control this.
193
     *
194
     * @return bool
195
     */
196
    public function getLimitItems()
197
    {
198
        return $this->limitItems;
199
    }
200
201
    /**
202
     * @param bool $limit
203
     * @return $this
204
     */
205
    public function setLimitItems($limit)
206
    {
207
        $this->limitItems = (bool)$limit;
208
        return $this;
209
    }
210
211
    /**
212
     * @return IteratorIterator
213
     */
214
    public function getIterator()
215
    {
216
        $pageLength = $this->getPageLength();
217
        if ($this->limitItems && $pageLength) {
218
            $tmptList = clone $this->list;
219
            return new IteratorIterator(
220
                $tmptList->limit($pageLength, $this->getPageStart())
0 ignored issues
show
Bug introduced by
The method limit() does not exist on SilverStripe\ORM\SS_List. It seems like you code against a sub-type of said class. However, the method does not exist in SilverStripe\ORM\Sortable or SilverStripe\ORM\Filterable. 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

220
                $tmptList->/** @scrutinizer ignore-call */ 
221
                           limit($pageLength, $this->getPageStart())
Loading history...
221
            );
222
        } else {
223
            return new IteratorIterator($this->list);
224
        }
225
    }
226
227
    /**
228
     * Returns a set of links to all the pages in the list. This is useful for
229
     * basic pagination.
230
     *
231
     * By default it returns links to every page, but if you pass the $max
232
     * parameter the number of pages will be limited to that number, centered
233
     * around the current page.
234
     *
235
     * @param  int $max
236
     * @return SS_List
237
     */
238
    public function Pages($max = null)
239
    {
240
        $result = new ArrayList();
241
242
        if ($max) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $max of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
243
            $start = ($this->CurrentPage() - floor($max / 2)) - 1;
244
            $end = $this->CurrentPage() + floor($max / 2);
245
246
            if ($start < 0) {
247
                $start = 0;
248
                $end = $max;
249
            }
250
251
            if ($end > $this->TotalPages()) {
252
                $end = $this->TotalPages();
253
                $start = max(0, $end - $max);
254
            }
255
        } else {
256
            $start = 0;
257
            $end = $this->TotalPages();
258
        }
259
260
        for ($i = $start; $i < $end; $i++) {
261
            $result->push(new ArrayData([
262
                'PageNum' => $i + 1,
263
                'Link' => HTTP::setGetVar(
264
                    $this->getPaginationGetVar(),
265
                    $i * $this->getPageLength(),
266
                    ($this->request instanceof HTTPRequest) ? $this->request->getURL(true) : null
267
                ),
268
                'CurrentBool' => $this->CurrentPage() == ($i + 1)
269
            ]));
270
        }
271
272
        return $result;
273
    }
274
275
    /**
276
     * Returns a summarised pagination which limits the number of pages shown
277
     * around the current page for visually balanced.
278
     *
279
     * Example: 25 pages total, currently on page 6, context of 4 pages
280
     * [prev] [1] ... [4] [5] [[6]] [7] [8] ... [25] [next]
281
     *
282
     * Example template usage:
283
     * <code>
284
     *    <% if MyPages.MoreThanOnePage %>
285
     *        <% if MyPages.NotFirstPage %>
286
     *            <a class="prev" href="$MyPages.PrevLink">Prev</a>
287
     *        <% end_if %>
288
     *        <% loop MyPages.PaginationSummary(4) %>
289
     *            <% if CurrentBool %>
290
     *                $PageNum
291
     *            <% else %>
292
     *                <% if Link %>
293
     *                    <a href="$Link">$PageNum</a>
294
     *                <% else %>
295
     *                    ...
296
     *                <% end_if %>
297
     *            <% end_if %>
298
     *        <% end_loop %>
299
     *        <% if MyPages.NotLastPage %>
300
     *            <a class="next" href="$MyPages.NextLink">Next</a>
301
     *        <% end_if %>
302
     *    <% end_if %>
303
     * </code>
304
     *
305
     * @param  int $context The number of pages to display around the current
306
     *         page. The number should be event, as half the number of each pages
307
     *         are displayed on either side of the current one.
308
     * @return SS_List
309
     */
310
    public function PaginationSummary($context = 4)
311
    {
312
        $result = new ArrayList();
313
        $current = $this->CurrentPage();
314
        $total = $this->TotalPages();
315
316
        // Make the number even for offset calculations.
317
        if ($context % 2) {
318
            $context--;
319
        }
320
321
        // If the first or last page is current, then show all context on one
322
        // side of it - otherwise show half on both sides.
323
        if ($current == 1 || $current == $total) {
324
            $offset = $context;
325
        } else {
326
            $offset = floor($context / 2);
327
        }
328
329
        $left = max($current - $offset, 1);
330
        $right = min($current + $offset, $total);
331
        $range = range($current - $offset, $current + $offset);
332
333
        if ($left + $context > $total) {
334
            $left = $total - $context;
335
        }
336
337
        for ($i = 0; $i < $total; $i++) {
338
            $link = HTTP::setGetVar(
339
                $this->getPaginationGetVar(),
340
                $i * $this->getPageLength(),
341
                ($this->request instanceof HTTPRequest) ? $this->request->getURL(true) : null
342
            );
343
            $num = $i + 1;
344
345
            $emptyRange = $num != 1 && $num != $total && (
346
                $num == $left - 1 || $num == $right + 1
347
            );
348
349
            if ($emptyRange) {
350
                $result->push(new ArrayData([
351
                    'PageNum' => null,
352
                    'Link' => null,
353
                    'CurrentBool' => false
354
                ]));
355
            } elseif ($num == 1 || $num == $total || in_array($num, $range)) {
356
                $result->push(new ArrayData([
357
                    'PageNum' => $num,
358
                    'Link' => $link,
359
                    'CurrentBool' => $current == $num
360
                ]));
361
            }
362
        }
363
364
        return $result;
365
    }
366
367
    /**
368
     * @return int
369
     */
370
    public function CurrentPage()
371
    {
372
        $pageLength = $this->getPageLength();
373
        return $pageLength
374
            ? floor($this->getPageStart() / $pageLength) + 1
375
            : 1;
376
    }
377
378
    /**
379
     * @return int
380
     */
381
    public function TotalPages()
382
    {
383
        $pageLength = $this->getPageLength();
384
        return $pageLength
385
            ? ceil($this->getTotalItems() / $pageLength)
386
            : min($this->getTotalItems(), 1);
387
    }
388
389
    /**
390
     * @return bool
391
     */
392
    public function MoreThanOnePage()
393
    {
394
        return $this->TotalPages() > 1;
395
    }
396
397
    /**
398
     * @return bool
399
     */
400
    public function FirstPage()
401
    {
402
        return $this->CurrentPage() == 1;
403
    }
404
405
    /**
406
     * @return bool
407
     */
408
    public function NotFirstPage()
409
    {
410
        return !$this->FirstPage();
411
    }
412
413
    /**
414
     * @return bool
415
     */
416
    public function LastPage()
417
    {
418
        return $this->CurrentPage() == $this->TotalPages();
419
    }
420
421
    /**
422
     * @return bool
423
     */
424
    public function NotLastPage()
425
    {
426
        return !$this->LastPage();
427
    }
428
429
    /**
430
     * Returns the number of the first item being displayed on the current
431
     * page. This is useful for things like "displaying 10-20".
432
     *
433
     * @return int
434
     */
435
    public function FirstItem()
436
    {
437
        return ($start = $this->getPageStart()) ? $start + 1 : 1;
438
    }
439
440
    /**
441
     * Returns the number of the last item being displayed on this page.
442
     *
443
     * @return int
444
     */
445
    public function LastItem()
446
    {
447
        $pageLength = $this->getPageLength();
448
        if (!$pageLength) {
449
            return $this->getTotalItems();
450
        } elseif ($start = $this->getPageStart()) {
451
            return min($start + $pageLength, $this->getTotalItems());
452
        } else {
453
            return min($pageLength, $this->getTotalItems());
454
        }
455
    }
456
457
    /**
458
     * Returns a link to the first page.
459
     *
460
     * @return string
461
     */
462
    public function FirstLink()
463
    {
464
        return HTTP::setGetVar(
465
            $this->getPaginationGetVar(),
466
            0,
467
            ($this->request instanceof HTTPRequest) ? $this->request->getURL(true) : null
468
        );
469
    }
470
471
    /**
472
     * Returns a link to the last page.
473
     *
474
     * @return string
475
     */
476
    public function LastLink()
477
    {
478
        return HTTP::setGetVar(
479
            $this->getPaginationGetVar(),
480
            ($this->TotalPages() - 1) * $this->getPageLength(),
481
            ($this->request instanceof HTTPRequest) ? $this->request->getURL(true) : null
482
        );
483
    }
484
485
    /**
486
     * Returns a link to the next page, if there is another page after the
487
     * current one.
488
     *
489
     * @return string
490
     */
491
    public function NextLink()
492
    {
493
        if ($this->NotLastPage()) {
494
            return HTTP::setGetVar(
495
                $this->getPaginationGetVar(),
496
                $this->getPageStart() + $this->getPageLength(),
497
                ($this->request instanceof HTTPRequest) ? $this->request->getURL(true) : null
498
            );
499
        }
500
    }
501
502
    /**
503
     * Returns a link to the previous page, if the first page is not currently
504
     * active.
505
     *
506
     * @return string
507
     */
508
    public function PrevLink()
509
    {
510
        if ($this->NotFirstPage()) {
511
            return HTTP::setGetVar(
512
                $this->getPaginationGetVar(),
513
                $this->getPageStart() - $this->getPageLength(),
514
                ($this->request instanceof HTTPRequest) ? $this->request->getURL(true) : null
515
            );
516
        }
517
    }
518
519
    /**
520
     * Returns the total number of items in the list
521
     */
522
    public function TotalItems()
523
    {
524
        return $this->getTotalItems();
525
    }
526
527
    /**
528
     * Set the request object for this list
529
     *
530
     * @param HTTPRequest|ArrayAccess $request
531
     */
532
    public function setRequest($request)
533
    {
534
        $this->request = $request;
535
    }
536
537
    /**
538
     * Get the request object for this list
539
     */
540
    public function getRequest()
541
    {
542
        return $this->request;
543
    }
544
}
545