Building Example Pager Class

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

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

Post 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;
	}
:)
timvw
DevNet Master
Posts: 4897
Joined: Mon Jan 19, 2004 11:11 pm
Location: Leuven, Belgium

Post 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)
timvw
DevNet Master
Posts: 4897
Joined: Mon Jan 19, 2004 11:11 pm
Location: Leuven, Belgium

Post 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.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post 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.
(#10850)
timvw
DevNet Master
Posts: 4897
Joined: Mon Jan 19, 2004 11:11 pm
Location: Leuven, Belgium

Post 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.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post 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.
(#10850)
timvw
DevNet Master
Posts: 4897
Joined: Mon Jan 19, 2004 11:11 pm
Location: Leuven, Belgium

Post 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...
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post 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);
        }
	}
}
(#10850)
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

Post by Jenk »

Would it be worth validating/checking $begin and $end are in the $this->data array?
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post 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?
(#10850)
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post 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);

	}

}
(#10850)
timvw
DevNet Master
Posts: 4897
Joined: Mon Jan 19, 2004 11:11 pm
Location: Leuven, Belgium

Post 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();
  ...
}
?>
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post 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.
(#10850)
Stripe-man
Forum Newbie
Posts: 1
Joined: Fri Feb 24, 2006 3:52 am

Post by Stripe-man »

Interesting guys...
Im interested in seeing where this might go...
timvw
DevNet Master
Posts: 4897
Joined: Mon Jan 19, 2004 11:11 pm
Location: Leuven, Belgium

Post 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)
Post Reply