PHP5 Paginator Class

Coding Critique is the place to post source code for peer review by other members of DevNetwork. Any kind of code can be posted. Code posted does not have to be limited to PHP. All members are invited to contribute constructive criticism with the goal of improving the code. Posted code should include some background information about it and what areas you specifically would like help with.

Popular code excerpts may be moved to "Code Snippets" by the moderators.

Moderator: General Moderators

Post Reply
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

PHP5 Paginator Class

Post by Jenk »

Knocked up a simple Pagination class with the primary objective being as much flexibility as possible. :)

Thus instead of tieing it in directly with mysql_* functions, or even a database object, I have built it as a decorator/helper for any array.

As this is the Critique forum, please crticise away :)

Code: Select all

<?php

/**
 * Pagination Class
 * Used for easy creation of paginated data
 *  which is stored in an array
 */
class jmt_Paginator
{
    /**
     * Data to be paginated.
     * @var array data
     * @private
     */
    private $data;
    /**
     * Rows to be displayed per page
     * @var int itemsPerPage
     * @private
     */
    private $itemsPerPage;
    /**
     * Current Page
     * @var int currentPage
     * @private
     */
    private $currentPage;
    /**
     * The first page
     * @var int firstPage
     * @private
     */
    private $firstPage;
    /**
     * The last page of paginated data.
     * @var int lastPage
     * @private
     */
    private $lastPage;

    /**
     * Paginator constructor
     * sets default values of properties
     * @param object data
     * @param int itemsPerPage
     */
    public function __construct (array $data, $itemsPerPage = 10)
    {
        $this->data         = $data;
        $this->itemsPerPage = $itemsPerPage;
        $this->currentPage  = 1;
        $this->firstPage    = 1;
        $this->lastPage     = ceil(count($this->data) / $this->itemsPerPage);
    }

    /**
     * Retrieve the data of the current page.
     * @return array data
     */
    public function getPageData ()
    {
        $start  = ($this->itemsPerPage * ($this->currentPage - 1));

        return array_slice($this->data, $start, $this->itemsPerPage);
    }

    /**
     * Set the current page.
     * Will default to first/last if supplied page number out of range.
     * @param int page
     */
    public function setPage ($page)
    {
        if ($page > $this->lastPage) $page = $this->lastPage;
        if ($page < $this->firstPage) $page = $this->firstPage;

        $this->currentPage = $page;
    }

    /**
     * Get the current page number.
     * @return int pageNumber
     */
    public function getCurrentPage ()
    {
        return $this->currentPage;
    }

    /**
     * Get the first page number
     * @return int pageNumber
     */
    public function getFirstPage ()
    {
        return $this->firstPage;
    }

    /**
     * Get the last page number
     * @return int pageNumber
     */
    public function getLastPage ()
    {
        return $this->lastPage;
    }

    /**
     * Get the next page number
     * @return int pageNumber
     * @return bool noNextPage
     */
    public function getNextPage ()
    {
        if ($this->currentPage < $this->lastPage) {
            return ($this->currentPage + 1);
        } else {
            return false;
        }
    }

    /**
     * Get the previous page number
     * @return int pageNumber
     * @return bool noPrevPage
     */
    public function getPrevPage ()
    {
        if ($this->currentPage > $this->firstPage) {
            return ($this->currentPage - 1);
        } else {
            return false;
        }
    }

    /**
     * Get total number of pages
     * @return int totalPages
     */
    public function getTotalPages ()
    {
        return $this->lastPage;
    }
}

?>
The Test:

Code: Select all

class PaginationTest extends UnitTestCase 
{

    var $array;
    function PaginationTest ()
    {
        $this->array = array(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);
        $this->UnitTestCase();
    }
    
