Passed
Push — fix-8832 ( 2eb5fa )
by Sam
07:48
created

PaginatedList   F

Complexity

Total Complexity 71

Size/Duplication

Total Lines 526
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 147
dl 0
loc 526
rs 2.7199
c 0
b 0
f 0
wmc 71

31 Methods

Rating   Name   Duplication   Size   Complexity  
A setLimitItems() 0 4 1
A setPageLength() 0 4 1
A setTotalItems() 0 4 1
A getPageLength() 0 3 1
A setRequest() 0 3 1
A MoreThanOnePage() 0 3 1
A FirstItem() 0 3 2
A setCurrentPage() 0 4 1
A getLimitItems() 0 3 1
A NotFirstPage() 0 3 1
A getRequest() 0 3 1
A getPaginationGetVar() 0 3 1
A TotalItems() 0 3 1
A setPaginationGetVar() 0 4 1
A setPageStart() 0 4 1
A NotLastPage() 0 3 1
A setPaginationFromQuery() 0 8 2
A getIterator() 0 10 3
A getPageStart() 0 15 5
A getTotalItems() 0 7 2
A __construct() 0 8 3
A PrevLink() 0 7 3
A TotalPages() 0 6 2
A toArray() 0 10 2
A LastItem() 0 9 3
A LastLink() 0 6 2
C PaginationSummary() 0 55 14
B Pages() 0 35 6
A NextLink() 0 7 3
A FirstLink() 0 6 2
A CurrentPage() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like PaginatedList often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PaginatedList, and based on these observations, apply Extract Interface, too.

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 Either a map of request parameters or
0 ignored issues
show
Bug introduced by
The type SilverStripe\ORM\Either was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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