Page 4 of 5
Posted: Tue Feb 14, 2006 5:04 am
by Jenk
Very quick one..
From Tim's Pageable class:
Code: Select all
function getNumItems() {
if (!$this->numitems) {
$this->numitems = $this->getNumItemsExtended();
}
return $this->numitems;
}
If $this->numitems === 0 that will still call $this->getNumItemsExtended();, it appears the criteria should be:
Code: Select all
function getNumItems() {
if ($this->numitems === false) {
$this->numitems = $this->getNumItemsExtended();
}
return $this->numitems;
}

Posted: Tue Feb 14, 2006 11:47 am
by timvw
arborint wrote:I am not sure about having a separate Paginator class, I need to think about whether having a PagerRequest class (like your Paginator) makes sense.
I admit that i looks like overkill right now.. But it would be handy if i added a static array that keeps track of all the paginator instances.. This way i can automagically add/update parameters for other independant paginators (instead of doing it manually now)
arborint wrote:
I think the real differene now probably comes down to whether the code generates the rows or not. I'd rather do as little formatting as possible to give maximum control to the programmer.
It should be pretty simply to remove that dependency on "rows" with data:
- A mysqlpageable implementation could simply return the query or resultset of the query instead of the rows in that resultset...
- A customized pagewriter could then simply fetch (or whatever) from that resultset.. instead of assuming that $pageabledata is an array and iterating over it...
arborint wrote:
Unfortunately we now have two very similar codebases and still no clear requirements -- the PHP way! So much for my group/pair programming plans.

