PHP has a history of using functions for iteration, but there is now a push to use objects in PHP 5. The Iterator Pattern is not language specific and doesn't need any language enhancements to create. The added benefit in PHP 5 is that objects are allowed in foreach loops with the SPL Iterator interface.
The Iterator Pattern is not constrained to the class which holds the data and can aggregate the iterator implementation to another object. The aggregated class provides iteration implementation separate from the main class. Aggregation of the Iterator is the best practice for optimizing overhead.
The Iterator Pattern allows you to control how the data is passed to the developer in the loop and what methods they call. The pattern is a standard that creates a smoother transition and easier for the developer. They will know which methods to call for an action without having to look up method name in your reference manual.
When using the Iterator Pattern, you should only collect the information that you need.
The Iterator Pattern allows for accessing the current element and allowing the retrieval of additional elements in a loop routine or manually in a code block.
This function will create an array and return it for testing the Iterators.
function getPageUser($page) { $list = array(); for ($i = 97; $i < 117; $i) { $user = new User(); $user->name = str_repeat(chr($i), 6); $list[] = $user; } return $list; }For the Iterator object, we are going to get the full list and also allow for multiple pages implementation. This object uses mysql queries to get user list and go through the list.
class UserListMysql implements Iterator { protected $query = null; private $row = null; function __construct($page = 1, $limit = 20) { $this->query = mysql_query("SELECT * FROM users ORDER BY username LIMIT ". ($page-1) .", $limit"); } public function rewind() { $this->row = mysql_data_seek($this->query, 0); return true; } public function next() { $this->row = mysql_fetch_assoc($this->query); return $this->row; } public function valid() { if($this->row == false) { mysql_free_result($this->query); return false; } return true; } public function current() { return $this->row; } // Not Used. public function key() { return true; } }To use the Iterator, it easy if you use the foreach loop.
foreach(new UserList(1) as $row) { print_r($row); }There is also an array version for doing a similar action. It will work with any mysql fetched array, or a user created array.
class ArrayPagedList implements Iterator { protected $_page; protected $_limit; protected $_list; private $_at = 1; public function __construct($list, $page = 1, $limit = 20) { if($page == 1) { $this->_list = $list; } else { $offset = $page * $limit; $this->_list = @array_slice($list, $offset, 20); } $this->_page = $page; $this->_limit = $limit; } public function rewind() { $this->_at = 1; return reset($this->_list); } public function next() { $this->_at ; return next($this->_list); } public function valid() { // Test to see if limit has been reached. // Test to see if current exists if(($this->_at > 20) or (current($this->_list) == false)) return false; return true; } public function current() { return current($this->_list); } public function key() { return key($this->_list); } }The execution of the Iterator expected behavior will only work in the foreach loop.
foreach(new ArrayPagedList($array) as $current) { print_r($current); }Most will use the next method in the while loop. With the default behavior, it will continue on ignoring the limit and page rules.
$iterator = new ArrayPagedList($array); while($row = $iterator->next()) { // Do stuff. }The valid method has to be called to stay within the ruleset of the iterator. This is the wanted behavior for those who wish to bypass the ruleset of the iterator, which can't be done in the foreach loop.
Even without using the SPL Iterator, it is possible for objects to be used in the foreach loop. However, the results aren't what you would expect.
class Album { public $title; public $band; public $year; private $songs; public function __construct ($title, $band, $year) { $this -> title = $title; $this -> band = $band; $this -> year = $year; } public function song ($title) { $this -> songs[] = $title; } } $ai = new Album ('Forty Licks', 'The Rolling Stones', 2002); $ai -> song ('Brown Sugar'); $ai -> song ('Street Fighting Man'); $ai -> song ('Rocks Off'); $ai -> song ('Midnight Rambler'); foreach ($ai as $key => $value) { print ($key . ' (' . $value . ')'); }Would display the public variables name and value.
title (Forty Licks) band (The Rolling Stones) year (2002) It isn't desired result to display that for the user. Adding the Iterator Interface gives the expected result.
class AlbumIterator implements Iterator { public $title; public $band; public $year; private $songs; public function __construct ($title, $band, $year) { $this -> title = $title; $this -> band = $band; $this -> year = $year; } public function song ($title) { $this -> songs[] = $title; } public function current () { return current ($this -> songs); } public function key () { return key($this -> songs); } public function valid () { return current ($this -> songs); } public function rewind () { return reset ($this -> songs); } public function next () { return next ($this -> songs); } }Now to test using the Iterator.
$ai = new AlbumIterator ('Forty Licks', 'The Rolling Stones', 2002); $ai -> song ('Brown Sugar'); $ai -> song ('Street Fighting Man'); $ai -> song ('Rocks Off'); $ai -> song ('Midnight Rambler'); foreach ($ai as $id => $song) { print ($song . ' (' . $id . ')<br />'); }Will display the following.
Brown Sugar (0) Street Fighting Man (1) Rocks Off (2) Midnight Rambler (3)
The Iterator pattern is possible in PHP 4, but can't be used in the foreach loop.
class MyIterator { var $data; function MyIterator($data) { $this->data = $data; } function rewind() { return reset($this->data); } function next() { return next($this->data); } function valid() { if(!current($this->data)) { return false; } return true; } function current() { return current($this->data); } function key() { return key($this->data); } }You would use the object in a while loop, calling rewind first.
$iterator = new MyIterator(); $iterator->rewind(); while($iterator->valid()) { $current = $iterator->current(); $key = $iterator->key(); $iterator->next(); }
Iterators don't save you from poor interface methods' implementations and poor optimizations. Using the Iterator Pattern will decrease the speed of the script over the procedural method, but if done right it should be minimal and predictable.
Separation of the iteration and main class functionality adds to the code reuse and speed for which projects can be developed. They are an useful tool for managing data and controlling how a developer has access.