PageRenderTime 114ms CodeModel.GetById 19ms RepoModel.GetById 3ms app.codeStats 0ms

/classes/fPagination.php

https://bitbucket.org/dsqmoore/flourish
PHP | 693 lines | 407 code | 54 blank | 232 comment | 53 complexity | 22f725980f7e153cf72f5e5f83b05ab7 MD5 | raw file
  1. <?php
  2. /**
  3. * Prints pagination links for fRecordSet or other paginated records
  4. *
  5. * @copyright Copyright (c) 2010-2011 Will Bond
  6. * @author Will Bond [wb] <will@flourishlib.com>
  7. * @license http://flourishlib.com/license
  8. *
  9. * @package Flourish
  10. * @link http://flourishlib.com/fActiveRecord
  11. *
  12. * @version 1.0.0b
  13. * @changes 1.0.0b Added the `prev_disabled` and `next_disabled` pieces [wb, 2011-09-06]
  14. */
  15. class fPagination
  16. {
  17. // The following constants allow for nice looking callbacks to static methods
  18. const defineTemplate = 'fPagination::defineTemplate';
  19. const extend = 'fPagination::extend';
  20. const printRecordSetInfo = 'fPagination::printRecordSetInfo';
  21. const reset = 'fPagination::reset';
  22. const showRecordSetLinks = 'fPagination::showRecordSetLinks';
  23. /**
  24. * The available filters to use in templates
  25. *
  26. * @var array
  27. */
  28. static private $filters = array(
  29. 'inflect',
  30. 'lower',
  31. 'url_encode',
  32. 'humanize'
  33. );
  34. /**
  35. * The available templates to use for a paginator
  36. *
  37. * @var array
  38. */
  39. static private $templates = array(
  40. 'default' => array(
  41. 'type' => 'without_first_last',
  42. 'size' => 4,
  43. 'pieces' => array(
  44. 'info' => '<div class="paginator_info">Page {{ page }} of {{ total_records }} items</div>',
  45. 'start' => '<div class="paginator_list"><ul>',
  46. 'prev' => '<li class="prev"><a href="{{ url }}">Prev</a></li>',
  47. 'prev_disabled' => '',
  48. 'page' => '<li class="page {{ first }} {{ last }} {{ current }}"><a href="{{ url }}">{{ page }}</a></li>',
  49. 'next' => '<li class="next"><a href="{{ url }}">Next</a></li>',
  50. 'next_disabled' => '',
  51. 'end' => '</ul></div>'
  52. )
  53. )
  54. );
  55. /**
  56. * Defines a new template to use with the paginator
  57. *
  58. * The `$pieces` array must contain the following array keys:
  59. *
  60. * - `info`: the template to use when calling the `printInfo()` method
  61. * - `start`: the template to start the page list with
  62. * - `prev`: the template for the previous link
  63. * - `prev_disabled`: the template for the previous link, when disabled
  64. * - `page`: the template for a single page link
  65. * - `separator`: the template for the separator to use when the type is `with_first_last`
  66. * - `next`: the template for the next link
  67. * - `next_disabled`: the template for the next link, when disabled
  68. * - `end`: the template to end the page list with
  69. *
  70. * There are various pre-defined variables available for use in the template
  71. * pieces. These variables are printed by using the syntax `{{ variable }}`.
  72. *
  73. * The `info`, `start` and `end` pieces may use the following variables:
  74. *
  75. * - `page`: the page of records being shown
  76. * - `total_pages`: the total number of pages of records
  77. * - `first_record`: the record number of the first record being shown
  78. * - `last_record`: the record number of the last record being shown
  79. * - `total_records`: the total number of records being paginated
  80. *
  81. * The `prev` and `next` pieces may use the following variables:
  82. *
  83. * - `page`: the page number of the page of results being linked to
  84. * - `url`: the URL of the page being linked to
  85. *
  86. * The `page` piece may use the following variables:
  87. *
  88. * - `page`: the page number of the page being linked to
  89. * - `url`: the URL of the page being linked to
  90. * - `first`: the string "first" if the link is to the first page
  91. * - `last`: the string "last" if the link is to the last page
  92. * - `current`: the string "current" if the link is to the current page
  93. *
  94. * The `separator` piece does not have access to any pre-defined variables.
  95. *
  96. * In addition to the pre-defined variables, it is possible to add any other
  97. * variables to be used in any of the pieces by calling the instance method
  98. * ::set().
  99. *
  100. * It is possible to use variable filters on a variable to modify it. The
  101. * most common variable to filter would be `name`. To filter a variable,
  102. * add a `|` and the filter name after the variable name, in the form
  103. * `{{ variable|filter }}`. The following filters are available:
  104. *
  105. * - `inflect`: if the total number of records is not 1, pluralize the variable - this only works for nouns
  106. * - `lower`: converts the contents of the variable to lower case
  107. * - `url_encode`: encode the value for inclusion in a URL
  108. * - `humanize`: converts a `underscore_notation` or `CamelCase` string to a string with spaces between words and in `Title Caps`
  109. *
  110. * Filters can be combined, in which case they are list one after the other
  111. * in the form `{{ variable|filter_1|filter_2 }}`.
  112. *
  113. * @param string $name The name of the template
  114. * @param string $type The type of pagination: `without_first_last` or `with_first_last` - `with_first_last` always includes links to the first and last pages
  115. * @param integer $size The number of pages to show on either side of the current page
  116. * @param array $pieces The chunks of HTML to create the paginator from - see method description for details
  117. * @return void
  118. */
  119. static public function defineTemplate($name, $type, $size, $pieces)
  120. {
  121. $valid_types = array('without_first_last', 'with_first_last');
  122. if (!in_array($type, $valid_types)) {
  123. throw new fProgrammerException(
  124. 'The type specified, %1$s, is invalid. Must be one of: %2$s.',
  125. $type,
  126. join(', ', $valid_types)
  127. );
  128. }
  129. if (!preg_match('#^\d+$#D', $size)) {
  130. throw new fProgrammerException(
  131. 'The size specified, %1$s, is not a positive integer',
  132. $size
  133. );
  134. }
  135. if ($type == 'with_first_last' && $size < 3) {
  136. throw new fProgrammerException(
  137. 'The size specified, %1$s, is less than %2$s, which is the minimum size for the type %3$s',
  138. $size,
  139. 3,
  140. 'with_first_last'
  141. );
  142. }
  143. if ($type == 'without_first_last' && $size < 1) {
  144. throw new fProgrammerException(
  145. 'The size specified, %1$s, is less than %2$s, which is the minimum size for the type %3$s',
  146. $size,
  147. 1,
  148. 'without_first_last'
  149. );
  150. }
  151. $required_pieces = array('info', 'start', 'prev', 'prev_disabled', 'page');
  152. if ($type == 'with_first_last') {
  153. $required_pieces[] = 'separator';
  154. }
  155. $required_pieces = array_merge($required_pieces, array('next', 'next_disabled', 'end'));
  156. if (array_keys($pieces) != $required_pieces) {
  157. throw new fProgrammerException(
  158. 'The pieces specified, %1$s, do not correspond to the pieces required by the type %2$s: %3$s.',
  159. join(' ', array_keys($pieces)),
  160. $type,
  161. join(' ', $required_pieces)
  162. );
  163. }
  164. self::$templates[$name] = array(
  165. 'type' => $type,
  166. 'size' => $size,
  167. 'pieces' => $pieces
  168. );
  169. }
  170. /**
  171. * Adds the methods `printInfo()` and `showLinks()` to fRecordSet
  172. *
  173. * @return void
  174. */
  175. static public function extend()
  176. {
  177. fORM::registerRecordSetMethod('printInfo', self::printRecordSetInfo);
  178. fORM::registerRecordSetMethod('showLinks', self::showRecordSetLinks);
  179. }
  180. /**
  181. * Overlays user data over info from the record set
  182. *
  183. * @param array $data The user data
  184. * @param string|array $class The class or classes present in the record set
  185. * @return array The merged data
  186. */
  187. static private function extendRecordSetInfo($data, $class)
  188. {
  189. if (is_array($class)) {
  190. $record_name = array_map(array('fORM', 'getRecordName'), $class);
  191. } else {
  192. $record_name = fORM::getRecordName($class);
  193. }
  194. return array_merge(
  195. array(
  196. 'class' => $class,
  197. 'record_name' => $record_name
  198. ),
  199. $data
  200. );
  201. }
  202. /**
  203. * Handles the `printInfo()` method for fRecordSet
  204. *
  205. * @internal
  206. *
  207. * @param fRecordSet $object The record set
  208. * @param string|array $class The class(es) contained in the record set
  209. * @param array &$records The records
  210. * @param string $method_name The method that was called
  211. * @param array $parameters The parameters passed to the method
  212. * @return void
  213. */
  214. static public function printRecordSetInfo($object, $class, &$records, $method_name, $parameters)
  215. {
  216. $template = count($parameters) < 1 ? 'default' : $parameters[0];
  217. $data = count($parameters) < 2 ? array() : $parameters[1];
  218. $data = self::extendRecordSetInfo($data, $class);
  219. self::printTemplatedInfo($template, $data, $object->getPage(), $object->getLimit(), $object->count(TRUE));
  220. }
  221. /**
  222. * Prints the info for the displayed records
  223. *
  224. * @param string $template The template to use
  225. * @param array $data The extra data to make available to the template
  226. * @param integer $page The page of records being displayed
  227. * @param integer $per_page The number of records being displayed on each page
  228. * @param integer $total_records The total number of records
  229. * @return void
  230. */
  231. static private function printTemplatedInfo($template, $data, $page, $per_page, $total_records)
  232. {
  233. $total_pages = ceil($total_records/$per_page);
  234. self::printPiece(
  235. $template,
  236. 'info',
  237. array_merge(
  238. array(
  239. 'page' => $page,
  240. 'total_pages' => $total_pages,
  241. 'first_record' => (($page - 1) * $per_page) + 1,
  242. 'last_record' => min($page * $per_page, $total_records),
  243. 'total_records' => $total_records
  244. ),
  245. $data
  246. )
  247. );
  248. }
  249. /**
  250. * Resets the configuration of the class
  251. *
  252. * @internal
  253. *
  254. * @return void
  255. */
  256. static public function reset()
  257. {
  258. self::$filters = array(
  259. 'inflect',
  260. 'lower',
  261. 'url_encode',
  262. 'humanize'
  263. );
  264. self::$templates = array(
  265. 'default' => array(
  266. 'type' => 'without_first_last',
  267. 'size' => 4,
  268. 'pieces' => array(
  269. 'info' => '<div class="paginator_info">Page {{ page }} of {{ total_records }} items</div>',
  270. 'start' => '<div class="paginator_list"><ul>',
  271. 'prev' => '<li class="prev"><a href="{{ url }}">Prev</a></li>',
  272. 'prev_disabled' => '',
  273. 'page' => '<li class="page {{ first }} {{ last }} {{ current }}"><a href="{{ url }}">{{ page }}</a></li>',
  274. 'next' => '<li class="next"><a href="{{ url }}">Next</a></li>',
  275. 'next_disabled' => '',
  276. 'end' => '</ul></div>'
  277. )
  278. )
  279. );
  280. }
  281. /**
  282. * Handles the `showLinks()` method for fRecordSet
  283. *
  284. * @internal
  285. *
  286. * @param fRecordSet $object The record set
  287. * @param string|array $class The class(es) contained in the record set
  288. * @param array &$records The records
  289. * @param string $method_name The method that was called
  290. * @param array $parameters The parameters passed to the method
  291. * @return boolean If the links were shown
  292. */
  293. static public function showRecordSetLinks($object, $class, &$records, $method_name, $parameters)
  294. {
  295. $template = count($parameters) < 1 ? 'default' : $parameters[0];
  296. $data = count($parameters) < 2 ? array() : $parameters[1];
  297. $data = self::extendRecordSetInfo($data, $class);
  298. return self::showTemplatedLinks($template, $data, $object->getPage(), $object->getLimit(), $object->count(TRUE));
  299. }
  300. /**
  301. * Prints the links for a set of records
  302. *
  303. * @param string $template The template to use
  304. * @param array $data The extra data to make available to the template
  305. * @param integer $page The page of records being displayed
  306. * @param integer $per_page The number of records being displayed on each page
  307. * @param integer $total_records The total number of records
  308. * @return void
  309. */
  310. static private function showTemplatedLinks($template, $data, $page, $per_page, $total_records)
  311. {
  312. if ($total_records <= $per_page) {
  313. return FALSE;
  314. }
  315. $total_pages = ceil($total_records/$per_page);
  316. self::printPiece(
  317. $template,
  318. 'start',
  319. array_merge(
  320. array(
  321. 'page' => $page,
  322. 'total_pages' => $total_pages,
  323. 'first_record' => (($page - 1) * $per_page) + 1,
  324. 'last_record' => min($page * $per_page, $total_records),
  325. 'total_records' => $total_records
  326. ),
  327. $data
  328. )
  329. );
  330. if ($page > 1) {
  331. self::printPiece(
  332. $template,
  333. 'prev',
  334. array_merge(
  335. array(
  336. 'page' => $page - 1,
  337. 'url' => fURL::replaceInQueryString('page', $page - 1)
  338. ),
  339. $data
  340. )
  341. );
  342. } else {
  343. self::printPiece(
  344. $template,
  345. 'prev_disabled',
  346. $data
  347. );
  348. }
  349. if (self::$templates[$template]['type'] == 'without_first_last') {
  350. $start_page = max(1, $page - self::$templates[$template]['size']);
  351. $end_page = min($total_pages, $page + self::$templates[$template]['size']);
  352. } else {
  353. $start_separator = TRUE;
  354. $start_page = $page - (self::$templates[$template]['size'] - 2);
  355. if ($start_page <= 2) {
  356. $start_separator = FALSE;
  357. $start_page = 1;
  358. }
  359. $end_separator = TRUE;
  360. $end_page = $page + (self::$templates[$template]['size'] - 2);
  361. if ($end_page >= $total_pages - 1) {
  362. $end_separator = FALSE;
  363. $end_page = $total_pages;
  364. }
  365. }
  366. if (self::$templates[$template]['type'] == 'with_first_last' && $start_separator) {
  367. self::printPiece(
  368. $template,
  369. 'page',
  370. array_merge(
  371. array(
  372. 'page' => 1,
  373. 'url' => fURL::replaceInQueryString('page', 1),
  374. 'first' => 'first',
  375. 'last' => '',
  376. 'current' => ''
  377. ),
  378. $data
  379. )
  380. );
  381. self::printPiece(
  382. $template,
  383. 'separator',
  384. $data
  385. );
  386. }
  387. for ($loop_page = $start_page; $loop_page <= $end_page; $loop_page++) {
  388. self::printPiece(
  389. $template,
  390. 'page',
  391. array_merge(
  392. array(
  393. 'page' => $loop_page,
  394. 'url' => fURL::replaceInQueryString('page', $loop_page),
  395. 'first' => ($loop_page == 1) ? 'first' : '',
  396. 'last' => ($loop_page == $total_pages) ? 'last' : '',
  397. 'current' => ($loop_page == $page) ? 'current' : ''
  398. ),
  399. $data
  400. )
  401. );
  402. }
  403. if (self::$templates[$template]['type'] == 'with_first_last' && $end_separator) {
  404. self::printPiece(
  405. $template,
  406. 'separator',
  407. $data
  408. );
  409. self::printPiece(
  410. $template,
  411. 'page',
  412. array_merge(
  413. array(
  414. 'page' => $total_pages,
  415. 'url' => fURL::replaceInQueryString('page', $total_pages),
  416. 'first' => '',
  417. 'last' => 'last',
  418. 'current' => ''
  419. ),
  420. $data
  421. )
  422. );
  423. }
  424. if ($page < $total_pages) {
  425. self::printPiece(
  426. $template,
  427. 'next',
  428. array_merge(
  429. array(
  430. 'page' => $page + 1,
  431. 'url' => fURL::replaceInQueryString('page', $page + 1)
  432. ),
  433. $data
  434. )
  435. );
  436. } else {
  437. self::printPiece(
  438. $template,
  439. 'next_disabled',
  440. $data
  441. );
  442. }
  443. self::printPiece(
  444. $template,
  445. 'end',
  446. array_merge(
  447. array(
  448. 'page' => $page,
  449. 'total_pages' => $total_pages,
  450. 'first_record' => (($page - 1) * $per_page) + 1,
  451. 'last_record' => min($page * $per_page, $total_records),
  452. 'total_records' => $total_records
  453. ),
  454. $data
  455. )
  456. );
  457. return TRUE;
  458. }
  459. /**
  460. * Prints out a piece of a template
  461. *
  462. * @param string $template The name of the template to print
  463. * @param string $piece The piece of the template to print
  464. * @param array $data The data to replace the variables with
  465. * @return void
  466. */
  467. static private function printPiece($template, $name, $data)
  468. {
  469. if (!isset(self::$templates[$template]['pieces'][$name])) {
  470. throw new fProgrammerException(
  471. 'The template piece, %s, was not specified when defining the %s template',
  472. $name,
  473. $template
  474. );
  475. }
  476. $piece = self::$templates[$template]['pieces'][$name];
  477. preg_match_all('#\{\{ (\w+)((?:\|\w+)+)? \}\}#', $piece, $matches, PREG_SET_ORDER);
  478. foreach ($matches as $match) {
  479. $variable = $match[1];
  480. $value = (!isset($data[$variable])) ? NULL : $data[$variable];
  481. if (isset($match[2])) {
  482. $filters = array_slice(explode('|', $match[2]), 1);
  483. foreach ($filters as $filter) {
  484. if (!in_array($filter, self::$filters)) {
  485. throw new fProgrammerException(
  486. 'The filter specified, %1$s, is invalid. Must be one of: %2$s.',
  487. $filter,
  488. join(', ', self::$filters)
  489. );
  490. }
  491. if (!strlen($value)) {
  492. continue;
  493. }
  494. if ($filter == 'inflect') {
  495. $value = fGrammar::inflectOnQuantity($data['total_records'], $value);
  496. } elseif ($filter == 'lower') {
  497. $value = fUTF8::lower($value);
  498. } elseif ($filter == 'url_encode') {
  499. $value = urlencode($value);
  500. } elseif ($filter == 'humanize') {
  501. $value = fGrammar::humanize($value);
  502. }
  503. }
  504. }
  505. $piece = preg_replace('#' . preg_quote($match[0], '#') . '#', fHTML::encode($value), $piece, 1);
  506. }
  507. echo $piece;
  508. }
  509. /**
  510. * Extra data for the templates
  511. *
  512. * @var array
  513. */
  514. private $data;
  515. /**
  516. * The page number
  517. *
  518. * @var integer
  519. */
  520. private $page;
  521. /**
  522. * The number of records per page
  523. *
  524. * @var integer
  525. */
  526. private $per_page;
  527. /**
  528. * The total number of records
  529. *
  530. * @var integer
  531. */
  532. private $total_records;
  533. /**
  534. * Accepts the record information necessary for printing pagination
  535. *
  536. * @throws fValidationException When the `$page` is less than 1 or not an integer
  537. * @throws fNoRemainingException When there are not records for the specified `$page` and `$page` is greater than 1
  538. *
  539. * @param integer $records The total number of records
  540. * @param integer $per_page The number of records per page
  541. * @param integer $page The page number
  542. * @param fRecordSet :$records The records to create the paginator for
  543. * @return fPaginator
  544. */
  545. public function __construct($records, $per_page=NULL, $page=NULL)
  546. {
  547. if ($records instanceof fRecordSet) {
  548. $this->total_records = $records->count(TRUE);
  549. $this->per_page = $per_page === NULL ? $records->getLimit() : $per_page;
  550. $this->page = $page === NULL ? $records->getPage() : $page;
  551. } else {
  552. $this->total_records = $records;
  553. if ($per_page === NULL) {
  554. throw new fProgrammerException(
  555. 'No value was specified for the parameter %s',
  556. '$per_page'
  557. );
  558. }
  559. $this->per_page = $per_page;
  560. if ($page === NULL) {
  561. throw new fProgrammerException(
  562. 'No value was specified for the parameter %s',
  563. '$page'
  564. );
  565. }
  566. $this->page = $page;
  567. }
  568. if (!preg_match('#^\d+$#D', $this->page) || $this->page < 1) {
  569. throw new fValidationException(
  570. 'The page specified, %1$s, is not a whole number, or less than one',
  571. $this->page
  572. );
  573. }
  574. if ($this->page > 1 && ($this->per_page * ($this->page - 1)) + 1 > $this->total_records) {
  575. throw new fNoRemainingException(
  576. 'There are no remaining records to display',
  577. $this->page
  578. );
  579. }
  580. $this->data = array();
  581. }
  582. /**
  583. * Prints which records are showing on the current page
  584. *
  585. * @param string $template The template to use
  586. * @return void
  587. */
  588. public function printInfo($template='default')
  589. {
  590. self::printTemplatedInfo($template, $this->data, $this->page, $this->per_page, $this->total_records);
  591. }
  592. /**
  593. * Sets data to be available to the templates
  594. *
  595. * @param string $key The key to set
  596. * @param mixed $value The value to set
  597. * @param array :$data An associative array of keys and values
  598. * @return void
  599. */
  600. public function set($key, $value=NULL)
  601. {
  602. if (is_array($key)) {
  603. $this->data = array_merge($this->data, $key);
  604. } else {
  605. $this->data[$key] = $value;
  606. }
  607. }
  608. /**
  609. * Shows links to other pages when more than one page of records exists
  610. *
  611. * @param string $template The template to use
  612. * @return boolean If link were printed
  613. */
  614. public function showLinks($template='default')
  615. {
  616. return self::showTemplatedLinks($template, $this->data, $this->page, $this->per_page, $this->total_records);
  617. }
  618. }
  619. /**
  620. * Copyright (c) 2010-2011 Will Bond <will@flourishlib.com>
  621. *
  622. * Permission is hereby granted, free of charge, to any person obtaining a copy
  623. * of this software and associated documentation files (the "Software"), to deal
  624. * in the Software without restriction, including without limitation the rights
  625. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  626. * copies of the Software, and to permit persons to whom the Software is
  627. * furnished to do so, subject to the following conditions:
  628. *
  629. * The above copyright notice and this permission notice shall be included in
  630. * all copies or substantial portions of the Software.
  631. *
  632. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  633. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  634. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  635. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  636. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  637. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  638. * THE SOFTWARE.
  639. */