    function testPaginationDefaultRowsPerPage () {
        // default items per page = 10; 15 items = 2pages.
        $pagin = new jmt_Paginator($this->array);
        
        $pagin->setPage(-99);
        $this->assertEqual($pagin->getPageData(), array(1,2,3,4,5,6,7,8,9,10));
        
        $pagin->setPage(1);
        $this->assertEqual($pagin->getPageData(), array(1,2,3,4,5,6,7,8,9,10));
        
        $pagin->setPage(2);
        $this->assertEqual($pagin->getPageData(), array(11,12,13,14,15));
        
        $pagin->setPage(99);
        $this->assertEqual($pagin->getPageData(), array(11,12,13,14,15));                 
    }
   
    function testPaginationNonDefaultRowsPerPage () {
        // 5 items per page; 15 items = 3 pages.
        $pagin = new jmt_Paginator($this->array, 5);
        
        $pagin->setPage(-99);
        $this->assertEqual($pagin->getPageData(), array(1,2,3,4,5));
        
        $pagin->setPage(1);
        $this->assertEqual($pagin->getPageData(), array(1,2,3,4,5));
        
        $pagin->setPage(2);
        $this->assertEqual($pagin->getPageData(), array(6,7,8,9,10));
        
        $pagin->setPage(3);
        $this->assertEqual($pagin->getPageData(), array(11,12,13,14,15));
        
        $pagin->setPage(99);
        $this->assertEqual($pagin->getPageData(), array(11,12,13,14,15));        
    }
    
    function testPaginationGetPage () {
        // 5 items per page; 15 items = 3 pages.
        $pagin = new jmt_Paginator($this->array, 5);
        
        $this->assertEqual($pagin->getNextPage(), 2);
        $this->assertEqual($pagin->getPrevPage(), false);
        $this->assertEqual($pagin->getCurrentPage(), 1);
        
        $pagin->setPage(2);
        $this->assertEqual($pagin->getNextPage(), 3);
        $this->assertEqual($pagin->getPrevPage(), 1);
        $this->assertEqual($pagin->getCurrentPage(), 2);
        
        $pagin->setPage(99);
        $this->assertEqual($pagin->getNextPage(), false);
        $this->assertEqual($pagin->getPrevPage(), 2);
        $this->assertEqual($pagin->getCurrentPage(), 3);
    }
}
EDIT: update - binned iArrayObject, now accepts primitive type array in constructor and uses array_* functions.
Last edited by Jenk on Tue Aug 22, 2006 7:56 am, edited 3 times in total.
User avatar
Oren
DevNet Resident
Posts: 1640
Joined: Fri Apr 07, 2006 5:13 am
Location: Israel

Post by Oren »

Haven't read it up to the end, but if you look at this:

Code: Select all

$this->rowsPerPage * $this->currentPage - $this->rowsPerPage
To make things easier... x = $this->rowsPerPage and y = $this->currentPage, now your code will look like this: x * y - x and...
x * y - x === x(y - 1)
Therefore your code can be replaced with this:

Code: Select all

$this->rowsPerPage * ($this->currentPage - 1)
:wink:
User avatar
shiznatix
DevNet Master
Posts: 2745
Joined: Tue Dec 28, 2004 5:57 pm
Location: Tallinn, Estonia
Contact:

Post by shiznatix »

Oren wrote:Haven't read it up to the end, but if you look at this:

Code: Select all

$this->rowsPerPage * $this->currentPage - $this->rowsPerPage
To make things easier... x = $this->rowsPerPage and y = $this->currentPage, now your code will look like this: x * y - x and...
x * y - x === x(y - 1)
Therefore your code can be replaced with this:

Code: Select all

$this->rowsPerPage * ($this->currentPage - 1)
:wink:
negative. 5 * (4 - 1) != (5 * 4) - 1

your algebra is flawed. remember * and / are done before - and + :wink:

edit: spelling error fixed
User avatar
feyd
Neighborhood Spidermoddy
Posts: 31559
Joined: Mon Mar 29, 2004 3:24 pm
Location: Bothell, Washington, USA

Post by feyd »

the offset calculation is

perPageCount * (pageNumber - 1) which does equate to perPageCount * pageNumber - perPageCount

Oren's math is correct.
User avatar
shiznatix
DevNet Master
Posts: 2745
Joined: Tue Dec 28, 2004 5:57 pm
Location: Tallinn, Estonia
Contact:

Post by shiznatix »

feyd wrote:the offset calculation is

perPageCount * (pageNumber - 1) which does equate to perPageCount * pageNumber - perPageCount

Oren's math is correct.
mis hasai!? i thought php did things like algebra with the * and / being done before others. How does php preform mathmatical calculations?
Ree
Forum Regular
Posts: 592
Joined: Fri Jun 10, 2005 1:43 am
Location: LT

Post by Ree »

It performs them the same way the whole civilized world does ;)
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

shiznatix, it's not about operator precedence, it's about the distributive rule:

Code: Select all

x ( y + z ) = xy + xz
x ( y - 1 ) = xy - x
Taking a look at the code right now.
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

Personally, I think it's fantastic. Even though whichever object implements iArrayObject will need to know about the database, the actual pagination logic is localized so that it's easy to test.

However, there are some problems:

- Need more accessor functions: All your properties are private, so how do we figure out how many items are being shown from just the object? (I know, you ought to be able to figure it out from a previously passed value, but it would be nicer to get it from the object).
- assertEqual(s) are backwards: The expected value is first, and then the result.
- Way to bypass page (offset): While pages are very nice, occasionally, you'll want to pass an arbitrary offset to the class. The internal representation should be in offset + limit (rowsPerPage), while page numbers map on to an offset.
- Missing sanity checks: for one thing, make sure rowsPerPage is int by typecasting it or something. Make sure it's not negative either.

You asked for criticism! ;-)
timvw
DevNet Master
Posts: 4897
Joined: Mon Jan 19, 2004 11:11 pm
Location: Leuven, Belgium

Post by timvw »

I find the 'iArrayObject' a bit of a confusing name... If you look at viewtopic.php?t=43777&highlight=paging you'll see that we decided to name it 'PageableDataSource' or something like that... (Personally i found the code in that thread pretty well-designed, so i can only advise to look at it and get inspiration ;))
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

Post by Jenk »

Thanks all for feedback :)

I'll try to answer what I can:

iArrayObject is the interface to a class that is an Array contained within an Object.

This is the interface:

Code: Select all

interface iArrayObject
{
    public function hasItems();
    public function itemCount ();
    public function getRange ($start = 0, $length = null, $keys = false);
    public function getItem ($ind = null);
    public function addItem ($val, $ind = null);
    public function addRange ($arr, $ind = null);
    public function keyExists ($key);
    public function inArray ($val);    	
}
(Some of those can be optional, but I've put them all in regardless.)

Once I have completed the ArrayObject class I use to a satisfactory level, I'll post that too :) Currently am rewotking the class to accept associative arrays, as at the moment it only accepts numerical 0 index arrays :)

The properties are private, as I am a firm believer of setters and getters. All but one property is immutable post instantiation, and thus I like to keep things locked down. :) Even for the property that is not immutable, I like to keep things 'secure' and thus if in future I need to make any changes to the process of assignment for $currentPage, I can do it in setPage(); :)

I've updated the class with a getTotalPages method, and have changed the $start calculation to that suggested. Original post to be updated very shortly :)
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

Post by Jenk »

Oh, and re: assertEqual(s) being backwards - technically, it doesn't matter - however this is the first time I have come to use SimpleTest and I simply followed the lead in these examples :)
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

Hmm... that's interesting... (may need to go rewrite unit tests).
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

Post by Jenk »

edit: updated orignal post
Last edited by Jenk on Tue Aug 22, 2006 7:50 am, edited 1 time in total.
wei
Forum Contributor
Posts: 140
Joined: Wed Jul 12, 2006 12:18 am

Post by wei »

return an iterator from the paginator may be useful
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

Post by Jenk »

that is true, but at the same time I am allowing for flexibility so for those situations where the application(s) require use of an iterator, they can use :

Code: Select all

<?php

$pagedata = new jmt_Paginator($array);
$iterator = new Iterator($pagedata->getPageData());
//etc..

?>
Rather than the class 'forcing' them to use it :)
Post Reply