<?php
/**
* A generic pagination class that automatically generates navigation links given the total number of items and the
* number of items per page.
*
* Please note that this is a *generic* pagination class, meaning that it does not display any records! It is up to
* developer to fetch the actual data and displaying it based on the information returned by this class. The advantage
* is that it can be used to paginate over records coming from any source (arrays, database, etc).
*
* The appearance is customizable through CSS.
*
* The code is heavily commented and generates no warnings/errors/notices when PHP's error reporting level is set to
* E_ALL.
*
* Visit {@link http://stefangabos.ro/php-libraries/zebra-pagination/} for more information.
*
* For more resources visit {@link http://stefangabos.ro/}
*
* @author Stefan Gabos <hide@address.com>
* @version 1.4 (last revision: September 03, 2012)
* @copyright (c) 2009 - 2012 Stefan Gabos
* @license http://www.gnu.org/licenses/lgpl-3.0.txt GNU LESSER GENERAL PUBLIC LICENSE
* @package Zebra_Pagination
*/
class Zebra_Pagination
{
/**
* Constructor of the class.
*
* Initializes the class and the default properties.
*
* @return void
*/
function Zebra_Pagination()
{
// set default starting page
$this->page = 1;
// number of selectable pages
$this->selectable_pages(11);
// records per page
$this->records_per_page(10);
// by default, we assume there are no records
// we expect this number to be set after the class is instantiated
$this->records(0);
// by default, prefix page number with zeroes
$this->padding();
// this is the variable name to be used in the URL for propagating the page number
$this->variable_name('page');
// default method for page propagation
$this->method('get');
// trailing slashes are added to generated URLs
// (when "method" is "url")
$this->trailing_slash(true);
// set the base url
$this->base_url();
}
/**
* The base URL to be used when generating the navigation links.
*
* This is helpful for the case when, for example, the URL where the records are paginated may have parameters that
* are needed only once and need to be removed for any subsequent requests generated by pagination.
*
* For example, suppose some records are paginated at <i>http://yourwebsite/mypage/</i>. When a record from the list
* is updated, the URL could become something like <i>http://youwebsite/mypage/?action=updated</i>. Based on the
* value of <i>action</i> a message would be shown to the user.
*
* Because of the way this script works, the pagination links would become:
*
* - <i>http://youwebsite/mypage/?action=updated&page=[page number]</i> when {@link method()} is "get" and
* {@link variable_name()} is "page";
*
* - <i>http://youwebsite/mypage/page[page number]/?action=updated</i> when {@link method()} is "url" and
* {@link variable_name()} is "page").
*
* Because of this, whenever the user would paginate, the message would be shown to him again and again because
* <i>action</i> will be preserved in the URL!
*
* The solution is to set the <i>base_url</i> to <i>http://youwebsite/mypage/</i> and in this way, regardless of
* however will the URL be changed, the pagination links will always be in the form of
*
* - <i>http://youwebsite/mypage/?page=[page number]</i> when {@link method()} is "get" and {@link variable_name()}
* is "page";
*
* - <i>http://youwebsite/mypage/page[page number]/</i> when {@link method()} is "url" and {@link variable_name()} is "page").
*
* Of course, you may still have query strings in the value of the $base_url if you wish so, and these will be
* preserved when paginating.
*
* <samp>If you want to preserve the hash in the URL, make sure you load the zebra_pagination.js file!</samp>
*
* @param string $base_url (Optional) The base URL to be used when generating the navigation
* links
*
* Defaults is whatever returned by
* {@link http://www.php.net/manual/en/reserved.variables.server.php $_SERVER['REQUEST_URI']}
*
* @param boolean $preserve_query_strings (Optional) Indicates whether values in query strings, other than
* those set in $base_url, should be preserved
*
* Default is TRUE
*
* @return void
*/
function base_url($base_url = '', $preserve_query_strings = true)
{
// set the base URL
$this->_base_url = ($base_url == '' ? $_SERVER['REQUEST_URI'] : $base_url);
// parse the URL
$parsed_url = parse_url($this->_base_url);
// cache the "path" part of the URL (that is, everything *before* the "?")
$this->_base_url_path = $parsed_url['path'];
// cache the "query" part of the URL (that is, everything *after* the "?")
$this->_base_url_query = isset($parsed_url['query']) ? $parsed_url['query'] : '';
// store query string as an associative array
parse_str($this->_base_url_query, $this->_base_url_query);
// should query strings (other than those set in $base_url) be preserved?
$this->_preserve_query_strings = $preserve_query_strings;
}
/**
* Returns the current page's number.
*
* <code>
* // echoes the current page
* echo $pagination->get_page();
* </code>
*
* @return integer Returns the current page's number
*/
function get_page()
{
// if page was not already set through the "set_page" method
if (!isset($this->page_set)) {
// if
if (
// page propagation is SEO friendly
$this->_method == 'url' &&
// the current page is set in the URL
preg_match('/\b' . preg_quote($this->_variable_name) . '([0-9]+)\b/i', $_SERVER['REQUEST_URI'], $matches) > 0
) {
// set the current page to whatever it is indicated in the URL
$this->set_page((int)$matches[1]);
// if page propagation is done through GET and the current page is set in $_GET
} elseif (isset($_GET[$this->_variable_name])) {
// set the current page to whatever it was set to
$this->set_page((int)$_GET[$this->_variable_name]);
}
}
// get the total number of pages
$this->_total_pages = ceil($this->_records / $this->_records_per_page);
// if there are any pages
if ($this->_total_pages > 0) {
// if current page is beyond the total number pages
/// make the current page be the last page
if ($this->page > $this->_total_pages) $this->page = $this->_total_pages;
// if current page is smaller than 1
// make the current page 1
elseif ($this->page < 1) $this->page = 1;
}
// return the current page
return $this->page;
}
/**
* Set the method to be used for page propagation.
*
* <code>
* // set the method to the SEO friendly way
* $pagination->method('url');
* </code>
*
* @param string $method The method to be used for page propagation.
*
* Values can be:
*
* - <b>url</b> - page propagation is done in a SEO friendly way;
*
* This method requires the {@link http://httpd.apache.org/docs/current/mod/mod_rewrite.html mod_rewrite}
* module to be enabled on your Apache server (or the equivalent for other webservers);
*
* When using this method, the current page will be passed in the URL as
* <i>http://youwebsite.com/yourpage/[variable name][page number]/</i> where
* <i>[variable name]</i> is set by {@link variable_name()} and <i>[page number]</i>
* represents the current page.
*
* - <b>get</b> - page propagation is done through GET;
*
* When using this method, the current page will be passed in the URL as
* <i>http://youwebsite.com/yourpage?[variable name]=[page number]</i> where
* <i>[variable name]</i> is set by {@link variable_name()} and <i>[page number]</i>
* represents the current page.
*
* Default is "get".
*
* @returns void
*/
function method($method)
{
// by default, we assume page propagation is done through GET
$this->_method = 'get';
// make sure method is lowercase
$method = strtolower($method);
// if a valid method was specified
// set the page propagation method
if ($method == 'get' || $method == 'url') $this->_method = $method;
}
/**
* Sets whether page numbers should be prefixed by zeroes.
*
* This is useful to keep the layout consistent by having the same number of characters for each page number.
*
* <code>
* // disable padding numbers with zeroes
* $pagination->padding(false);
* </code>
*
* @param boolean $enabled (Optional) Setting this property to FALSE will disable padding rather than
* enabling it.
*
* Default is TRUE.
*
* @return void
*/
function padding($enabled = true)
{
// set padding
$this->_padding = $enabled;
}
/**
* Sets the total number of records that need to be paginated.
*
* Based on this and on the value of {@link records_per_page()}, the script will know how many pages there are.
*
* The total number of pages is given by the fraction between the total number records (set through {@link records()})
* and the number of records that are shown on a page (set through {@link records_per_page()}).
*
* <code>
* // tell the script that there are 100 total records
* $pagination->records(100);
* </code>
*
* @param integer $records The total number of records that need to be paginated
*
* @return void
*/
function records($records)
{
// the number of records
// make sure we save it as an integer
$this->_records = (int)$records;
}
/**
* Sets the number of records that are displayed on one page.
*
* Based on this and on the value of {@link records()}, the script will know how many pages there are: the total
* number of pages is given by the fraction between the total number of records and the number of records that are
* shown on one page.
*
* <code>
* // tell the class that there are 20 records displayed on one page
* $pagination->records_per_page(20);
* </code>
*
* @param integer $records_per_page The number of records displayed on one page.
*
* Default is 10.
*
* @return void
*/
function records_per_page($records_per_page)
{
// the number of records displayed on one page
// make sure we save it as an integer
$this->_records_per_page = (int)$records_per_page;
}
/**
* Generates the output.
*
* <i>Make sure your script references the CSS file!</i>
*
* <code>
* // generate output but don't echo it
* // but return it instead
* $output = $pagination->render(true);
* </code>
*
* @param boolean $return_output Setting this argument to TRUE will instruct the script to return the
* generated output rather than outputting it to the screen.
*
* Default is FALSE.
*
* @return void
*/
function render($return_output = false)
{
// get some properties of the class
$this->get_page();
// if there is a single page, or no pages at all, don't display anything
if ($this->_total_pages <= 1) return '';
// start building output
$output = '<div class="pagination">';
// if the number of total pages available is greater than the number of selectable pages
// it means we can show the "previous page" link
if ($this->_total_pages > $this->_selectable_pages) {
$output .= '<a href="' .
// the href is different if we're on the first page
($this->page == 1 ? 'javascript:void(0)' : $this->_build_uri($this->page - 1)) .
// if we're on the first page, the link is disabled
'" class="navigation left' . ($this->page == 1 ? ' disabled' : '') . '"' .
// good for SEO
// http://googlewebmastercentral.blogspot.de/2011/09/pagination-with-relnext-and-relprev.html
' rel="prev"' .
'>previous page</a>';
}
// if the total number of pages is lesser than the number of selectable pages
if ($this->_total_pages <= $this->_selectable_pages) {
// iterate through the available pages
for ($i = 1; $i <= $this->_total_pages; $i++) {
// render the link for each page
$output .= '<a href="' . $this->_build_uri($i) . '" ' .
// make sure to highlight the currently selected page
($this->page == $i ? 'class="current"' : '') . '>' .
// apply padding if required
($this->_padding ? str_pad($i, strlen($this->_total_pages), '0', STR_PAD_LEFT) : $i) .
'</a>';
}
// if the total number of pages is greater than the number of selectable pages
} else {
// put a link to the first page
$output .= '<a href="' . $this->_build_uri(1) . '" ' .
// highlight if it is the currently selected page
($this->page == 1 ? 'class="current"' : '') . '>' .
// apply padding if required
($this->_padding ? str_pad('1', strlen($this->_total_pages), '0', STR_PAD_LEFT) : '1') .
'</a>';
// compute the number of pages to display to the left of the currently selected page
// so that the current page is always centered
// (when at the first and the last pages this will not be possible and we'll make some adjustments on the fly)
$adjacent = floor(($this->_selectable_pages - 3) / 2);
// this number must be at least "1"
$adjacent = ($adjacent == 0 ? 1 : $adjacent);
// compute the page after which to show "..." after the link to the first page
$scroll_from = $this->_selectable_pages - $adjacent;
// this is the number from where we should start rendering selectable pages
// it's "2" because we have already rendered the first page
$starting_page = 2;
// if we need to show "..." after the link to the first page
if ($this->page >= $scroll_from) {
// by default, the starting_page should be whatever the current page minus $adjacent
$starting_page = $this->page - $adjacent;
// but if that would cause us to display less navigation links than specified in $this->_selectable_pages
if ($this->_total_pages - $starting_page < ($this->_selectable_pages - 2)) {
// adjust it
$starting_page -= ($this->_selectable_pages - 2) - ($this->_total_pages - $starting_page);
}
// put the "..." after the link to the first page
$output .= '<span>…</span>';
}
// this is the number where we should stop rendering selectable pages
// by default, this value is the sum of the starting page plus whatever the number of $this->_selectable_pages
// minus 3 (first page, last page and current page)
$ending_page = $starting_page + $this->_selectable_pages - 3;
// if ending page would be greater than the total number of pages minus 1
// (minus one because we don't take into account the very last page which we output automatically)
// adjust the ending page
if ($ending_page > $this->_total_pages - 1) $ending_page = $this->_total_pages - 1;
// place links for each page
for ($i = $starting_page; $i <= $ending_page; $i++) {
$output .= '<a href="' . $this->_build_uri($i) . '" ' .
// highlight the currently selected page
($this->page == $i ? 'class="current"' : '') . '>' .
// apply padding if required
($this->_padding ? str_pad($i, strlen($this->_total_pages), '0', STR_PAD_LEFT) : $i) .
'</a>';
}
// place the "..." before the link to the last page, if it is the case
if ($this->_total_pages - $ending_page > 1) $output .= '<span>…</span>';
// put a link to the last page
$output .= '<a href="' . $this->_build_uri($this->_total_pages) . '" ' .
// highlight if it is the currently selected page
($this->page == $i ? 'class="current"' : '') . '>' .
$this->_total_pages .
'</a>';
// if the total number of available pages is greater than the number of pages to be displayed at once
// it means we can show the "next page" link
if ($this->_total_pages > $this->_selectable_pages) {
$output .= '<a href="' .
// the href is different if we're on the last page
($this->page == $this->_total_pages ? 'javascript:void(0)' : $this->_build_uri($this->page + 1)) .
// if we're on the last page, the link is disabled
'" class="navigation right' . ($this->page == $this->_total_pages ? ' disabled' : '') . '"' .
// good for SEO
// http://googlewebmastercentral.blogspot.de/2011/09/pagination-with-relnext-and-relprev.html
' rel="next"' .
'>next page</a>';
}
}
// finish generating the output
$output .= '</div>';
// if $return_output is TRUE
// return the generated content
if ($return_output) return $output;
// if script gets this far, print generated content to the screen
echo $output;
}
/**
* Sets the number of links to be displayed at once (besides the "previous" and "next" buttons)
*
* <code>
* // display links to 15 pages
* $pagination->selectable_pages(15);
* </code>
*
* @param integer $selectable_pages The number of links to be displayed at once (besides the "previous" and
* "next" buttons).
*
* Default is 10.
*
* @return void
*/
function selectable_pages($selectable_pages)
{
// the number of selectable pages
// make sure we save it as an integer
$this->_selectable_pages = (int)$selectable_pages;
}
/**
* Sets the current page.
*
* <code>
* // sets the fifth page as the current page
* $pagination->set_page(5);
* </code>
*
* @param integer $page The page's number.
*
* A number lower than <b>1</b> will be interpreted as <b>1</b>, while a number
* greater than the total number of pages will be interpreted as the last page.
*
* The total number of pages is given by the fraction between the total number
* records (set through {@link records()}) and the number of records that are
* shown on one page (set through {@link records_per_page()}).
*
* @return void
*/
function set_page($page)
{
// set the current page
// make sure we save it as an integer
$this->page = (int)$page;
// if the number is lower than one
// make it '1'
if ($this->page < 1) $this->page = 1;
// set a flag so that the "get_page" method doesn't change this value
$this->page_set = true;
}
/**
* Enables or disabled trailing slash on the generated URLs when {@link method} is "url".
*
* Read more on the subject on {@link http://googlewebmastercentral.blogspot.com/2010/04/to-slash-or-not-to-slash.html Google Webmasters's official blog}.
*
* <code>
* // disables trailing slashes on generated URLs
* $pagination->trailing_slash(false);
* </code>
*
* @param boolean $enabled (Optional) Setting this property to FALSE will disable trailing slashes on generated
* URLs when {@link method} is "url".
*
* Default is TRUE (trailing slashes are enabled by default).
*
* @return void
*/
function trailing_slash($enabled)
{
// set the state of trailing slashes
$this->_trailing_slash = $enabled;
}
/**
* Sets the variable name to be used for page propagation.
*
* <code>
* // sets the variable name to "foo"
* // now, in the URL, the current page will be passed either as "foo=[page number]" (if method is "get") or
* // as "/foo/[page number]" (if method is "url")
* $pagination->variable_name('foo');
* </code>
*
* @param string $variable_name A string representing the variable name to be used for page propagation.
*
* Default is "page".
*
* @return void
*/
function variable_name($variable_name)
{
// set the variable name
$this->_variable_name = strtolower($variable_name);
}
/**
* Generate the links for the page buttons
*
* @access private
*
* @return void
*/
function _build_uri($page)
{
// if page propagation method is through SEO friendly URLs
if ($this->_method == 'url') {
// see if the current page is already set in the URL
if (preg_match('/\b' . $this->_variable_name . '([0-9]+)\b/i', $this->_base_url_path, $matches) > 0) {
// build string
$url = str_replace('//', '/', preg_replace(
// replace the currently existing value
'/\b' . $this->_variable_name . '([0-9]+)\b/i',
// if on the first page, remove it in order to avoid duplicate content
($page == 1 ? '' : $this->_variable_name . $page),
$this->_base_url_path
));
// if the current page is not yet in the URL, set it, unless we're on the first page
// case in which we don't set it in order to avoid duplicate content
} else $url = rtrim($this->_base_url_path, '/') . '/' . ($page != 1 ? $this->_variable_name . $page : '');
// handle trailing slash according to preferences
$url = rtrim($url, '/') . ($this->_trailing_slash ? '/' : '');
// if values in the query string - other than those set through base_url() - are not to be preserved
// preserve only those set initially
if (!$this->_preserve_query_strings) $query = implode('&', $this->_base_url_query);
// otherwise, get the current query string
else $query = $_SERVER['QUERY_STRING'];
// return the built string also appending the query string, if any
return $url . ($query != '' ? '?' . $query : '');
// if page propagation is to be done through GET
} else {
// if values in the query string - other than those set through base_url() - are not to be preserved
// preserve only those set initially
if (!$this->_preserve_query_strings) $query = $this->_base_url_query;
// otherwise, get the current query string, if any, and transform it to an array
else parse_str($_SERVER['QUERY_STRING'], $query);
// if not the first page
if ($page != 1)
// add/update the page number
$query[$this->_variable_name] = $page;
// don't use the "page" variable in order to avoid duplicate content
else unset($query[$this->_variable_name]);
// make sure the returned HTML is W3C compliant
return htmlspecialchars($this->_base_url_path . (!empty($query) ? '?' . http_build_query($query) : ''));
}
}
}
?>