Fou-Lu
11-02-2008, 08:05 AM
Note:
An updated version with SQLPagination can be downloaded a few posts down. As well, code has been added to provide the visual presentation for the pagination.
I've seen a few posts the last few weeks about pagination. So I decided that I'd write a class to handle this.
This is PHP5 only, please refer to firepages thread here for a php4 pagination class.
Pagination.class.php
<?php
/**
* Create a pagination view from a given collection.
*
* @author Kevin Simpson
* @copyright 2008
* @version 1.0
*/
class Pagination
{
/**
* The default number of items per page.
*/
const DPERPAGE = 10;
/**
* The default page number if less than 1.
*/
const DPAGE = 1;
/**
* The constant defined to list all pages
*/
const ALLPAGES = -1;
/**
* The default number of pages to show. Can be self::ALLPAGES.
*
*/
const DSHOWPAGES = 2;
protected $itorStack;
protected $iPerPage;
protected $iPage;
protected $iShowPages;
protected $iTotalPages;
/**
* Create a new Pagination object
*
* @param Iterator $itorStack An interable collection
* @param int $iPerPage The number of items per page
* @param int $iPage The current page
* @param int $iShowPages The number of pages overview to show
*/
public function __construct(Iterator $itorStack,
$iPage = self::DPAGE, $iPerPage = self::DPERPAGE,
$iShowPages = self::DSHOWPAGES)
{
$iPerPage = (int)$iPerPage;
$iPage = (int)$iPage;
$iShowPages = (int)$iShowPages;
$this->itorStack = $itorStack;
$this->iPerPage = ($iPerPage <= 0) ? 1 : $iPerPage;
$this->iShowPages = ($iShowPages <= 0) ? self::ALLPAGES : $iShowPages;
$iTotalItems = $this->getItemCount();
$this->iTotalPages = ($iTotalItems <= 0) ? 1 :
ceil($iTotalItems / $this->iPerPage);
$iPage = ($iPage > $this->iTotalPages) ? $this->iTotalPages : $iPage;
$this->iPage = ($iPage <= 0) ? 1 : $iPage;
$this->seek();
}
/**
* Get the total number of items in the collection.
* @return int The number of items in the collection.
*/
protected function getItemCount()
{
$iItems = 0;
$this->itorStack->rewind();
if ($this->itorStack instanceof Countable)
{
$iItems = count($this->itorStack);
}
else
{
foreach ($this->itorStack AS $obItems)
{
$iItems++;
}
}
return $iItems;
}
/**
* Change the pointer in the collection to start at the correct item.
*
*/
protected function seek()
{
$iToItem = ($this->iPage - 1) * $this->iPerPage;
$i = 0;
$this->itorStack->rewind();
if ($this->itorStack instanceof SeekableIterator)
{
$this->itorStack->seek($iToItem);
}
else
{
while ($i++ < $iToItem)
{
$this->itorStack->next();
}
}
}
/**
* Display the items using a function pointer.
* Callback must have a signature of mixed fp(mixed).
*
* @param callback $fpCallback The processing function pointer.
*/
public function getItemDisplay($fpCallback)
{
if (!function_exists($fpCallback))
{
throw new Exception('Provided function pointer does not exist!');
}
$aResult = array();
$i = 0;
while ($i++ < $this->iPerPage && $this->itorStack->valid())
{
$obItem = $this->itorStack->current();
$aResult[] = call_user_func($fpCallback, $obItem);
$this->itorStack->next();
}
return $aResult;
}
/**
* Get the page overview as an array.
*
* @return array The page overview array.
*/
public function getPagesAsArray()
{
$iMinPage = 0;
$iMaxPage = 0;
$aPages = array();
$aPrevPages = array();
$aNextPages = array();
$aPages['prevPages'] = &$aPrevPages;
$aPages['nextPages'] = &$aNextPages;
$aPages['currentPage'] = $this->iPage;
$aPages['totalPages'] = $this->iTotalPages;
if ($this->iShowPages == self::ALLPAGES)
{
for ($i = $this->iPage - 1; $i > 0; $i--)
{
array_unshift($aPrevPages, $i);
}
for ($i = $this->iPage + 1; $i <= $this->iTotalPages; $i++)
{
array_push($aNextPages, $i);
}
}
else
{
$iMinPage = $this->iPage - $this->iShowPages - 1;
$iMaxPage = $this->iPage + $this->iShowPages + 1;
if (($this->iPage - $this->iShowPages) > 1)
{
$aPages['firstPage'] = 1;
}
if (($this->iTotalPages - $this->iShowPages) > $this->iPage)
{
$aPages['lastPage'] = $this->iTotalPages;
}
if ($this->iPage > 1)
{
$aPages['prevPage'] = $this->iPage - 1;
}
if ($this->iPage < $this->iTotalPages)
{
$aPages['nextPage'] = $this->iPage + 1;
}
for ($i = $this->iPage - 1; $i >= 1 && $i > $iMinPage; $i--)
{
array_unshift($aPrevPages, $i);
}
for ($i = $this->iPage + 1; $i <= $this->iTotalPages &&
$i < $iMaxPage; $i++)
{
array_push($aNextPages, $i);
}
}
return $aPages;
}
/**
* Display the pages overview using a function pointer.
* Callback must have a signature of: mixed fp(array)
* @param callback $fpCallback The processing function pointer.
*/
public function getPageDisplay($fpCallback)
{
if (!function_exists($fpCallback))
{
throw new Exception('Provided function pointer does not exist!');
}
return call_user_func($fpCallback, $this->getPagesAsArray());
}
}
?>
And an array extension:
ArrayPagination.class.php
<?php
$preArrayPaginationWD = getcwd();
chdir(dirname(__FILE__));
require_once './Pagination.class.php';
chdir($preArrayPaginationWD);
unset($preArrayPaginationWD);
/**
* Extend the functionality of Pagination to apply to an array.
* ArrayObject class must exist.
*
* @author Kevin Simpson
* @copyright 2008
* @version 1.0
*/
class ArrayPagination extends Pagination
{
/**
* @see Pagination::__construct
*/
public function __construct(array $aStack, $iPage = self::DPAGE,
$iPerPage = self::DPERPAGE, $iShowPages = self::DSHOWPAGES)
{
if (!class_exists('ArrayObject'))
{
throw new Exception('ArrayObject class cannot be found!');
}
$aoStack = new ArrayObject($aStack);
parent::__construct($aoStack->getIterator(), $iPage,
$iPerPage, $iShowPages);
}
/**
* @see Pagination::getItemCount
*/
protected function getItemCount()
{
return count($this->itorStack);
}
}
?>
I think that the class comments should be sufficient.
This one has a different approach the the output though, instead of this class handling the formatting, it accepts function pointers (or at least the php equivilent) into the getItemDisplay and getPageDisplay methods. This lets you handle it flexibly with whatever you're anticipated data is.
Here is a quick example of using this with an array.
Array's must go through the ArrayPagination in order to paginate. This is becuase php arrays are not object and are not considered to be of type Iterable. The whole purpose of the ArrayPagination class is to generate an ArrayObject so we can extract an Iterable interface out of it.
<?php
require_once './OO/Pagination/ArrayPagination.class.php';
$items = 121;
$aItems = array();
for ($i = 0; $i < $items; $i++)
{
$aItems[] = $i;
}
$page = 1;
if (isset($_GET['p']))
{
$page = (int)$_GET['p'];
}
$pagination = new ArrayPagination($aItems, $page);
$pagination->getItemDisplay('dataView');
$pagination->getPageDisplay('linkOverview');
function dataView($data)
{
print $data . "<br />\n";
}
function linkOverview(array $aLinks)
{
$sLinkFormat = "<span class=\"pageinationLinks\"><a href=\"?p=%s\">%s</a></span>";
$sNoLinkFormat = "<span class=\"paginationNoLink\">%s</span>";
print "<div>";
if (isset($aLinks['firstPage']))
{
printf($sLinkFormat, $aLinks['firstPage'], "«");
}
if (isset($aLinks['prevPage']))
{
printf($sLinkFormat, $aLinks['prevPage'], "<");
}
if (isset($aLinks['prevPages']) && is_array($aLinks['prevPages']))
{
foreach ($aLinks['prevPages'] AS $page)
{
printf($sLinkFormat, $page, $page);
}
}
if (isset($aLinks['currentPage']))
{
printf($sNoLinkFormat, $aLinks['currentPage']);
}
if (isset($aLinks['nextPages']) && is_array($aLinks['nextPages']))
{
foreach ($aLinks['nextPages'] AS $page)
{
printf($sLinkFormat, $page, $page);
}
}
if (isset($aLinks['nextPage']))
{
printf($sLinkFormat, $aLinks['nextPage'], ">");
}
if (isset($aLinks['lastPage']))
{
printf($sLinkFormat, $aLinks['lastPage'], "»");
}
print "</div>";
}
?>
Oh, and I should mention this as well, so nobody needs to ask.
Yes, its a limited display overview by default. The current configurations will allow you to view 2 pages +/- from the current page given that the pages are available. It can be configured to show all of the pages. If the function pointer for the overview display is correctly configured, it can look like what the vbulletin has.
Hope this helps, I may post an update with an SQL specific pagination class. I have not put extensive testing into this, though I have done bounds testing, so this should be ok.
An updated version with SQLPagination can be downloaded a few posts down. As well, code has been added to provide the visual presentation for the pagination.
I've seen a few posts the last few weeks about pagination. So I decided that I'd write a class to handle this.
This is PHP5 only, please refer to firepages thread here for a php4 pagination class.
Pagination.class.php
<?php
/**
* Create a pagination view from a given collection.
*
* @author Kevin Simpson
* @copyright 2008
* @version 1.0
*/
class Pagination
{
/**
* The default number of items per page.
*/
const DPERPAGE = 10;
/**
* The default page number if less than 1.
*/
const DPAGE = 1;
/**
* The constant defined to list all pages
*/
const ALLPAGES = -1;
/**
* The default number of pages to show. Can be self::ALLPAGES.
*
*/
const DSHOWPAGES = 2;
protected $itorStack;
protected $iPerPage;
protected $iPage;
protected $iShowPages;
protected $iTotalPages;
/**
* Create a new Pagination object
*
* @param Iterator $itorStack An interable collection
* @param int $iPerPage The number of items per page
* @param int $iPage The current page
* @param int $iShowPages The number of pages overview to show
*/
public function __construct(Iterator $itorStack,
$iPage = self::DPAGE, $iPerPage = self::DPERPAGE,
$iShowPages = self::DSHOWPAGES)
{
$iPerPage = (int)$iPerPage;
$iPage = (int)$iPage;
$iShowPages = (int)$iShowPages;
$this->itorStack = $itorStack;
$this->iPerPage = ($iPerPage <= 0) ? 1 : $iPerPage;
$this->iShowPages = ($iShowPages <= 0) ? self::ALLPAGES : $iShowPages;
$iTotalItems = $this->getItemCount();
$this->iTotalPages = ($iTotalItems <= 0) ? 1 :
ceil($iTotalItems / $this->iPerPage);
$iPage = ($iPage > $this->iTotalPages) ? $this->iTotalPages : $iPage;
$this->iPage = ($iPage <= 0) ? 1 : $iPage;
$this->seek();
}
/**
* Get the total number of items in the collection.
* @return int The number of items in the collection.
*/
protected function getItemCount()
{
$iItems = 0;
$this->itorStack->rewind();
if ($this->itorStack instanceof Countable)
{
$iItems = count($this->itorStack);
}
else
{
foreach ($this->itorStack AS $obItems)
{
$iItems++;
}
}
return $iItems;
}
/**
* Change the pointer in the collection to start at the correct item.
*
*/
protected function seek()
{
$iToItem = ($this->iPage - 1) * $this->iPerPage;
$i = 0;
$this->itorStack->rewind();
if ($this->itorStack instanceof SeekableIterator)
{
$this->itorStack->seek($iToItem);
}
else
{
while ($i++ < $iToItem)
{
$this->itorStack->next();
}
}
}
/**
* Display the items using a function pointer.
* Callback must have a signature of mixed fp(mixed).
*
* @param callback $fpCallback The processing function pointer.
*/
public function getItemDisplay($fpCallback)
{
if (!function_exists($fpCallback))
{
throw new Exception('Provided function pointer does not exist!');
}
$aResult = array();
$i = 0;
while ($i++ < $this->iPerPage && $this->itorStack->valid())
{
$obItem = $this->itorStack->current();
$aResult[] = call_user_func($fpCallback, $obItem);
$this->itorStack->next();
}
return $aResult;
}
/**
* Get the page overview as an array.
*
* @return array The page overview array.
*/
public function getPagesAsArray()
{
$iMinPage = 0;
$iMaxPage = 0;
$aPages = array();
$aPrevPages = array();
$aNextPages = array();
$aPages['prevPages'] = &$aPrevPages;
$aPages['nextPages'] = &$aNextPages;
$aPages['currentPage'] = $this->iPage;
$aPages['totalPages'] = $this->iTotalPages;
if ($this->iShowPages == self::ALLPAGES)
{
for ($i = $this->iPage - 1; $i > 0; $i--)
{
array_unshift($aPrevPages, $i);
}
for ($i = $this->iPage + 1; $i <= $this->iTotalPages; $i++)
{
array_push($aNextPages, $i);
}
}
else
{
$iMinPage = $this->iPage - $this->iShowPages - 1;
$iMaxPage = $this->iPage + $this->iShowPages + 1;
if (($this->iPage - $this->iShowPages) > 1)
{
$aPages['firstPage'] = 1;
}
if (($this->iTotalPages - $this->iShowPages) > $this->iPage)
{
$aPages['lastPage'] = $this->iTotalPages;
}
if ($this->iPage > 1)
{
$aPages['prevPage'] = $this->iPage - 1;
}
if ($this->iPage < $this->iTotalPages)
{
$aPages['nextPage'] = $this->iPage + 1;
}
for ($i = $this->iPage - 1; $i >= 1 && $i > $iMinPage; $i--)
{
array_unshift($aPrevPages, $i);
}
for ($i = $this->iPage + 1; $i <= $this->iTotalPages &&
$i < $iMaxPage; $i++)
{
array_push($aNextPages, $i);
}
}
return $aPages;
}
/**
* Display the pages overview using a function pointer.
* Callback must have a signature of: mixed fp(array)
* @param callback $fpCallback The processing function pointer.
*/
public function getPageDisplay($fpCallback)
{
if (!function_exists($fpCallback))
{
throw new Exception('Provided function pointer does not exist!');
}
return call_user_func($fpCallback, $this->getPagesAsArray());
}
}
?>
And an array extension:
ArrayPagination.class.php
<?php
$preArrayPaginationWD = getcwd();
chdir(dirname(__FILE__));
require_once './Pagination.class.php';
chdir($preArrayPaginationWD);
unset($preArrayPaginationWD);
/**
* Extend the functionality of Pagination to apply to an array.
* ArrayObject class must exist.
*
* @author Kevin Simpson
* @copyright 2008
* @version 1.0
*/
class ArrayPagination extends Pagination
{
/**
* @see Pagination::__construct
*/
public function __construct(array $aStack, $iPage = self::DPAGE,
$iPerPage = self::DPERPAGE, $iShowPages = self::DSHOWPAGES)
{
if (!class_exists('ArrayObject'))
{
throw new Exception('ArrayObject class cannot be found!');
}
$aoStack = new ArrayObject($aStack);
parent::__construct($aoStack->getIterator(), $iPage,
$iPerPage, $iShowPages);
}
/**
* @see Pagination::getItemCount
*/
protected function getItemCount()
{
return count($this->itorStack);
}
}
?>
I think that the class comments should be sufficient.
This one has a different approach the the output though, instead of this class handling the formatting, it accepts function pointers (or at least the php equivilent) into the getItemDisplay and getPageDisplay methods. This lets you handle it flexibly with whatever you're anticipated data is.
Here is a quick example of using this with an array.
Array's must go through the ArrayPagination in order to paginate. This is becuase php arrays are not object and are not considered to be of type Iterable. The whole purpose of the ArrayPagination class is to generate an ArrayObject so we can extract an Iterable interface out of it.
<?php
require_once './OO/Pagination/ArrayPagination.class.php';
$items = 121;
$aItems = array();
for ($i = 0; $i < $items; $i++)
{
$aItems[] = $i;
}
$page = 1;
if (isset($_GET['p']))
{
$page = (int)$_GET['p'];
}
$pagination = new ArrayPagination($aItems, $page);
$pagination->getItemDisplay('dataView');
$pagination->getPageDisplay('linkOverview');
function dataView($data)
{
print $data . "<br />\n";
}
function linkOverview(array $aLinks)
{
$sLinkFormat = "<span class=\"pageinationLinks\"><a href=\"?p=%s\">%s</a></span>";
$sNoLinkFormat = "<span class=\"paginationNoLink\">%s</span>";
print "<div>";
if (isset($aLinks['firstPage']))
{
printf($sLinkFormat, $aLinks['firstPage'], "«");
}
if (isset($aLinks['prevPage']))
{
printf($sLinkFormat, $aLinks['prevPage'], "<");
}
if (isset($aLinks['prevPages']) && is_array($aLinks['prevPages']))
{
foreach ($aLinks['prevPages'] AS $page)
{
printf($sLinkFormat, $page, $page);
}
}
if (isset($aLinks['currentPage']))
{
printf($sNoLinkFormat, $aLinks['currentPage']);
}
if (isset($aLinks['nextPages']) && is_array($aLinks['nextPages']))
{
foreach ($aLinks['nextPages'] AS $page)
{
printf($sLinkFormat, $page, $page);
}
}
if (isset($aLinks['nextPage']))
{
printf($sLinkFormat, $aLinks['nextPage'], ">");
}
if (isset($aLinks['lastPage']))
{
printf($sLinkFormat, $aLinks['lastPage'], "»");
}
print "</div>";
}
?>
Oh, and I should mention this as well, so nobody needs to ask.
Yes, its a limited display overview by default. The current configurations will allow you to view 2 pages +/- from the current page given that the pages are available. It can be configured to show all of the pages. If the function pointer for the overview display is correctly configured, it can look like what the vbulletin has.
Hope this helps, I may post an update with an SQL specific pagination class. I have not put extensive testing into this, though I have done bounds testing, so this should be ok.