<?php
/**
* Pagination Class
*
* This class displays a pagination navigation bar complete with links to first, last,
* previous, next, and all pages. This class handles cookie setting, page bounds checking/
* redirection, appropriate error reporting, CSS styling, and POST/GET retrieval all
* internally.
*
* PHP version 5
*
* @author 		Dane Gardow <dane.g87@gmail.com>
* @copyright	2013 Dane Gardow
* @date			01 January 2013
* @version		1.0
* @license		Free
*
******************************************************************************************/

if(!class_exists("Pagination")):	// Prevent multiple class definitions

class Pagination
{
	/*******************************************************
	  PROPERTIES / DATA MEMBERS
	*******************************************************/

	// Edit these as you desire
	const DEFAULT_ENTRIES_DISPLAY = 10;							 // Default number of entries to display per page	
	const PAGE_GETVAR_NAME		  = "page";					     // Name of GET variable name for page values (i.e., example.php?page=1)

	private $_paginationID		  = "pagination";		// ID Name of pagination object			"pagination" is default
														// used also for form name for select options

	// Do not edit these values; they are simply null initializations
	private $_totalEntries         = null;		// Total number of entries (usually supplied by MySQL query)
	private $_pagesPerSection      = null;		// Total number of pages displayed per section (supplied by admin)
	private $_entriesPerPage       = null;		// Total number of entries displayed per page (supplied by user)
	private $_currentPage          = null;		// Current page viewed by user
	private $_displayOptions       = array();		// Array of options for viewing how many entries per page (supplied by user)
	private	$_errors               = array();		// Array of encountered error messages
	private	$_styles               = array();		// Array of CSS styles for pagination navigation display




	/*******************************************************
	  CONSTRUCTOR
	*******************************************************/
	
	function __construct($totalEntries, $pagesPerSection, $options = "", $paginationID = "", $stylePageOff = "",
						 $stylePageOn = "", $styleErrors = "", $styleSelect = "")
	{	
		$this->setPaginationID($paginationID);			// Set ID name of pagination object
		$this->setTotalEntries($totalEntries);			// Set total entries
		$this->setPagesPerSection($pagesPerSection);	// Set pages per section
		$this->setDisplayOptions($options);				// Set viewing display options
		$this->setEntriesPerPage();						// Set entries per page (input from POST or cookies)
		$this->setCurrentPage();						// Set current page (input from GET)
														// ! This function must follow after setEntriesPerPage()
		$this->setStyles($stylePageOff, $stylePageOn,
						 $styleSelect, $styleErrors);	// Set CSS styles for pagination navigation display
	}



	/*******************************************************
	  UTILITY FUNCTIONS
	*******************************************************/
	
	public function deleteCookie()			// deletes the cookie if it exists
	{
		$cookieVar = $this->_getPOSTVarName();
	
		if(isset($_COOKIE[$cookieVar]))					// If cookie is set
		{
			$_COOKIE[$cookieVar] = "";					// Clear cookie
			setcookie($cookieVar, "", time()-3600, "/");	// Delete cookie
		}
	}
	
	private function _getURL($input = 1)							// returns appropriate URL with all GET variables intact
	{																// updates only the particular GET variable in question
		$getVars = $_GET;											// Get all GET variables		
		
		/* Uncomment this if you need to exclude any GET variables (due to HTACCESS issues, for example) from being
		*  processed in the ensuing URL. Simply enter in the GET variable name in the unset(...) function below.
		unset($getVars["foo"], $getVars["bar"], ... );	// Remove any so they do not appear in URL
		*/
		
		$output  = '?'.http_build_query(array_merge($getVars, array($this->_getIDGETVarName()=>$input)));
		$output .= '#'. $this->getPaginationID();	// Add #xxx at the end of URL for auto-scrolling
		
		return $output;
	}
	
	private function _isError()				// determines if an error exists and registers any errors
	{		
		if($this->_errors)				// If error already exists, return
			return true;

		if(!$this->_totalEntries)		// If total entries not set
			$this->_errors[] = "The value for <strong>total entries</strong> has not been specified.";
		
		if(!$this->_displayOptions)		// If display options not set
			$this->_errors[] = "The values for <strong>display options</strong> have not been specified.";
		
		if(!$this->_entriesPerPage)		// If entries per page not set
			$this->_errors[] = "The value for <strong>entries per page</strong> has not been specified.";
		
		if(!$this->_currentPage)			// If current page not set
			$this->_errors[] = "The value for <strong>current page</strong> has not been specified.";
		
		return ($this->_errors) ? true : false;
	}

