1<?php
2
3namespace JasonGrimes;
4
5class Paginator
6{
7    const NUM_PLACEHOLDER = '(:num)';
8
9    protected $totalItems;
10    protected $numPages;
11    protected $itemsPerPage;
12    protected $currentPage;
13    protected $urlPattern;
14    protected $maxPagesToShow = 10;
15    protected $previousText = 'Previous';
16    protected $nextText = 'Next';
17
18    /**
19     * @param int $totalItems The total number of items.
20     * @param int $itemsPerPage The number of items per page.
21     * @param int $currentPage The current page number.
22     * @param string $urlPattern A URL for each page, with (:num) as a placeholder for the page number. Ex. '/foo/page/(:num)'
23     */
24    public function __construct($totalItems, $itemsPerPage, $currentPage, $urlPattern = '')
25    {
26        $this->totalItems = $totalItems;
27        $this->itemsPerPage = $itemsPerPage;
28        $this->currentPage = $currentPage;
29        $this->urlPattern = $urlPattern;
30
31        $this->updateNumPages();
32    }
33
34    protected function updateNumPages()
35    {
36        $this->numPages = ($this->itemsPerPage == 0 ? 0 : (int) ceil($this->totalItems/$this->itemsPerPage));
37    }
38
39    /**
40     * @param int $maxPagesToShow
41     * @throws \InvalidArgumentException if $maxPagesToShow is less than 3.
42     */
43    public function setMaxPagesToShow($maxPagesToShow)
44    {
45        if ($maxPagesToShow < 3) {
46            throw new \InvalidArgumentException('maxPagesToShow cannot be less than 3.');
47        }
48        $this->maxPagesToShow = $maxPagesToShow;
49    }
50
51    /**
52     * @return int
53     */
54    public function getMaxPagesToShow()
55    {
56        return $this->maxPagesToShow;
57    }
58
59    /**
60     * @param int $currentPage
61     */
62    public function setCurrentPage($currentPage)
63    {
64        $this->currentPage = $currentPage;
65    }
66
67    /**
68     * @return int
69     */
70    public function getCurrentPage()
71    {
72        return $this->currentPage;
73    }
74
75    /**
76     * @param int $itemsPerPage
77     */
78    public function setItemsPerPage($itemsPerPage)
79    {
80        $this->itemsPerPage = $itemsPerPage;
81        $this->updateNumPages();
82    }
83
84    /**
85     * @return int
86     */
87    public function getItemsPerPage()
88    {
89        return $this->itemsPerPage;
90    }
91
92    /**
93     * @param int $totalItems
94     */
95    public function setTotalItems($totalItems)
96    {
97        $this->totalItems = $totalItems;
98        $this->updateNumPages();
99    }
100
101    /**
102     * @return int
103     */
104    public function getTotalItems()
105    {
106        return $this->totalItems;
107    }
108
109    /**
110     * @return int
111     */
112    public function getNumPages()
113    {
114        return $this->numPages;
115    }
116
117    /**
118     * @param string $urlPattern
119     */
120    public function setUrlPattern($urlPattern)
121    {
122        $this->urlPattern = $urlPattern;
123    }
124
125    /**
126     * @return string
127     */
128    public function getUrlPattern()
129    {
130        return $this->urlPattern;
131    }
132
133    /**
134     * @param int $pageNum
135     * @return string
136     */
137    public function getPageUrl($pageNum)
138    {
139        return str_replace(self::NUM_PLACEHOLDER, $pageNum, $this->urlPattern);
140    }
141
142    public function getNextPage()
143    {
144        if ($this->currentPage < $this->numPages) {
145            return $this->currentPage + 1;
146        }
147
148        return null;
149    }
150
151    public function getPrevPage()
152    {
153        if ($this->currentPage > 1) {
154            return $this->currentPage - 1;
155        }
156
157        return null;
158    }
159
160    public function getNextUrl()
161    {
162        if (!$this->getNextPage()) {
163            return null;
164        }
165
166        return $this->getPageUrl($this->getNextPage());
167    }
168
169    /**
170     * @return string|null
171     */
172    public function getPrevUrl()
173    {
174        if (!$this->getPrevPage()) {
175            return null;
176        }
177
178        return $this->getPageUrl($this->getPrevPage());
179    }
180
181    /**
182     * Get an array of paginated page data.
183     *
184     * Example:
185     * array(
186     *     array ('num' => 1,     'url' => '/example/page/1',  'isCurrent' => false),
187     *     array ('num' => '...', 'url' => NULL,               'isCurrent' => false),
188     *     array ('num' => 3,     'url' => '/example/page/3',  'isCurrent' => false),
189     *     array ('num' => 4,     'url' => '/example/page/4',  'isCurrent' => true ),
190     *     array ('num' => 5,     'url' => '/example/page/5',  'isCurrent' => false),
191     *     array ('num' => '...', 'url' => NULL,               'isCurrent' => false),
192     *     array ('num' => 10,    'url' => '/example/page/10', 'isCurrent' => false),
193     * )
194     *
195     * @return array
196     */
197    public function getPages()
198    {
199        $pages = array();
200
201        if ($this->numPages <= 1) {
202            return array();
203        }
204
205        if ($this->numPages <= $this->maxPagesToShow) {
206            for ($i = 1; $i <= $this->numPages; $i++) {
207                $pages[] = $this->createPage($i, $i == $this->currentPage);
208            }
209        } else {
210
211            // Determine the sliding range, centered around the current page.
212            $numAdjacents = (int) floor(($this->maxPagesToShow - 3) / 2);
213
214            if ($this->currentPage + $numAdjacents > $this->numPages) {
215                $slidingStart = $this->numPages - $this->maxPagesToShow + 2;
216            } else {
217                $slidingStart = $this->currentPage - $numAdjacents;
218            }
219            if ($slidingStart < 2) $slidingStart = 2;
220
221            $slidingEnd = $slidingStart + $this->maxPagesToShow - 3;
222            if ($slidingEnd >= $this->numPages) $slidingEnd = $this->numPages - 1;
223
224            // Build the list of pages.
225            $pages[] = $this->createPage(1, $this->currentPage == 1);
226            if ($slidingStart > 2) {
227                $pages[] = $this->createPageEllipsis();
228            }
229            for ($i = $slidingStart; $i <= $slidingEnd; $i++) {
230                $pages[] = $this->createPage($i, $i == $this->currentPage);
231            }
232            if ($slidingEnd < $this->numPages - 1) {
233                $pages[] = $this->createPageEllipsis();
234            }
235            $pages[] = $this->createPage($this->numPages, $this->currentPage == $this->numPages);
236        }
237
238
239        return $pages;
240    }
241
242
243    /**
244     * Create a page data structure.
245     *
246     * @param int $pageNum
247     * @param bool $isCurrent
248     * @return Array
249     */
250    protected function createPage($pageNum, $isCurrent = false)
251    {
252        return array(
253            'num' => $pageNum,
254            'url' => $this->getPageUrl($pageNum),
255            'isCurrent' => $isCurrent,
256        );
257    }
258
259    /**
260     * @return array
261     */
262    protected function createPageEllipsis()
263    {
264        return array(
265            'num' => '...',
266            'url' => null,
267            'isCurrent' => false,
268        );
269    }
270
271    /**
272     * Render an HTML pagination control.
273     *
274     * @return string
275     */
276    public function toHtml()
277    {
278        if ($this->numPages <= 1) {
279            return '';
280        }
281
282        $html = '<ul class="pagination">';
283        if ($this->getPrevUrl()) {
284            $html .= '<li><a href="' . $this->getPrevUrl() . '">&laquo; '. $this->previousText .'</a></li>';
285        }
286
287        foreach ($this->getPages() as $page) {
288            if ($page['url']) {
289                $html .= '<li' . ($page['isCurrent'] ? ' class="active"' : '') . '><a href="' . $page['url'] . '">' . $page['num'] . '</a></li>';
290            } else {
291                $html .= '<li class="disabled"><span>' . $page['num'] . '</span></li>';
292            }
293        }
294
295        if ($this->getNextUrl()) {
296            $html .= '<li><a href="' . $this->getNextUrl() . '">'. $this->nextText .' &raquo;</a></li>';
297        }
298        $html .= '</ul>';
299
300        return $html;
301    }
302
303    public function __toString()
304    {
305        return $this->toHtml();
306    }
307
308    public function getCurrentPageFirstItem()
309    {
310        $first = ($this->currentPage - 1) * $this->itemsPerPage + 1;
311
312        if ($first > $this->totalItems) {
313            return null;
314        }
315
316        return $first;
317    }
318
319    public function getCurrentPageLastItem()
320    {
321        $first = $this->getCurrentPageFirstItem();
322        if ($first === null) {
323            return null;
324        }
325
326        $last = $first + $this->itemsPerPage - 1;
327        if ($last > $this->totalItems) {
328            return $this->totalItems;
329        }
330
331        return $last;
332    }
333
334    public function setPreviousText($text)
335    {
336        $this->previousText = $text;
337        return $this;
338    }
339
340    public function setNextText($text)
341    {
342        $this->nextText = $text;
343        return $this;
344    }
345}