Well, there was at least group thinking... I think there is agreement about the interface requirements.. (naming and implementations may vary)
Posted: Tue Feb 14, 2006 11:48 am
by timvw
Jenk wrote:Very quick one..
(snip code)
Thanks, you're right. At first i had the constructor that set properties to null.. Anyway, problem fixed now.
Posted: Tue Feb 14, 2006 2:35 pm
by Christopher
timvw wrote:I admit that i looks like overkill right now.. But it would be handy if i added a static array that keeps track of all the paginator instances.. This way i can automagically add/update parameters for other independant paginators (instead of doing it manually now)
I think the multiple-list feature is fun, but I have never implemented one (or seen one that I can remember). So making them do it manually does not seem like too much work.
timvw wrote:It should be pretty simply to remove that dependency on "rows" with data:
- A mysqlpageable implementation could simply return the query or resultset of the query instead of the rows in that resultset...
- A customized pagewriter could then simply fetch (or whatever) from that resultset.. instead of assuming that $pageabledata is an array and iterating over it...
Actually, I was talking about formatting the output into HTML. I think that arrays are probably fine as result sets for Pagers never get too large.
timvw wrote:Well, there was at least group thinking... I think there is agreement about the interface requirements.. (naming and implementations may vary)
But neither of our codebases is a good as it could be because only one programmer worked on it. The real advantage of pair/team programming is that it produces better designed code. It goes far beyond additional eyes like Jenk finding a bug or two.
Posted: Tue Feb 14, 2006 3:45 pm
by timvw
arborint wrote:
Actually, I was talking about formatting the output into HTML.
Well, either one simply uses the pageable and pager class... And then does the input -> pager manually... Or gives the paginator a nullwriter...
To me it seemed cleaner to keep the processing of the input ($_GET) outside the pager.. Imho a pager should "page", being:
- set/get the current page,
- set/get number of items per page
- set/get the last page and
- set/get the data in that page
and nothing more.
For someone that wants the "complete package" i've added the paginator that processes the input -> pager and then call the writer->render.
arborint wrote:
But neither of our codebases is a good as it could be because only one programmer worked on it. The real advantage of pair/team programming is that it produces better designed code. It goes far beyond additional eyes like Jenk finding a bug or two.
I'm convinced that pair (or group) thinking usually results in better designed code. I'm not really convinced (also due to the lack of experience with it) that pair code writing results in better code.
Posted: Tue Feb 14, 2006 4:13 pm
by Christopher
timvw wrote:To me it seemed cleaner to keep the processing of the input ($_GET) outside the pager..
I agree.
timvw wrote:I'm convinced that pair (or group) thinking usually results in better designed code. I'm not really convinced (also due to the lack of experience with it) that pair code writing results in better code.
I have found from experience that it almost always prodces a better design and better code. Often startlingly better.
PHP (and similar languages) generate tons duplicate code that is only slightly different in naming and features -- and usually of poor/medium quality.
Posted: Tue Feb 14, 2006 4:54 pm
by timvw
Can't agree more. And i admit that i was, 've been trying to changed that for a while now, living with the following mindset: My own implementation will probably have at least as many bugs and problems as any other... but it are at least my own mistakes.. -> The reason why there are so many php frameworks...
Posted: Wed Feb 15, 2006 5:18 pm
by Christopher
OK, here is my latest code for the Pager following the back and forth with Tim. There are still some more additions I would like to make, but I think they might entail using Sessions -- so I need to think about a clean way to do that.
Code: Select all
class Pager {
var $datasource = null; // Pageable datasource object
var $page_size = 10; // number of rows of data per page
var $range_size = 10; // number of links in pager
var $current_page = 0;
var $first_page = 1;
var $last_page = -1;
var $first_row = 1;
var $last_row = 0;
var $start_row = 0;
var $end_row = 0;
var $page_param = 'page'; // parameter names for URLs
var $last_row_param = 'last_row';
var $page_size_param = 'page_size';
function Pager(&$datasource) {
$this->datasource = &$datasource;
}
function setPageSize($n) {
if ($n > 0) {
$this->page_size = $n;
}
}
function setRangeSize($n) {
if ($n > 2) {
$this->range_size = $n;
}
}
function setPageParameter($name) {
$this->page_param = $name;
}
function setPageSizeParameter($name) {
$this->page_size_param = $name;
}
function setLastRowParameter($name) {
$this->last_row_param = $name;
}
function setCurrentPage($n) {
if ($this->last_row < 0) { // do not access datasource if last_row has been set
$this->last_row = $this->datasource->getNumRows();
}
if ($this->last_row > 0) {
$this->last_page = ceil($this->last_row / $this->page_size);
if ($n < $this->first_page) {
$n = $this->first_page;
}
if ($n > $this->last_page) {
$n = $this->last_page;
}
$this->current_page = $n;
$this->start_row = ($n - 1) * $this->page_size + 1;
$this->end_row = $this->start_row + $this->page_size - 1;
if ($this->end_row > $this->last_row) {
$this->end_row = $this->last_row;
}
} else {
$this->last_page = 0;
$this->current_page = 0;
$this->start_row = 0;
$this->end_row = 0;
}
}
function getPageSize() {
return $this->page_size;
}
function getCurrentPage() {
return $this->current_page;
}
function getLastPage() {
return $this->last_page;
}
function getFirstPage() {
return $this->first_page;
}
function getFirstRow() {
return $this->first_row;
}
function getLastRow() {
return $this->last_row;
}
function getStartRow() {
return $this->start_row;
}
function getEndRow() {
return $this->end_row;
}
function getValues() {
return array(
'page_size' => $this->page_size,
'page_size' => $this->last_page,
'current_page' => $this->current_page,
'start_row' => $this->start_row,
'end_row' => $this->end_row,
'last_row' => $this->last_row,
);
}
function getRange() {
// get number of links each side of current page
$side_size = floor($this->range_size / 2);
$range_start = $this->current_page - $side_size;
$range_end = $range_start + $this->range_size - 1;
// bounds check lower side of range
if ($range_start < $this->first_page) {
$range_start = $this->first_page;
}
if ($range_end > $this->last_page) {
$range_end = $this->last_page;
}
return range($range_start, $range_end);
}
}
class PagerRequest {
var $pager = null;
function PagerRequest(&$pager) {
$this->pager = &$pager;
}
function process() {
$pager = &$this->pager;
if (isset($_GET[$pager->last_row_param])) {
$pager->current_page = intval($_GET[$pager->page_param]);
} else {
$pager->current_page = $pager->first_page;
}
if (isset($_GET[$pager->last_row_param])) {
$pager->last_row = intval($_GET[$pager->last_row_param]);
} else {
$pager->last_row = $pager->datasource->getNumRows();
}
if (isset($_GET[$pager->page_size_param])) {
$pager->page_size = intval($_GET[$pager->page_size_param]);
}
$pager->setCurrentPage($pager->current_page);
}
}
class PagerHTMLWriter {
var $pager = null;
var $no_current_link = true; // no link for current page
var $base_url = ''; // domain and script name part of URL
var $extra_params = array(); // array of parameters that are added to all URLs
function PagerHTMLWriter(&$pager) {
$this->pager = &$pager;
}
function getParameters($n=0) {
$params = $this->extra_params;
$params[$this->pager->last_row_param] = $this->pager->last_row;
$params[$this->pager->page_size_param] = $this->pager->page_size;
if ($n > 0) {
$params[$this->pager->page_param] = $n;
}
return $params;
}
function setExtraParameters($params=array()) {
$this->extra_params = $params;
}
function setBaseURL($url) {
$this->base_url = $url;
}
function getPageURL($n) {
if (($n > 0) && ($n <= $this->pager->last_page)) {
$params = $this->getParameters($n);
foreach ($params as $name => $value) {
$param_strs[$name] = $name . '=' . $value;
}
$url = $this->base_url . '?' . implode('&', $param_strs);
} else {
$url = '';
}
return $url;
}
function getPrevURL() {
if ($this->pager->current_page > $this->pager->first_page) {
$url = $this->getPageURL($this->pager->current_page - 1);
} else {
$url = '';
}
return $url;
}
function getNextURL() {
if ($this->pager->current_page < $this->pager->last_page) {
$url = $this->getPageURL($this->pager->current_page + 1);
} else {
$url = '';
}
return $url;
}
function getFirstURL() {
return $this->getPageURL($this->pager->first_page);
}
function getLastURL() {
return $this->getPageURL($this->pager->last_page);
}
function getRangeURLs() {
foreach ($this->pager->getRange() as $n) {
$urls[$n] = $this->getPageURL($n);
}
return $urls;
}
function getPageSizeURL($size) {
$save_page_size = $this->pager->page_size;
$save_no_current_link = $this->no_current_link;
$this->pager->page_size = $size;
$this->no_current_link = false;
$url = $this->getPageURL($this->pager->current_page);
$this->pager->page_size = $save_page_size;
$this->no_current_link = $save_no_current_link;
return $url;
}
function getPageLink($n, $text='', $attrs='') {
if (($n > 0) && ($n <= $this->pager->last_page)) {
$str = ($text ? $text : $n);
if ($this->no_current_link && ($n ==$this->pager->current_page)) {
$link = $str;
} else {
$link = '<a href="' . $this->getPageURL($n) . "\" $attrs>" . $str . '</a>';
}
} else {
$link = '';
}
return $link;
}
function getPrevLink($text='Prev', $attrs='') {
if ($this->pager->current_page > $this->pager->first_page) {
$url = $this->getPageLink($this->pager->current_page - 1, $text, $attrs);
} else {
$url = '';
}
return $url;
}
function getNextLink($text='Next', $attrs='') {
if ($this->pager->current_page < $this->pager->last_page) {
$url = $this->getPageLink($this->pager->current_page + 1, $text, $attrs);
} else {
$url = '';
}
return $url;
}
function getFirstLink($text='First', $attrs='') {
return $this->getPageLink($this->pager->first_page, $text, $attrs);
}
function getLastLink($text='Last', $attrs='') {
return $this->getPageLink($this->pager->last_page, $text, $attrs);
}
function getRangeLinks($attrs='') {
foreach ($this->pager->getRange() as $n) {
$links[$n] = $this->getPageLink($n, '', $attrs);
}
return $links;
}
function getPageSizeLink($size, $text='', $attrs='') {
if ($size > 0) {
$str = ($text ? $text : $size);
$link = '<a href="' . $this->getPageSizeURL($size) . "\" $attrs>" . $str . '</a>';
} else {
$link = '';
}
return $link;
}
}
And here is an example using it:
Code: Select all
include 'Pager.php';
include 'PageableArray.php';
// initialize an array for testing
for ($i=43; $i<=75; ++$i) {
$myarray[$i] = 'This is row ' . $i;
}
// create a data object that has the interface needed by the Pager object
$datasource = & new PageableArray($myarray);
if ($datasource->getNumRows() > 0) {
// create pager using values from datasource and request params
$pager = & new Pager($datasource);
$pager->setRangeSize(5);
// create a request processor to set pager from GET parameters
$request = & new PagerRequest($pager);
$request->process();
// create a HTML writer to output
$writer = & new PagerHTMLWriter($pager);
// get rows of data
$start_row = $pager->getStartRow();
$end_row = $pager->getEndRow();
$rows = $datasource->getRows($start_row, $end_row);
// display the data
echo '<table border="1">';
$n = $start_row;
foreach ($rows as $value) {
echo '<tr>';
echo '<td>' . $n++ . '.</td><td>' . $value . '</td>';
echo '</tr>';
}
echo '</table>';
// display the paging links
echo $writer->getPrevLink() . ' | ' . implode(' | ', $writer->getRangeLinks()) . ' | ' . $writer->getNextLink() . '<p/>';
} else {
echo 'No records found.';
}
If you want to add a second Pager to a page like Tim showed, you would add something like this:
Code: Select all
// configure first pager here
$pager2 = & new Pager($datasource);
$pager2->setPageParameter('page2');
$pager2->setPageSizeParameter('page_size2');
$pager2->setLastRowParameter('last_row2');
$request->PagerRequest($pager2);
$request->process();
$writer2 = & new PagerHTMLWriter($pager2);
$start_row2 = $pager2->getStartRow();
$end_row2 = $pager2->getEndRow();
// these two lines copy each pager's parameters to the other pager as extra parameters
$writer->setExtraParameters($writer2->getParameters($pager2->getCurrentPage()));
$writer2->setExtraParameters($writer->getParameters($pager->getCurrentPage()));
// show first pager here
$rows = $datasource->getRows($start_row2, $end_row2);
$n = $start_row2;
#echo '<pre>' . print_r($rows, 1) . '</pre>';
echo '<table border="1">';
foreach ($rows as $value) {
echo '<tr>';
echo '<td>' . $n++ . '.</td><td>' . $value . '</td>';
echo '</tr>';
}
echo '</table>';
echo $writer2->getPrevLink() . ' | ' . implode(' | ', $writer2->getRangeLinks()) . ' | ' . $writer2->getNextLink() . '<p/>';
And here is my PagableArray class with a check for no data added:
Code: Select all
class PageableArray {
var $data;
function PageableArray($data) {
$this->data = $data;
}
function getNumRows() {
return count($this->data);
}
function getRows($begin, $end) {
if (count($this->data) > 0) {
--$begin;
return array_slice($this->data, $begin, $end - $begin);
}
}
}
Posted: Wed Feb 15, 2006 5:58 pm
by Jenk
Would it be worth validating/checking $begin and $end are in the $this->data array?
Posted: Wed Feb 15, 2006 6:56 pm
by Christopher
Jenk wrote:Would it be worth validating/checking $begin and $end are in the $this->data array?
You are right. Something like this to show an error?
Code: Select all
function getRows($begin, $end) {
$max = count($this->data);
if (($max > 0) && ($begin > 0) && ($end <= $max)) {
--$begin;
return array_slice($this->data, $begin, $end - $begin);
}
}
Or should begin and end be clipped? I have not gone through the Pagable classes yet as I have been focused on the Pager.
Also Jenk, what do you think of the design in general?
Posted: Mon Feb 20, 2006 11:39 pm
by Christopher
I have not worked on this for a while, but got the ADODB and MySQL Pageable classes working so I thought I would bundle up the code. It is available here:
http://ap3.sourceforge.net/pager-0.2.0.zip
One other thing I wanted to add, because I use it, is to allow the pager to remember where it was and resume. This is very helpful in CRUD type code because the paged list contains a link for each item in the list. When you are done on the sub page for that list item, it is much nicer to return to the list where you left off. Sessions are needed to do that without requiring the sub pages to remember values and pass them back. Unfortunately the logic is messy and the code I wrote just seems terrible to me (and has a terrible name). Maybe someone can find a way to clean up this code:
Code: Select all
class PagerSessionRequest {
var $pager;
var $session_name = 'Pager';
var $page_resume = 'resume';
var $last_row_recalc = 'recalc';
function PagerSessionRequest(&$pager) {
$this->pager = &$pager;
}
function process() {
session_start();
$pager = &$this->pager;
$resume = false;
// code to set the page number
// no 'page=' parameter so initialize pager
if (! isset($_GET[$pager->page_param])) {
$pager->current_page = $pager->first_page;
unset($_SESSION[$this->session_name]); // clear any previous values
$_SESSION[$this->session_name]['page'] = $pager->current_page;
} else {
// 'page=resume' and page number in session so resume
if (($_GET[$pager->page_param] === $this->page_resume) && (isset($_SESSION[$this->session_name]['page']))) {
$pager->current_page = $_SESSION[$this->session_name]['page'];
$resume = true;
} else {
// use 'page=' parameter instead
$pager->current_page = intval($_GET[$pager->page_param]);
$_SESSION[$this->session_name]['page'] = $pager->current_page;
}
}
// code to set the last row value
// no 'last_row=' param and not resuming OR 'last_row=recalc' to force the datasource to recalculate value
if ((! isset($_GET[$pager->last_row_param]) && (! $resume))
|| ((isset($_GET[$pager->last_row_param]) && ($_GET[$pager->last_row_param] === $this->last_row_recalc)))) {
$pager->last_row = $pager->datasource->getNumRows();
$_SESSION[$this->session_name]['last_row'] = $pager->last_row;
} elseif (isset($_SESSION[$this->session_name]['last_row'])) {
// last_row value is in session so use that
$pager->last_row = $_SESSION[$this->session_name]['last_row'];
} else {
// set the value in the session to the value in the pager object
$_SESSION[$this->session_name]['last_row'] = $pager->last_row;
}
// code to set the page size value
// 'page_size=' parameter passed so save in session
if (isset($_GET[$pager->page_size_param])) {
$pager->page_size = intval($_GET[$pager->page_size_param]);
$_SESSION[$this->session_name]['page_size'] = $pager->page_size;
} elseif (isset($_SESSION[$this->session_name]['page_size'])) {
// page size value is in session so use that value
$pager->page_size = $_SESSION[$this->session_name]['page_size'];
} else {
// set the value in the session to the value in the pager object
$_SESSION[$this->session_name]['page_size'] = $pager->page_size;
}
$pager->setCurrentPage($pager->current_page);
}
}
Posted: Wed Feb 22, 2006 11:22 am
by timvw
Don't have the time to review all the code, but the following is one that might bite you
I'd change the process function a little to:
Code: Select all
<?php
function process() {
if (!isset($_SESSION)) session_start();
...
}
?>
Posted: Wed Feb 22, 2006 12:21 pm
by Christopher
Thanks Tim, I'll change that. Can you think of a better name than "PagerSessionRequest" (ugh!). Maybe PagerRequestWithSession ?
I think the only other thing I want to add is some support for changing the sort order by clicking on links (column heads usually). Then a refactoring pass and clean up my unit tests.
Posted: Fri Feb 24, 2006 4:21 am
by Stripe-man
Interesting guys...
Im interested in seeing where this might go...
Posted: Fri Feb 24, 2006 10:20 am
by timvw
arborint wrote:
I think the only other thing I want to add is some support for changing the sort order by clicking on links (column heads usually). Then a refactoring pass and clean up my unit tests.
Well, the ordering of data is one of the first things that i'd factor out of a Pager..
Sorting usually requires you can do the following on your data:
- void Sort($options);
Most implementation that i know simply have $options defined as following, an array with "column" -> "order" elements... eg:
$options = array( array('col1' => 'desc'), array('col2' => 'asc'), ... );
And now the story for a Sorter starts like the Pager story.. (A pattern in patterns... We're at the next level :p)