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 Code:
<?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;
/** * 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;
/** * 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!'); }
/** * 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); }
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.
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.
__________________
As of PHP 5.5, the MySQL library has been officially deprecated. It is recommended to move to either MySQLi or PDO libraries for your mysql connectivity. See here for help choosing which interface you prefer: http://php.net/manual/en/mysqlinfo.api.choosing.php
As promised I've upgraded this to SQL handling as well.
I'll warn you right off the bat, this is not designed to handle any type of proprietary features to optimize (like MySQL's LIMIT command). Instead, it makes use of you're entire resultset. However, you can likely control you're extensions to handle this if you really wanted to.
I thought I was being sneaky when I created the pagination as a type of Iterable object. Instead, it created more trouble since resultsets are not considered iterable. I've created a wrapper class (which I so creatively called ResultSetWrapper) to convert it to iterable.
ResultSetWrapper takes four parameters, a resultset (either resource or object), a function pointer for counting results (ie: mysql_num_rows), a function pointer for seeking data (ie: mysql_data_seek), and a function pointer for extracting data (ie: mysql_fetch_assoc). These can be whatever you like them to be so long as they provide correct results. Any errors should be muted instead of returned.
I have also created an abstract SQLPagination class which can be extended to handle specific database types. Currently I will only supply MySQL and MySQLi extensions. The only purpose is to shortcut the ResultSetWrapper class so you don't need to explicitly define you're own callback methods.
I've also extended the returned values of the Pagination class adding totalItems and perPage for access.
Example Usage:
PHP Code:
<?php
require_once 'MySQLPagination.class.php';
// $con is an already defined connection resource.
For those who want to see what it looks like before you try it out, I've selected 10,000 records from my random names table, and built a couple of output methods for it. I've attached an image of what that looks like as well.
Remember to leave the copywrite intact.
Let me know if you have any trouble (or are having difficulty extending it into other database types)!
__________________
As of PHP 5.5, the MySQL library has been officially deprecated. It is recommended to move to either MySQLi or PDO libraries for your mysql connectivity. See here for help choosing which interface you prefer: http://php.net/manual/en/mysqlinfo.api.choosing.php
CF is a great programming forum, but Fou-Lu is way beyond anything!
I feel like reading these posts is like taking an online PHP programming course.
You can't imagine how much I've learned from all of the various posts on here.
That's a hint for those newbies learning PHP and MySQL.
CF is a great programming forum, but Fou-Lu is way beyond anything!
I feel like reading these posts is like taking an online PHP programming course.
You can't imagine how much I've learned from all of the various posts on here.
That's a hint for those newbies learning PHP and MySQL.
Lol, I almost deleted you're post when I tried to quote it >.<. Gotta remember theres another button there now :P
I'm good, but I'm not that good. PHP just happens to be my main language, I could do this kind of stuff in maybe Java (maybe, lack of function pointers / delegates would be tough) and C#. Otherwise I'll be stabbing at it for quite awhile lol.
I should really remark about what is returned from the pagination gathering array (which will shed light on why the first example I have uses so many issets). The inline documentation doesn't actually include what is returned as a result of the call.
Code:
HashMap
{
'prevPages' => (int[]) Always exists. Can be empty. Contains all previous pages from current based off of $iShowPages number.
'nextPages' => (int[]) Always exists. Same as above, though next pages.
'currentPage' => (int) Always exists. Current page number
'totalPages' => (int) Always exists. Defines number of pages.
'totalItems' => (int) Always exists. NEW. Contains the total number of items available.
'perPage' => (int) Always exists. NEW. Contains the number of items shown per page.
'firstPage' => (int) Conditional. Only exists if the first page is not visible.
'prevPage' => (int) Conditional. Only exists if you're not on the first page.
'lastPage' => (int) Conditional. Only exists if the last page is not visible.
'nextPage' => (int) Conditional. Only exists if you're not on the last page.
}
Thats the breakdown of whats available in you're callback function for showing you're pagination. My first post has an example of defining this function called 'linkOverview' if you don't know what a callback is.
I've also noticed that the vb here has a jump in its pagination, where it shows pages 1-3 then 13 then the next and last links and so forth. This isn't programmed for that, but can be calculated to show this using the above definitions if you really wanted to.
Oh, and the image is a little deceiving as well. It doesn't show my mouse pointer on it, but thats highlighting the page previous to the current page (which is why the item count doesn't correspond to whats being shown).
Thats it in a nutshell.
Feel free to post any feedback or PM me if you're having trouble writing an extension class or callbacks for this.
__________________
As of PHP 5.5, the MySQL library has been officially deprecated. It is recommended to move to either MySQLi or PDO libraries for your mysql connectivity. See here for help choosing which interface you prefer: http://php.net/manual/en/mysqlinfo.api.choosing.php
Those are callback methods that are required.
The object itself doesn't provide any of the formatting, and instead lets the user choose how they would like to display their data. The logic behind it is once the data is retrieved (for the pagination overview), it then passes the data to you're user defined function for display. From this point, the pagination object no longer cares about any of the data, its yours to deal with. Same for displaying the data, but that one works by calling the display pointer on every iteration of the data. You don't need to actually print out from it either, you can just capture you're data and return it if you desire.
Refer to my first post in the third code block. Scroll near the bottom and you will see two functions defined: dataView and linkOverview (the same two used as the callbacks).
The linkOverview will always take an array (further outlined in my previous post for the values contained in the array). The dataView takes mixed data depending on you're pagination data. It could be an array if you're using MySQLPagination (the default pointer for retrieving the data is mysql_fetch_assoc) so it will be a string indexed array. If its a primitive array it will just be primitive.
Sadly, PHP doesn't actually support function pointers or delegates. These have an advantage in that you can typecast a signature for the function forcing specific parameters and return values.
PM me if you're still having trouble and I'll show you how to handle these callbacks.
__________________
As of PHP 5.5, the MySQL library has been officially deprecated. It is recommended to move to either MySQLi or PDO libraries for your mysql connectivity. See here for help choosing which interface you prefer: http://php.net/manual/en/mysqlinfo.api.choosing.php
Bah, I hate double posting.
I've had a request for implementing limits. I won't write a new class object for these (since they would extend a base level class: MySQLPagination or MySQLiPagination for example), but I'll show you how to do this otherwise.
Essentially, we simply trick the pagination class into thinking we're seeking rows when we are not actually moving the pointer anywhere except back to the start.
$sCount = 'SELECT count(personID) AS pcount FROM Person WHERE personID < 10000';
$sQry = 'SELECT * FROM Person ORDER BY personID WHERE personID < 10000 LIMIT ' .
($iLimitPage * $iPerPage) . ', ' . $iPerPage;
$storage = mysql_connect('localhost', 'root', '');
mysql_select_db('Test');
$obCount = mysql_query($sCount) or die('failed.');
$obCRecord = mysql_fetch_assoc($obCount); // Yeah yeah, no error checking
$iCount = $obCRecord['pcount'];
$obQry = mysql_query($sQry) or die('failed.');
// Here is how we handle the pagination on 'fake' recordsets
$MySQLWrap = new ResultSetWrapper($obQry,
create_function('$obResultSet', 'global $iCount; return $iCount;'),
create_function('$obResultSet, $iSeekTo', 'mysql_data_seek($obResultSet, 0);'),
'mysql_fetch_assoc');
$pagination = new Pagination($MySQLWrap, $iPage, $iPerPage);
The same logic can be applied to any object that is in use, and can be used to create an extension (I'm just too lazy to do both).
Also, a possible bug was pointed out to me regarding the usage of mysql_query($sQry) || die('fail'); always resulting in boolean true. I cannot find any official documentation on the php site that states ||/or die can be used in conjunction with a resultset resource (if anyone can find it, please point me to it so I can file an official bug report with zend). The usage of mysql_query($sQry) or die('fail'); does however work, but without supporting documentation its possible it will fail in the future.
And finally, lots of requests for the pagination functions I've used. So I'll put those up, enjoy my masterful use of inline CSS (and lazy tables :P) lol. Change them to classes to make it easier on yourself.
This will produce the exact same layout as show in the image above. Remember, snap them in as function pointers when you're calling the output methods.
$sDisplayItems = "Show Results %d to %d of " . $aLinks['totalItems'];
if (isset($aLinks['firstPage']))
{
printf($sLinkFormat, $aLinks['firstPage'],
getDisplayRange($aLinks['firstPage'], $aLinks['perPage'], $aLinks['totalItems']),
"« First");
}
if (isset($aLinks['prevPage']))
{
printf($sLinkFormat, $aLinks['prevPage'],
getDisplayRange($aLinks['prevPage'], $aLinks['perPage'], $aLinks['totalItems']),
"<");
}
if (isset($aLinks['prevPages']) && is_array($aLinks['prevPages']))
{
foreach ($aLinks['prevPages'] AS $page)
{
printf($sLinkFormat, $page,
getDisplayRange($page, $aLinks['perPage'], $aLinks['totalItems']),
$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,
getDisplayRange($page, $aLinks['perPage'], $aLinks['totalItems']),
$page);
}
}
if (isset($aLinks['nextPage']))
{
printf($sLinkFormat, $aLinks['nextPage'],
getDisplayRange($aLinks['nextPage'], $aLinks['perPage'], $aLinks['totalItems']),
">");
}
if (isset($aLinks['lastPage']))
{
printf($sLinkFormat, $aLinks['lastPage'],
getDisplayRange($aLinks['lastPage'], $aLinks['perPage'], $aLinks['totalItems']),
"Last »");
}
print "</div>";
}
__________________
As of PHP 5.5, the MySQL library has been officially deprecated. It is recommended to move to either MySQLi or PDO libraries for your mysql connectivity. See here for help choosing which interface you prefer: http://php.net/manual/en/mysqlinfo.api.choosing.php
Also, a possible bug was pointed out to me regarding the usage of mysql_query($sQry) || die('fail'); always resulting in boolean true. I cannot find any official documentation on the php site that states ||/or die can be used in conjunction with a resultset resource (if anyone can find it, please point me to it so I can file an official bug report with zend). The usage of mysql_query($sQry) or die('fail'); does however work, but without supporting documentation its possible it will fail in the future.
both expression, using or/|| are always true, that's the point here and the only difference between this two operators, as far as I know, is only the precedence.
Edit:I guess that pointing out usage of || as bug or posible bug here is based on usage frecvency of this two operators,
No worries mate, I'm about to bump this from 5 months ago to answer you lol (sorry don't know how I missed this O.o).
The answer is simple; the constructor accepts the number to display for the links overview:
Code:
public function __construct(array $aStack, $iPage = self::DPAGE,
$iPerPage = self::DPERPAGE, $iShowPages = self::DSHOWPAGES);
So you can either specify customs or use defaults when constructing:
PHP Code:
new ArrayPagination($collection, $iPage, Pagination::DPERPAGE, 10);
For example.
It's been quite a few years since I've written this (or anything in PHP for that matter), but the above should still hold true in the newer versions of PHP5. I learned a lot about the OO of PHP by doing this, and if I did it again I could write a much better version of the above.