Page 1 of 1

PHP5 Paginator Class

Posted: Sat Aug 05, 2006 11:01 am
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.

Posted: Sat Aug 05, 2006 1:39 pm
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:

Posted: Sat Aug 05, 2006 4:01 pm
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

Posted: Sat Aug 05, 2006 4:05 pm
by feyd
the offset calculation is

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

Oren's math is correct.

Posted: Sat Aug 05, 2006 4:11 pm
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?

Posted: Sat Aug 05, 2006 4:22 pm
by Ree
It performs them the same way the whole civilized world does ;)

Posted: Sat Aug 05, 2006 4:36 pm
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.

Posted: Sat Aug 05, 2006 4:48 pm
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! ;-)

Posted: Sat Aug 05, 2006 5:22 pm
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 ;))

Posted: Sun Aug 06, 2006 1:59 pm
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 :)

Posted: Sun Aug 06, 2006 3:09 pm
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 :)

Posted: Sun Aug 06, 2006 3:16 pm
by Ambush Commander
Hmm... that's interesting... (may need to go rewrite unit tests).

Posted: Tue Aug 15, 2006 9:47 am
by Jenk
edit: updated orignal post

Posted: Tue Aug 15, 2006 6:41 pm
by wei
return an iterator from the paginator may be useful

Posted: Tue Aug 15, 2006 7:43 pm
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 :)