Article
Object Oriented PHP: Paging Result Sets
Implementing the Class
My description of the interface presented by objects of the MySQLPagedResultSet class no doubt gave you plenty of clues as to how the class itself will work. In this final section, we'll plough through the code for the class to see what makes it tick. If your only interest is to use the class in a project of yours, however, you can download pagedresults.php here.
class MySQLPagedResultSet
{
var $results;
var $pageSize;
var $page;
var $row;
We start with the list of member variables (a.k.a. properties):
$resultswill be used to store the actual MySQL result set.$pageSizewill store the number of records to be displayed per page.$pagewill keep track of which page of the result set is to be displayed.$rowis used by thefetchArraymethod to track the current row in the result set.
Next up, the constructor:
function MySQLPagedResultSet($query,$pageSize,$cnx)
{
global $resultpage;
As we had decided, the constructor takes three parameters -- the SQL query, the page size (records per page), and the database connection.
The first thing we do in this function is obtain access to the global variable $resultpage. This is the variable that the getPageNav method will insert into the navigation links to indicate which page is to be displayed, so we need to get access to it here to determine whether a page has been specified.
PHP 4.1 NOTE: As of PHP 4.1, the powers that be recommend that theregister_globalsoption inphp.inibe disabled, and that variables be instead accessed through one of the new global arrays:$_GET,$_POST,$_COOKIE,$_SERVER,$_ENV, or$_REQUEST. If you are using PHP 4.1 or later and choose to comply with this recommendation, you can replace this first line with$resultpage = $_GET['resultpage'];.
Next, the constructor executes the query it was given:
$this->results = @mysql_query($query,$cnx);
The results are placed in $this->results -- the member variable we declared above. While we're at it, we initialize $this->pageSize, which will simply be given the value of the $pageSize parameter that was passed to the constructor:
$this->pageSize = $pageSize;
Finally, we need to set the current page. We must first consult the $resultpage variable to see if a particular page was set. If the variable is found to be empty (or erroneously set to a negative number), we set it to 1 so that the first page will be selected by default:
if ((int)$resultpage <= 0) $resultpage = 1;
The (int) forces PHP to cast (i.e. convert) the value in $resultpage to an integer, just in case someone modified the query string and allowed a non-numerical value to slip in.
We also need to be sure the page specified isn't past the end of the result set. For this we use the getNumPages method, which we'll see in a moment:
if ($resultpage > $this->getNumPages())
$resultpage = $this->getNumPages();
Finally, with a valid page number in hand we pass the value to the setPageNum method, which we'll see shortly:
$this->setPageNum($resultpage);
}
The getNumPages method determines the number of pages in the current result set. This method is used internally by other methods of the class, in particular the constructor we have just seen.
function getNumPages()
{
if (!$this->results) return FALSE;
return ceil(mysql_num_rows($this->results) /
(float)$this->pageSize);
}
As you can see, finding the number of pages is a relatively simple calculation. We just divide the number of rows in the result set (mysql_num_rows($this->result)) by the page size ($this->pageSize), and then round up using PHP's ceil function. To ensure that the division produces a fractional value that can be rounded up, we have cast the pageSize property to a floating point number.
The setPageNum method can be used to set the page of the result set that is being viewed. Instances where this method would be useful are fairly rare (try to imagine a case where you'd want to jump to a particular page of the result set but you can't pass, or wish to override, the $resultpage variable), but the constructor also uses it to set the page based on the $resultpage variable. So practically you can think of it as an extension of the constructor.
function setPageNum($pageNum)
{
if ($pageNum > $this->getNumPages() or
$pageNum <= 0) return FALSE;
$this->page = $pageNum;
$this->row = 0;
mysql_data_seek($this->results,($pageNum-1) * $this->pageSize);
}
First the method checks the $pageNum parameter to make sure it's a number within the valid range of pages. If not, it returns false. Next it stores the new page number in $this->page, resets $this->row to zero, and finally uses PHP's mysql_data_seek function to move to the first row of the selected page in the MySQL result set, as calculated from the $pageNum and $this->pageSize.
Next up, getPageNum, which returns the current page number:
function getPageNum()
{
return $this->page;
}
Why a getPageNum method, you ask? After all, if $rs is a MySQLResultSet, you could obtain the page number from $rs->page, right? Well, have a look at the setPage method. As you can see, there's a lot involved in setting a new page number for the result set. If a user of the class decided to set the page by directly modifying $rs->page, none of that stuff would happen, and the object would not behave as expected.
Current thinking on object-oriented design suggests that no property should be directly accessed from outside the class. Rather, methods should be provided to 'get' and 'set' the value of every property that users of the class might need access to. In most object-oriented languages, this can be enforced by making the properties 'private', and thus completely inaccessible to the outside world.
PHP has no such facility for ensuring data privacy, but the principle still applies. Rather than asking a user of the class to remember that, while he or she may read the page number directly, it must always be set using setPageNum, we provide a matching getPageNum method to retrieve the value, and instead do everything we can to discourage accessing the properties directly.
A practical instance where getPageNum might be of use is if you wanted to display "Page X of Y" at the top of your result list. The code for doing so would just be:
<p>Page <?=$rs->getPageNum()?> of <?=$rs->getNumPages()?>.</p>
Wow -- a lot of talk for such a simple method! Fortunately, these next two need little explanation:
function isLastPage()
{
return ($this->page >= $this->getNumPages());
}
function isFirstPage()
{
return ($this->page <= 1);
}
These two functions allow a user of the class to determine if the current page is the first page and/or the last page of the result set. These functions both return either true or false. The code should be pretty self-explanatory.
All that's left are the two real 'workhorse' methods of the class. First, we have fetchArray, which takes the place of mysql_fetch_array for our paged result sets:
function fetchArray()
{
if (!$this->results) return FALSE;
if ($this->row >= $this->pageSize) return FALSE;
$this->row++;
return mysql_fetch_array($this->results);
}
The method returns false both if the result set stored in $this->results is false (which would indicate a failed query), or if the current row number is greater or equal to the page size (which would indicate that the end of the page has been reached). Otherwise, the current row number is incremented and mysql_fetch_array is called to return the next row in the result set.
And finally the 'Big Kahunah', getPageNav:
function getPageNav($queryvars = '')
{
As you can see, the $queryvars parameter is given a default value of '' (the empty string) to make it optional, so you can call getPageNav without parameters if you don't need to pass any variables in the query strings of the generated links.
$nav = '';
if (!$this->isFirstPage())
{
$nav .= "<a href=\"?resultpage=".
($this->getPageNum()-1).'&'.$queryvars.'">Prev</a> ';
}
First off, if the current page is not the first page (which we check using the isFirstPage method) we want to display the 'Prev' link. The URL for the link is a pure query string, since we're just linking straight back to the same page. The query string contains the magic resultpage variable, which will tell the MySQLPagedResultSet object which page to display. We also add any additional query variables (as indicated by the $queryvars parameter) onto the end of the query string.
The output of this function is built up in a variable called $nav, which we'll return at the end, rather than echoing it straight to the output. This makes the method a little more flexible by allowing the user of the class to decide how to handle the output.
The next step is to build the list of pages:
if ($this->getNumPages() > 1)
for ($i=1; $i<=$this->getNumPages(); $i++)
{
if ($i==$this->page)
$nav .= "$i ";
else
$nav .= "<a href=\"?resultpage={$i}&".
$queryvars."\">{$i}</a> ";
}
The if statement ensures that we only bother creating a list if there are more than one. It wouldn't make sense just to have the number '1' displayed at the bottom of the list of results, would it?
We then use a pretty typical for loop to go through the page numbers outputting a linked number for each page, with the exception of the current page (where $i=$this->page), which we print out without a link.
Finally, we print out the 'Next' link if the current page is not the last page (according to isLastPage):
if (!$this->isLastPage())
{
$nav .= "<a href=\"?resultpage=".
($this->getPageNum()+1).'&'.$queryvars.'">Next</a> ';
}
return $nav;
}
}
Writing a class is definitely more time-consuming than writing the bare code to do the same job, but if it's a job you expect to do more than once in your career as a PHP developer, a well designed class like this one can be a real time-saver on future projects!