Iterating in PHP5
The chatter about PHP’s SPL has been picking up recently which is a good thing. SPL has been available for quite some time and a lot of people still don’t know about the awesome features it brings to the table. The problem I have however is that a lot of examples about the iterator interface don’t show off the power that it brings to the table.
For our example we are going to take a look at a simplified version of my ISBNDB wrapper which uses this technique to transparantly page the results received from the webservice.
A lot of the examples in essence show you a different way of iterating over datastructures that you have been iterating over for as long as you’ve been programming. The real power comes in when you add your custom logic to the iterating process.
In this case we query a webservice for information about an author, we like to know all the books he has written. Because this list can be very long the webservice in question, ISBNDB.com, has implemented a paging mechanism. The resulting XML shows us
http://isbndb.com/api/books.xml?access_key=Z&page_number=5&index1=full&value1=sherlock "(...)<BookList total_results="672" page_size="10" page_number="5" shown_results="10">(...)"
Which essentially says:
- I found 672 results
- My page size (in this case immutable) is 10 results per page
- You are seeing page number 1
- On this page there are 10 results (which makes sense if you take into consideration that the final page in this case only has 2 results)
If you tackled the problem of parsing one result into a object (or array) and combining these into one results array you would first retrieve all 68 pages of results, parse each of these into php datastructures and then returning this for further processing. This would use a pretty large amount of memory and above all doesn’t really look pretty.
What we can do with an iterator is retrieve each page only when needed. In this way we only store the 10 results we are currently working on, and if needed, implements easy caching.
A simple implementation of this mechanism is the ISBNDBPaginator class:
class ISBNDBPaginator implements Iterator, Countable
{
/**
* @var ISBNDBServiceCollection The collection from the current page
*/
protected $currentCollection = null;
/**
* @var string The query string used to get a page
*/
protected $queryString = null;
/**
* @var int The total number results found
*/
protected $totalResults = null;
/**
* @var int The total number of pages
*/
protected $totalPages = null;
/**
* @var int The current page id
*/
protected $currentPage = null;
/**
* @var string The result type for the ISBNServiceCollection
*/
protected $resultType = null;
/**
* Creates a new instance of ISBNDBServicePaginator
* @param string $responseXML The response returned from the webservice
* @param string $resultType The result type for the ISBNServiceCollection
* @param string $queryString The querystring used to get the results
*/
public function __construct ($responseXML, $resultType, $queryString)
{
$this->response = DOMDocument::loadXML($responseXML);
$this->resultType= $resultType;
$this->queryString = $queryString;
$this->parsePagination();
}
/**
* This sets up all information needed to make the paginator work
*/
protected function parsePagination()
{
$resultList = $this->response->getElementsByTagName($this->resultType.'List')->item(0);
$this->totalResults = $resultList->getAttribute('total_results');
$this->currentPage = 1;
$this->totalPages = floor($this->totalResults/$resultList->getAttribute('page_size'));
if($this->totalResults%$resultList->getAttribute('page_size'))
{
$this->totalPages = $this->totalPages+1;
}
}
/**
* This sets the current page and loads the associated collection. Allows for lazy loading
* @param int $pageNumber The current page number to retrieve
*/
protected function setCurrentResultPage($pageNumber)
{
if((int)$pageNumber < 1)
{
$pageNumber = 1;
}
$queryString = $this->queryString.'&page_number='.$pageNumber;
$responseXML = file_get_contents($queryString);
$this->currentCollection = new ISBNDBServiceCollection($responseXML, $this->resultType);
}
/**
* Iterator function
* @see reset()
*/
public function rewind()
{
$this->setCurrentResultPage(1);
}
/**
* Iterator function
* @see current()
* @return ISBNDBServiceCollection or false
*/
public function current()
{
if($this->currentPage <= $this->totalPages)
{
return $this->currentCollection;
}
else
{
return false;
}
}
/**
* Iterator function
* @return int the current page
*/
public function key()
{
return key($this->currentPage);
}
/**
* Iterator function
* @see next()
* @retur ISBNDBServiceCollection
*/
public function next()
{
$this->currentPage = $this->currentPage+1;
$this->setCurrentResultPage($this->currentPage);
return $this->currentCollection;
}
/**
* Iterator function
* @see current()
* @return bool
*/
public function valid()
{
return ($this->currentPage <= $this->totalPages);
}
/**
* Counts the number of found pages
* @see count()
* @return int
*/
public function count()
{
return $this->totalPages;
}
/**
* Returns the ISBNDBServiceCollection at the given index
* @param int $index The index to retrieve the collection from
* @return ISBNDBServiceCollection
*/
public function get($index)
{
$this->setCurrentResultPage($index);
return $this->currentCollection;
}
}
?>
This code is used by looping over the paginator item and then working on each resulting collection. In this case a new page is only requested and parsed when we are done with the current one. Thus saving us valuable memory.
I hope this has been an interesting overview of SPL’s iterator interface and that it has given you some insights into it’s uses.
No Comments »
RSS feed for comments on this post. TrackBack URL