1: <?php
2: /**
3: * Pinoco: makes existing static web site dynamic transparently.
4: * Copyright 2010-2012, Hisateru Tanaka <tanakahisateru@gmail.com>
5: *
6: * Licensed under The MIT License
7: * Redistributions of files must retain the above copyright notice.
8: *
9: * PHP Version 5
10: *
11: * @author Hisateru Tanaka <tanakahisateru@gmail.com>
12: * @copyright Copyright 2010-2012, Hisateru Tanaka <tanakahisateru@gmail.com>
13: * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
14: * @package Pinoco
15: */
16:
17: /**
18: * Pagination object designed for PHPTAL.
19: * This object is independent from RDBMS. You can use it with any data source.
20: *
21: * <code>
22: * $pagination = new Pinoco_Pagination(
23: * // How many elements?
24: * function($pagination) {
25: * return $pagination->db->prepare(
26: * "SELECT count(id) as c FROM ..."
27: * )->query()->fetchOne()->c;
28: * },
29: * // What to be shown?
30: * function($pagination, $offset, $limit) {
31: * return $pagination->db->prepare(
32: * "SELECT * FROM ... LIMIT $offset, $limit"
33: * )->query()->fetchAll();
34: * },
35: * // How the page number is formatted in navigation?
36: * function($pagination, $page) {
37: * return 'list' . ($page > 1 ? '?page=' . $page : '');
38: * },
39: * array(
40: * 'elementsPerPage' => 20,
41: * 'db' => $db, // you can pass any custom property to pagination.
42: * )
43: * );
44: * $pagination->page = 1;
45: * if (!$pagination->isValidPage) { $this->notfound(); }
46: * </code>
47: *
48: * PHPTAL example
49: * <code>
50: * <tal:block tal:repeat="element pagination/data">
51: * ${element/prop}
52: * </tal:block>
53: * <div class="pagination"
54: * tal:define="prev pagination/prev; pages pagination/pages; next pagination/next">
55: * <!--! prev button -->
56: * <a href="" tal:condition="prev/enabled"
57: * tal:attributes="href url:${prev/href}">PREV</a>
58: * <span class="disabled" tal:condition="not:prev/enabled">PREV</span>
59: * <!--! page link buttons -->
60: * <tal:block tal:repeat="page pages">
61: * <tal:block tal:condition="not:page/padding">
62: * <a href="" tal:condition="not:page/current"
63: * tal:attributes="href url:${page/href}"
64: * tal:content="page/number">1</a>
65: * <span class="current" tal:condition="page/current"
66: * tal:content="page/number">1</span>
67: * </tal:block>
68: * <span tal:condition="page/padding">...</span>
69: * </tal:block>
70: * <!--! next button -->
71: * <a href="" tal:condition="next/enabled"
72: * tal:attributes="href url:${next/href}">NEXT</a>
73: * <span class="disabled" tal:condition="not:next/enabled">NEXT</span>
74: * </div>
75: * </code>
76: *
77: * @package Pinoco
78: * @property integer $page The page number that starts with 1. (not 0!)
79: * @property integer $elementsPerPage Amount of data shown in single page.
80: * @property integer $pagesAfterFirst How many buttons after the first page. (-1: hides first page)
81: * @property integer $pagesAroundCurrent How many buttons around current page. (-1: expand all pages)
82: * @property integer $pagesBeforeLast How many buttons before the last page. (-1: hides last page)
83: * @property-read integer $totalCount Total number of elements.
84: * @property-read integer $totalPages Total number of pages.
85: * @property-read mixed $data Elements in paginated range.
86: * @property-read Pinoco_List $pages Navigation information of each page buttons.
87: * @property-read Pinoco_Vars $prev Navigation information of the prev button.
88: * @property-read Pinoco_Vars $next Navigation information of the prev button.
89: */
90: class Pinoco_Pagination extends Pinoco_DynamicVars
91: {
92: private $totalCountCallable;
93: private $dataFetchCallable;
94: private $urlFormatCallable;
95: private $_currentPage;
96: private $_elementsPerPage;
97: private $_pagesAfterFirst;
98: private $_pagesAroundCurrent;
99: private $_pagesBeforeLast;
100:
101: private $_totalCount;
102: private $_data;
103:
104: /**
105: * Creates pagination object from user codes.
106: *
107: * @param callback $totalCountCallable
108: * @param callback $dataFetchCallable
109: * @param callback $urlFormatCallable
110: * @param array $options
111: */
112: public function __construct($totalCountCallable, $dataFetchCallable,
113: $urlFormatCallable, $options=array())
114: {
115: $this->totalCountCallable = $totalCountCallable;;
116: $this->dataFetchCallable = $dataFetchCallable;
117: $this->urlFormatCallable = $urlFormatCallable;
118: $this->_currentPage = 1;
119: $this->_elementsPerPage = 10;
120: $this->_pagesAfterFirst = 0;
121: $this->_pagesAroundCurrent = 1;
122: $this->_pagesBeforeLast = 0;
123: $this->_totalCount = null;
124: $this->_data = null;
125: foreach ($options as $name=>$value) {
126: $this->set($name, $value);
127: }
128: }
129:
130: public function get_page()
131: {
132: return $this->_currentPage;
133: }
134: public function set_page($value)
135: {
136: if ($value < 1) {
137: throw new InvalidArgumentException('Invalid number of page:' . $value);
138: }
139: if ($value != $this->_currentPage) {
140: $this->_data = null;
141: }
142: $this->_currentPage = $value;
143: }
144:
145: public function get_isValidPage() {
146: return $this->page <= $this->totalPages;
147: }
148:
149: public function get_elementsPerPage()
150: {
151: return $this->_elementsPerPage;
152: }
153: public function set_elementsPerPage($value)
154: {
155: if ($value < 1) {
156: throw new InvalidArgumentException('Invalid number of elements:' . $value);
157: }
158: $this->_elementsPerPage = $value;
159: $this->_data = null;
160: }
161:
162: public function get_pagesAfterFirst()
163: {
164: return $this->_pagesAfterFirst;
165: }
166: public function set_pagesAfterFirst($value)
167: {
168: if ($value < -1) {
169: throw new InvalidArgumentException('Invalid number of links:' . $value);
170: }
171: $this->_pagesAfterFirst = $value;
172: }
173:
174: public function get_pagesAroundCurrent()
175: {
176: return $this->_pagesAroundCurrent;
177: }
178: public function set_pagesAroundCurrent($value)
179: {
180: if ($value < -1) {
181: throw new InvalidArgumentException('Invalid number of links:' . $value);
182: }
183: $this->_pagesAroundCurrent = $value;
184: }
185:
186: public function get_pagesBeforeLast()
187: {
188: return $this->_pagesBeforeLast;
189: }
190: public function set_pagesBeforeLast($value)
191: {
192: if ($value < -1) {
193: throw new InvalidArgumentException('Invalid number of links:' . $value);
194: }
195: $this->_pagesBeforeLast = $value;
196: }
197:
198: /**
199: * Total number of elements.
200: *
201: * @return integer
202: */
203: public function get_totalCount()
204: {
205: if (is_null($this->_totalCount)) {
206: $this->_totalCount = call_user_func($this->totalCountCallable, $this);
207: }
208: return $this->_totalCount;
209: }
210:
211: /**
212: * Elements in paginated range.
213: * The fetched result is cached before changing current page.
214: *
215: * @return mixed
216: */
217: public function get_data()
218: {
219: if (is_null($this->_data)) {
220: $this->_data = call_user_func(
221: $this->dataFetchCallable,
222: $this,
223: ($this->page - 1) * $this->elementsPerPage, // offset
224: $this->elementsPerPage // limit
225: );
226: }
227: return $this->_data;
228: }
229:
230: /**
231: * Force to clear cached data.
232: *
233: * @return void
234: */
235: public function reset()
236: {
237: $this->_data = null;
238: $this->_totalCount = null;
239: }
240:
241: /**
242: * Total number of pages.
243: *
244: * @return integer
245: */
246: public function get_totalPages()
247: {
248: return max(1, intval(($this->totalCount - 1) / $this->elementsPerPage) + 1);
249: }
250:
251: /**
252: * Navigation information of each page buttons.
253: *
254: * @return Pinoco_List
255: */
256: public function get_pages()
257: {
258: $pages = Pinoco::newList();
259: $leftpad = false;
260: $rightpad = false;
261: for ($i = 1; $i <= $this->totalPages; $i++) {
262: if ($this->_pagesAroundCurrent >= 0) {
263: $skipped = false;
264: if (!$leftpad && $i > 1 + $this->_pagesAfterFirst) {
265: $leftpad = true;
266: if (abs($i - $this->_currentPage) > $this->_pagesAroundCurrent) {
267: $skipped = true;
268: $skipto = max($i+1, $this->_currentPage - $this->_pagesAroundCurrent);
269: }
270: }
271: if ($leftpad && !$rightpad && $i > $this->_currentPage + $this->_pagesAroundCurrent) {
272: $rightpad = true;
273: if ($i < $this->totalPages - $this->_pagesBeforeLast) {
274: $skipped = true;
275: $skipto = max($i, $this->totalPages - $this->_pagesBeforeLast);
276: }
277: }
278: if ($skipped && isset($skipto)) {
279: $pages->push(Pinoco::newVars(array('padding' => true)));
280: $i = $skipto - 1;
281: continue;
282: }
283: }
284: $pages->push(Pinoco::newVars(array(
285: 'padding' => false,
286: 'number' => $i,
287: 'href' => call_user_func($this->urlFormatCallable, $this, $i),
288: 'current' => $i == $this->_currentPage,
289: )));
290: }
291: return $pages;
292: }
293:
294: /**
295: * Navigation information of the prev button.
296: *
297: * @return Pinoco_Vars
298: */
299: public function get_prev()
300: {
301: if ($this->_currentPage > 1) {
302: return Pinoco::newVars(array(
303: 'enabled' => true,
304: 'number' => $this->_currentPage - 1,
305: 'href' => call_user_func($this->urlFormatCallable, $this, $this->_currentPage - 1),
306: ));
307: }
308: else {
309: return Pinoco::newVars(array(
310: 'enabled' => false,
311: ));
312: }
313: }
314:
315: /**
316: * Navigation information of the next button.
317: *
318: * @return Pinoco_Vars
319: */
320: public function get_next()
321: {
322: if ($this->_currentPage < $this->totalPages) {
323: return Pinoco::newVars(array(
324: 'enabled' => true,
325: 'number' => $this->_currentPage + 1,
326: 'href' => call_user_func($this->urlFormatCallable, $this, $this->_currentPage + 1),
327: ));
328: }
329: else {
330: return Pinoco::newVars(array(
331: 'enabled' => false,
332: ));
333: }
334: }
335: }
336: