Page 1 of 1

Yet even one more pagination class

Posted: Wed Dec 13, 2006 10:55 am
by Begby
I was looking for a good pagination class and came here and found JayBird's code. Although well written I needed it to be a bit more flexible for my use in that I am not always using MySQL or even a database for that matter. Some of my scripts get info from LDAP, log files, etc.

This script is in PHP 5.

The base of the class is the pagination class and also a renderer interface. The renderer is optional but allows you to create reusable HTML renderers to display the nav however you want. I have included one simple renderer example.

I also include a sample of an extended pagination class that puts back the mysql functionality so that it functions almost exactly like jaybird's original script (although I did not at this time put in his changes from the end of his thread). This extension has not been tested yet but I included it in this post as an example of extending it.

One thing that might need to be refactored in this is the URL handling. I hardly ever use '?category=hats&page=1&id=10' type url arguments but rather ?/hats/p.10/id/5/. This is part of the framework I use. For now I included the standard URL parameters as written in Jaybird's script but I might encapsulate that functionality into a delegate class or something (any ideas an good approach for that?).

Question: Since I used some of the code from JayBird's script, how do I credit him properly with regards to the GNU?

So here is the script

Renderer Interface

Code: Select all

/**
 * Interface for paginate renderers
 *
 * @abstract
 * @package lib
 * @subpackage paginatee
 */
interface IPaginateRenderer
{
	public function setNav( $array ) ;
	public function draw() ;
}
Implemented renderer using an HTML list

Code: Select all

/**
 * A paginate renderer for creating the navigation in a UL list
 * 
 * @package lib
 * @subpackage paginate
 */
class ULPaginateRenderer implements IPaginateRenderer 
{
	
	/**
	 * Array returned from Paginate
	 *
	 * @var array
	 * @access private
	 */
	private $nav ;
	
	/**
	 * The class for the UL tag
	 *
	 * @var string
	 * @access private
	 */
	private $ULClass ;
	
	
	/**
	 * The HTML ID for use with CSS
	 *
	 * @var string
	 */
	private $ID ;
	
	/**
	 * Constructor
	 *
	 * @param string $ULClass Class for the UL tag
	 */
	public function __construct( $ULClass, $ID )
	{
		$this->ULClass = $ULClass ;
		$this->ID = $ID ;
	}
	
	
	/**
	 * Set the array to render from the PaginateClass
	 *
	 * @param unknown_type $array
	 */
	public function setNav( $array )
	{
		$this->nav = $array ;
	}
	
	
	/**
	 * Create the navigation and return it as a string
	 *
	 * @return string
	 */
	public function draw()
	{
		$out = "<ul class=\"{$this->ULClass}\" id=\"{$this->ID}\">\n" ;
		foreach ($this->nav['pageLinks'] as $link)
		{
			$out.="<li>{$link}</li>\n" ;
		}
		$out.="</ul>\n" ;
		return $out ;
	}
}
Core pagination class

Code: Select all

/**
 * This class is used to create a set of navigation links for paging through a record listing
 * 
 *  *** INSERT GNU PUBLIC LICENSE HERE ****
 * 
 * This script uses logic and functionality from Mark Beechs Paginate script originally posted
 * at forums.devnetwork.net
 *
 * @version .9
 * @author Jeff Dorsch <jeff dot dorsch at g-mail> 
 * @package lib
 * @subpackage paginate
 *
 */
class Paginate
{
	
	/**
	 * The total number of records to page through
	 *
	 * @var int
	 * @access private
	 */
	private $numRecs ;
	
	/**
	 * The number of records to show per page
	 *
	 * @var int
	 * @access private
	 */
	private $recsPerPage ;
	
	/**
	 * The current page we are viewing
	 *
	 * @var int
	 * @access private
	 */
	private $currentPage ;
	
	/**
	 * A renderer delegate used to draw the navigation
	 *
	 * @see IPaginateRenderer
	 * @var IPaginateRenderer
	 * @access private
	 */
	private $renderer = null;
	
	/**
	 * The highest possible page that can be shown given the numeber of records
	 *
	 * @var int
	 * @access private
	 */
	private $maxPage ;
	
	/**
	 * The CSS class to use for the links
	 *
	 * @var string
	 * @access private
	 */
	private $linkClass = 'nav' ;
	
	/**
	 * The CSS class for the last page link
	 *
	 * @var string
	 * @access private
	 */
	private $linkLastClass = 'nav last' ;
	
	/**
	 * A string showing the navigation status in the form 'viewing 1 to x of x'
	 *
	 * @var string
	 * @access private
	 */
	private $viewing ;
	
	/**
	 * An array of generated links with the following keys
	 *
	 * 'pageLinks' - Links to individual pages with the current page in <strong> tags
	 * 'prevLink' - Link to the previous page
	 * 'nextLink' - Link to the next page
	 * 'first' - Link to the first page
	 * 'last' - I think you get it
	 * 
	 * @var array
	 * @access private
	 */
	private $nav = array() ;
	
	/**
	 * The number of page links to show after the current page
	 *
	 * @var int
	 * @access private
	 */
	private $pageLinksBefore = 3;
	
	/**
	 * The number of page links to show before the current pagt
	 *
	 * @var int
	 * @access private
	 */
	private $pageLinksAfter = 3; 
	
	
	/**
	 * Text for the last page link
	 *
	 * @var string
	 * @access private
	 */
	private $lastLink = 'Last' ;
	
	/**
	 * Text for the first page link
	 *
	 * @var string
	 * @access private
	 */
	private $firstLink = 'First' ;
	
	/**
	 * Text for the next page link
	 *
	 * @var string
	 * @access private
	 */
	private $nextLink = 'Next >>' ;
	
	/**
	 * Text for the previous page link
	 *
	 * @var string
	 * @access private
	 */
	private $prevLink = '<< Prev' ;
	

	/**
	 * The url Param to use for page links
	 *
	 * @var string
	 * @access private
	 */
	private $queryParam = 'page' ;
	
	
	/**
	 * The URL query straing sans the queryParam from above
	 *
	 * @var string
	 * @access private
	 */
	private $currentQueryString = '' ;
	
	/**
	 * Constructor
	 *
	 * @param int $numRecs Total number of records to page through
	 * @param int $recsPerPage Number of records to show per page
	 * @param int $currentPage The current page
	 * @param IPaginateRenderer $renderer The renderer to use for draw()
	 */
	public function __construct( $numRecs, $recsPerPage, $currentPage, IPaginateRenderer $renderer = null )
	{
		
		$this->numRecs = $numRecs ;
		$this->recsPerPage = $recsPerPage ;
		
		if( $renderer !== null ) $this->setRenderer( $renderer );


		$this->maxPage = ceil($this->numRecs / $this->recsPerPage);
		
		// Do some checks to make sure the current page is within range to avoid nasty errors
		$currentPage = intval($currentPage) ;
		$this->currentPage = ($currentPage > $this->maxPage) ? $this->maxPage : $currentPage ;
		if( $this->currentPage <= 0 ) $this->currentPage = 1 ;
		
		// Do stuff
		$this->getQueryString();
		$this->createNav();
		$this->createOtherNavElements();
		$this->createViewing();
		
	}
	
	
	/**
	 * Get the query string and remove the query param
	 *
	 * @access private
	 */
	private function getQueryString()
	{
		if(!empty($_GET))
		foreach($_GET as $k=>$v)
			if($k != $this->queryParam)
				$this->currentQueryString .= $k."=".$v."&";
	}
	
	
	/**
	 * Set the renderer for draw()
	 *
	 * @param IPaginateRenderer $renderer
	 */
	public function setRenderer( IPaginateRenderer $renderer )
	{
		$this->renderer = $renderer ;
	}
	
	
	/**
	 * Draw the navigation using a PaginateRenderer
	 *
	 * @return string HTML menu
	 */
	public function draw()
	{
		if ( !($this->renderer instanceof IPaginateRenderer) )
		{
			throw new Exception('No Paginate Renderer set in Paginate::draw()') ;
		}
		
		$this->renderer->setNav($this->getNavArray()) ;
		return $this->renderer->draw() ;
	}
	
	
	/**
	 * Get the viewing status
	 *
	 * @return string
	 */
	public function drawViewing()
	{
		return $this->viewing ;
	}
	
	
	/**
	 * Get the raw array of links
	 *
	 * @return array
	 */
	public function getNavArray()
	{
		return $this->nav ;
	}

	
	/**
	 * Get the first record number on the current page
	 *
	 * @return int
	 */
	public function getStartRecord()
	{
		return ($this->currentPage * $this->recsPerPage) - $this->recsPerPage + 1 ;
	}
	
	
	/**
	 * Returns the last record number on the current page
	 *
	 * @return int
	 */
	public function getEndRecord()
	{
		return ($this->currentPage == $this->maxPage) 
			? $this->numRecs 
			: $this->currentPage * $this->recsPerPage ;
	}
	
	
	 /**
     * Create the navigation page links
     * 
     * @access private
     */
    private function createNav()
	{
        if ($this->numRecs != 0)
        {    
        	// Upper and lower boundary
        	$lowerBoundary = (($this->currentPage - $this->pageLinksBefore) < 1)
        		? 1
        		: $this->currentPage - $this->pageLinksBefore ;
        	
        	$upperBoundary = (($this->currentPage + $this->pageLinksAfter) > $this->maxPage)
        		? $this->maxPage
        		: $this->currentPage + $this->pageLinksAfter ;
                
           
        	// Create our links
			for ($x = $lowerBoundary; $x <= $upperBoundary; $x++)
			{
				$links[] = ($this->currentPage == $x)
					? '<strong>'.$this->createLink( $x, $x, $this->linkLastClass ).'</strong>'
					: $this->createLink($x, $x, $this->linkClass) ;
			}
			
			// Add ... to first and last page links if applicable
			if (($this->currentPage - $this->pageLinksBefore) > 1) 				$links[0] = '... '.$links[0] ;
			if (($this->currentPage + $this->pageLinksAfter) < $this->maxPage) 	$links[count($links)-1] .=' ...' ;
			
			$this->nav['pageLinks'] = $links ;

        }
    }
    
    
    