	private function _validEntry($input)				// determines if input is valid
	{
		if(is_array($input))				// If array
		{		
			foreach($input as $element)
			{														// Recursion: evaluate each array element
				if(!$this->_validEntry($element))					// If invalid
					return false;
			}

			return true;											// If function makes it to this point, it is valid
		}
		else								// If not array
		{
			if( (preg_match("/^\d+$/",$input) && $input > 0) || strtolower($input) == "all")	// If positive digit or "all"
				return true;
			else
				return false;
		}
	}
	
	private function _navBox($text, $destinationPage, $end = 0)		// returns span-encased link for pagination bar
	{	
		switch($end)
		{
			case 1:
				$title = "First page";
				break;
			case 2:
				$title = "Previous page";
				break;
			case 3:
				$title = "Next page";
				break;
			case 4:
				$title = "Last page (". $this->getTotalPages() .")";
				break;
			default:
				$title = "";
				break;
		}

		$title = ($end > 0 && $title != "") ? 'title="' . $title . '"' : '';

		$style = $this->_styles["pageOff"];

		// Determine Style
		$style = ($this->_currentPage == $destinationPage && !$end) ? $this->_styles["pageOn"] : $this->_styles["pageOff"];

		// Determine Link URL/Text		
		$url = "";
		
		if($this->_currentPage != $destinationPage			// If current page is not same as destination page
			&& $destinationPage <= $this->getTotalPages()	// and destination page does not exceed last page
			&& $destinationPage >= 1)						// and destination page is not less than first page
		{
			$text = '<a href="'. $this->_getURL($destinationPage) .'">'. $text .'</a>'; // then make $text a link
		}

		if($style)
			$style = 'class="'. $style .'"';
		
		$onClick = ($url) ? "onclick=\"location.href='". $url ."'\"" : "";	// Determine if span element is clickable
		
		return '<span '. $style . $title .' '. $onClick .'>'. $text .'</span>';
	}
	
	
	
	/*******************************************************
	  DISPLAY FUNCTIONS
	*******************************************************/
	
	public function display()				// displays the pagination bar
	{	
		if($this->_isError())						// If error encountered, do not display, but display errors
			return $this->displayErrors();

		$firstPage = 1;
		$previousPage = $this->_currentPage - 1;
		$nextPage = $this->_currentPage + 1;
		$lastPage = $this->getTotalPages();
		
		$totalpages = $this->getTotalPages();
		
		$pagesPerSection = $this->getPagesPerSection();
		
		$sectionStart = $this->_currentPage - floor($pagesPerSection / 2);	// Section start is current page # minus half the # of pages per section
		
		if($sectionStart <= 0)										// Adjust section start to 1 (first page) if # pages between 1st page
			$sectionStart = 1;										// and current page is less than half the # of pages per section
		
		$sectionEnd = $sectionStart + $pagesPerSection - 1;			// Section end is # pages per section after section start,
																	// minus 1 (otherwise # of pages per section will exceed given amount by 1)

		if($sectionEnd > $lastPage)									// Adjust section end to last page if section end exceeds last page
			$sectionEnd = $lastPage;

		$sectionStart = $sectionEnd - $pagesPerSection + 1;			// Adjust section start to # of pages behind section end

		$output  = $this->_navBox("&lt;&lt;", $firstPage, 1);	// First page
		$output .= $this->_navBox("&lt;", $previousPage, 2);		// Previous page
		
		for($i = $sectionStart; $i <= $sectionEnd; ++$i)
			$output .= $this->_navBox($i, $i);					// Pagination
		
		$output .= $this->_navBox("&gt;", $nextPage, 3);			// Next Page
		$output .= $this->_navBox("&gt;&gt;", $lastPage, 4);		// Last Page
		
		return $output;
	}
	
	public function displayErrors()			// displays the errors encountered
	{
		if(!$this->_errors)
			return "No errors were encountered.";

		$words = (count($this->_errors) > 1) ? "errors were" : "error was";

		// Determine CSS styling for error reporting
		if($this->_styles["errors"])
			$css = 'class="'. $this->_styles["errors"] .'"';
		else
			$css = '';
		
		$output = '
		<div '. $css .'>
		The following '. $words .' encountered while using the '. get_class($this) .' class:<br/><br/>
		<ul>';

		foreach($this->_errors as $error)
			$output .= '<li>'. $error .'</li>';

		$output .= '
		</ul>
		</div>';

		return $output;
	}
	
