Sophisticated Object Iterators in PHP
In my previous post, Simple Object Iterators in PHP, we discovered how to iterate over array items defined within an object using a foreach loop. However, what if you need to iterate over items which are not stored in an array, e.g. records from a database or lines of text read from a file?
For the following example, we’ll create a class which:
- counts the number of users who are logged in to a web application by retrieving records from a database
- creates a collection of “user” objects which can be iterated with a foreach loop.
Ultimately, we want to be able to run simple code such as:
$loggedin = new LoggedIn();
// count users
echo '<p>', count($loggedin), ' user(s) are currently logged in:</p>';
// iterate over users
foreach ($loggedin as $rec => $user) {
echo
'Record ', $rec,
': ID=', $user->id,
' Email=', $user->email,
'<br/>';
}
PDO recordsets are already traversable so why wrap that functionality within a class?
The benefits are encapsulation and reuse. A developer using the LoggedIn class doesn’t need to worry about implementation details such as database connections or table structures. If user or login details were moved to another system, you could change the LoggedIn class without affecting other code.
In addition, class functionality could be extended without affecting code which uses it, e.g. to fetch further user details, write to a log file, raise security alerts, etc.
First, we’ll define a basic class which stores the ID and email address for a single user. An instance of this class will be returned when we iterate over logged in users:
class User
{
public $id, $email;
}
We now require a LoggedIn class which handles a collection of users who have logged in. Note that the class will implement both the Countable and Iterator interfaces:
class LoggedIn implements Countable, Iterator
{
private $rec; // database recordset
private $cursor; // number of current record
private $item; // current user in the collection
The constructor will immediately call a private FetchLoggedIn() method which runs a database query:
// constructor
public function __construct() {
$this->FetchLoggedIn();
}
// fetch logged in user records
private function FetchLoggedIn() {
$this->cursor = -1;
$this->item = null;
// find logged-in users
$db = new PDO('mysql:host=localhost;dbname=dbname', 'dbuser', 'dbpass');
$this->rec = $db->Prepare(
'SELECT id, email FROM `user` WHERE loggedin=1;',
array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
);
// convert returned records to a User object
$this->rec->setFetchMode(PDO::FETCH_INTO, new User());
// run query
$this->rec->execute();
}
Note:
$this->cursor
is the current record number (zero based). It’s initially set to -1 because we haven’t retrieved any records.$this->item
will hold the current User object.$this->rec
is the set of records (if any) returned from our SQL query.- The
$this->rec->setFetchMode
line sets the default PDO fetch mode. When we fetch a row, a new instance of the User class is returned with the $id and $email properties mapped to the record.
Since our class implements Countable, we must create a public method named count()
which returns the number items in the collection — in this case, the number of rows returned by our SQL query:
public function count() {
return $this->rec->rowCount();
}
We now require 5 public methods which implement Iterator functionality. The first is confusingly named rewind()
. It is called when the foreach loop starts and should “rewind” to the first item in the collection:
public function rewind() {
if ($this->cursor >= 0) $this->FetchLoggedIn();
$this->next();
}
This code checks whether one or more records has already been returned. If it has, we run the SQL query again since we’ve created a forward-only recordset.
The following line calls next()
— our second Iterator method:
public function next() {
$this->cursor++;
$this->item = $this->rec->fetch(PDO::FETCH_ORI_NEXT);
}
This method is called during each iteration of the foreach loop. It increments $this->cursor
, fetches the next row from the recordset, and creates a new User object which is assigned to $this->item
.
The next two Iterator methods are named current()
and key()
which return the current collection item (a User object) and record number respectively:
public function current() {
return $this->item;
}
public function key() {
return $this->cursor;
}
Finally, we require a public valid()
method. This is called immediately after the foreach loop executes rewind()
or next()
— it must return true if an item is available:
public function valid() {
return ($this->cursor < $this->count());
}
// end of class definition
}
Our class is complete and we can count or iterate over logged-in users using the code at the top of this article.
I hope you found this series useful and consider Iterators the next time you create an object which contains a collection of items.