	/**
	 * Create other navigation elements such as next, previous etc.
	 *
	 * @access private
	 */
	private function createOtherNavElements()
	{
		if ($this->currentPage != 1)
		{
			$this->nav['prevLink'] = $this->createLink( $this->currentPage-1, $this->prevLink, $this->linkClass ) ;
		}
		if ($this->currentPage != $this->maxPage)
		{
			$this->nav['nextLink'] = $this->createLink( $this->currentPage+1, $this->nextLink, $this->linkClass ) ;
		}
		
		$this->nav['firstLink'] = 	$this->createLink( 1, $this->firstLink, $this->linkClass ) ;
		$this->nav['lastLink'] = 	$this->createLink( $this->maxPage, $this->lastLink, $this->linkClass ) ;
		
	}
    
    
	
	/**
	 * Create the viewing string
	 * 
	 * @access private
	 */
	private function createViewing()
	{	
		$this->viewing = "{$this->getStartRecord()} to {$this->getEndRecord()} of {$this->numRecs}" ;
	}
	
	
	/**
	 * Create a link to a page, used internally
	 * 
	 * @param int $page The page number to link to
	 * @param string $linkText The text for the link
	 * @param string $class The CSS class
	 * @return string  An HTML link
	 * @access private
	 */
	private function createLink( $page, $linkText, $class )
	{
		return "<a href=\"?{$this->currentQueryString}{$this->queryParam}={$page}\" class=\"{$class}\">{$linkText}</a>" ;
	}
}
MySQL extension example - Untested at this point

Code: Select all

/**
 * An extension of Paginate that automatically retrieves the number of records
 * 
 * @package lib
 * @subpackage paginate
 * @todo This is very inefficent with large recordsets or slow queries as it will retrieve the full set with no limit everytime, this should be modified to allow for using COUNT()
 * @todo Actually test this
 */
class MySQLPaginate extends Paginate 
{
	/**
	 * The SQL query
	 *
	 * @var string
	 * @access private
	 */
	private $query ;
	
	/**
	 * Constructor
	 *
	 * @param string $query SQL Query
	 * @param int $recsPerPage Records to show per page
	 * @param int $currentPage The current page we are on
	 * @param IPaginateRenderer $renderer The renderer to use
	 */
	public function __construct($query, $recsPerPage, $currentPage, IPaginateRenderer $renderer = null )
	{
		
		$numRecs = $this->getNumRecs() ;
		parent::__construct( $numRecs, $recsPerPage, $currentPage, $renderer ) ;
		$this->alterQuery();
	}
	
	
	/**
	 * Get the number of records that we are browsing
	 *
	 * @return int
	 * @access private
	 * @todo Make more efficient, see main class comments
	 */
    private function getNumRecs()
    {
        $this->result = mysql_query($this->query);
        return mysql_num_rows($this->result);
    }

    
    /**
     * Modify the query to include a limit clause
     *
     * @param int $recsPerPage The records to display per page
     */
    private function alterQuery( $recsPerPage )
    {
    	$this->query .= " LIMIT {$this->getStartRecord()}, {$recsPerPage}" ;
    }
    
    
    /**
     * Return the raw array, this is the same array from Paginate with the query appended to it
     * 
     * @see Paginate::getNavArray()
     * @return array
     */
    public function getNavArray()
    {
    	$nav = parent::getNavArray() ;
    	$nav['query'] = $this->query ;
    	return $nav ;
    }
}

Usage

Code: Select all

$numRecs = 255 ; //Get this from a query or array count or something
$recsPerPage = config::getRecsPerPage() ;  // I fetch this from an app config
$currentPage = $_GET['page'] ;
$p = new Paginate( $numRecs, $recsPerPage, $currentPage, new ULPaginateRenderer('', 'rec_nav') ) ;

echo $p->drawViewing() ;
echo $p->draw() ;
Outputs

Code: Select all

11 to 20 of 144

<ul class="" id="rec_nav">
<li><a href="?page=1" class="nav">1</a></li>
<li><strong><a href="?page=2" class="nav last">2</a></strong></li>
<li><a href="?page=3" class="nav">3</a></li>
<li><a href="?page=4" class="nav">4</a></li>
<li><a href="?page=5" class="nav">5</a> ...</li>
</ul>
By editing the paginate class you can change the default values for the class and link names. This will also do next, prev, first, last links but the sample renderer I used doesn't use them.

If you don't want to use a renderer you can call getNavArray() to get an array of the links instead of calling draw().

Posted: Wed Dec 13, 2006 11:32 am
by John Cartwright

Code: Select all

/**
         * Get the number of records that we are browsing
         *
         * @return int
         * @access private
         * @todo Make more efficient, see main class comments
         */
    private function getNumRecs()
    {
        $this->result = mysql_query($this->query);
        return mysql_num_rows($this->result);
    }
See Jaybird's code for a possibility to manipulate the query to change the select to count