	public function displaySelectInterface()		// displays the <select> interface for choosing display amount
	{
		if(count($this->_displayOptions) < 2)				// If only 1 or fewer options, do not display
			return;
			
		if($this->_isError())								// If error encountered, do not display
			return;
		
		static $count = 0;	// This counts how many times function is run.
							// This variable value is tacked on the end of the form name which
							// will enable multiple instances of the display interface form
		
		$paginationID = $this->getPaginationID();
		
		$formName = $paginationID. '_select_form';
		
		// Determine CSS styling for <select>
		if($this->_styles["select"])
			$css = 'class="'. $this->_styles["select"] .'"';
		else
			$css = "";

		$formNumber = ($count) ? $count : "";
		
		$output = '
		<form name="'. $formName . $formNumber .'" method="post" style="display:inline-block;" action="'. $this->_getURL($this->_currentPage) .'">
		Show: 
		<select '. $css .' name="'. $this->_getPOSTVarName() .'" OnChange ="'. $formName . $formNumber .'.submit()">';
		
		foreach($this->_displayOptions as $line)
		{		
			if($this->_entriesPerPage == $line || $this->_entriesPerPage == $this->_totalEntries)	// If option equals entries per page
				$selected = "selected";															// or entries per page equals total entries
			else																				// then select option, otherwise
				$selected = "";																	// leave unselected

			$output .= '<option value="'. $line .'" '. $selected .'>'. $line .'</option>';
		}
		
		$output .= '
		</select>
		<noscript><input type="submit" name="paginationDisplaySubmit" value="Display"/></noscript>
		</form>';
		
		++$count;	// Increase # of times this function has run
		
		return $output;
	}
	
	
	
	/*******************************************************
	  SET FUNCTIONS
	*******************************************************/
	
	public function setCurrentPage()				// sets the currently accessed page from GET value
	{
		$idVar = $this->_getIDGETVarName();
	
		if(isset($_GET[$idVar]))		// If GET set
			$page = $_GET[$idVar];		// Retrieve page from GET
		else
			$page = 1;								// Otherwise use first page
		
		if($page < 1 || !preg_match("/^\d+$/", $page))						// If page is less than 1 or page value not a digit
		{
			header("Location: ". $this->_getURL());		// No argument for _getURL() sets it to 1 (first page)
			exit;
		}

		if($page > $this->getTotalPages() && $this->getTotalPages() != 0)	// If page exceeds last page
		{																	// 2nd condition prevents infinite loop should it equal 0
			header("Location: ". $this->_getURL($this->getTotalPages()));
			exit;
		}

		$this->_currentPage = $page;
	}

	public function setDisplayOptions($input)		// sets the user-specified display amount
	{
		if(!$this->_validEntry($input))				// If invalid entry encountered, register error and exit function
		{
			if(is_array($input))					// If array
			{
				$argument = "";
				
				foreach($input as $key=>$element)
				{
					if($key > 0)
						$argument .= ", ";

					$argument .= $element;			// Save all elements in string
				}
			}
			else									// If not array
			{
				$argument = $input;
			}

			$this->_errors[] = "The value(s) set for <strong>display options</strong> is/are invalid: ". $argument;

			return;
		}

		if(!is_array($input) && strtolower($input) == "all")	// If Not Array and "All" selected
			$input = $this->_totalEntries;						// Set total entries value
		
		$this->_displayOptions = $input;
	}

	public function setEntriesPerPage()				// sets entries per page amount from POST or COOKIE values
	{
		if($this->_errors)										// If an error, quit
			return;		
		
		$varName = $this->_getPOSTVarName();
		
		if(count($this->_displayOptions) > 1)					// If more than 1 display option
		{
			$value = $this->_displayOptions[0];					// Default is first selection
			
			if(isset($_POST[$varName]))				// If POST is set
			{
				if(in_array($_POST[$varName], $this->_displayOptions))		// Check for valid post value
				{
					$value = $_POST[$varName];
					setcookie($varName, $value, 604800 + time(), "/");		// Set cookie
					$_COOKIE[$varName] = $value;
				}
				else 																		// If invalid post value
				{					
					$value = self::DEFAULT_ENTRIES_DISPLAY;									// Set to default if invalid
				}
			}
			elseif(isset($_COOKIE[$varName]))			// If POST not set, but COOKIE set
			{
				// Check for valid cookie value
				if(in_array($_COOKIE[$varName], $this->_displayOptions))	// Check for valid cookie value
				{
					$value = $_COOKIE[$varName];							// Set to value if valid
				}
				else
				{
					$value = self::DEFAULT_ENTRIES_DISPLAY;									// Set to default if invalid
					$this->deleteCookie();													// Delete invalid cookie
				}
			}
		}
		else			// If only one option, set either to default or displayOptions value
		{
			$value = ($this->_displayOptions) ? $this->_displayOptions : self::DEFAULT_ENTRIES_DISPLAY;
		}

		if(strtolower($value) == "all")				// If set to "All", use total entries
			$value = $this->_totalEntries;
			
		$this->_entriesPerPage = $value;
	}
	
	public function setPagesPerSection($input)		// sets # of pages per section
	{	
		if(!$this->_validEntry($input))
		{
			$this->_errors[] = "The value set for <strong>pages per section</strong> is invalid: ". $input;
			return;
		}
		
		$this->_pagesPerSection = $input;
	}	
	
	public function setPaginationID($input)
	{
		if($input)
		{
			if(preg_match("/^\d+$/",$input[0]))		// Check if first character is a digit
			{
				$this->_errors[] = "The first character of the <strong>pagination ID</strong> cannot be a number: ". $input;
				return;					// cannot be a digit because variable names cannot start with digits,
			}							// and this value will be used as a variable name

			$this->_paginationID = $input;
		}
	}
	
	public function setStyles($pageOff = "", $pageOn = "", $select = "", $errors = "")	// sets CSS style class names
	{			
		$this->_styles = array(
			"pageOff"  => $pageOff,
			"pageOn"   => $pageOn,
			"select"   => $select,
			"errors" => $errors
			);
	}

	public function setTotalEntries($input)			// sets total number of entries
	{
		if(!$this->_validEntry($input))
		{
			$this->_errors[] = "The value set for <strong>total entries</strong> is invalid: ". $input;
			return;
		}
		
		$this->_totalEntries = $input;
	}




	/*******************************************************
	  GET FUNCTIONS
	*******************************************************/

	private function _getIDGETVarName()					// returns GET variable name for pagination pages
	{
		return $this->getPaginationID() .'-'. self::PAGE_GETVAR_NAME;
	}
	
	private function _getPOSTVarName()					// returns POST variable name for select/cookie entities
	{
		return $this->getPaginationID().'Display';
	}	
	
	public function getCurrentPage()					// returns the currently accessed page number
	{
		if(!$this->_currentPage)		// If not set, return first page
			return 1;
		else
			return $this->_currentPage;
	}

	public function getPagesPerSection()				// returns the # of pages per section
	{
		if(!$this->_pagesPerSection)									// If not set, set error and return 0
		{
			$this->_errors[] = "The value for <strong>pages per section</strong> has not been set.";
			return 0;
		}
	
		if($this->_pagesPerSection > $this->getTotalPages())			// If per section is greater than total pages
			return $this->getTotalPages();							// Return total pages
		else
			return $this->_pagesPerSection;							// Otherwise return per section
	}

	public function getPaginationID()					// returns ID name for pagination object
	{
		return $this->_paginationID;
	}
	
	public function getTotalPages()						// returns total pages
	{
		if($this->_errors)											// If there is an error, return 0
			return 0;
			
		if($this->_entriesPerPage == 0)								// Prevent division by zero
			return 0;
	
		return ceil($this->_totalEntries / $this->_entriesPerPage);	// Total pages: total # of entries divided by total entries per page
	}

	public function getEntryStart()						// returns the start entry for the page
	{
		if($this->_isError())			// If error encountered
			return 0;
	
		return ($this->_currentPage - 1) * $this->_entriesPerPage;	// Entry start: 1 less than current page multiplied by total entries per page
	}

	public function getEntryEnd()						// returns the last entry for the page
	{
		if($this->_isError())			// If error encountered
			return 0;

		if($this->_currentPage == $this->getTotalPages())			// If current page is last page
			return $this->_totalEntries - $this->getEntryStart();	// then entry end is total entries minus start entry
		else
			return $this->_entriesPerPage;							// otherwise entry end is # of entries per page
	}
	
	public function getEntryEndFF()		// Flat-file version of the above getEntryEnd() function
	{
		if($this->_isError())			// If error encountered
			return 0;
		
		if($this->_currentPage == $this->getTotalPages())			// If current page is last page
			return $this->_totalEntries;								// then entry end is total entries minus start entry
		else
			return $this->getEntryStart() + $this->_entriesPerPage;	// otherwise entry end is # of entries per page after start
	}
}

endif;	// Prevent multiple class definitions
?>