The getCommentList() method in the User class returns a Db_Iterator instance that holds a result set to the list of comments created by that User. getCommentList() uses Lazy Loading, a method to only load up the comments when necessary (i.e., the first time getCommentList() is called, rather than on object construction). Looking at the Db_Iterator object (with kind thanks to the original implementer, Trevor Andreas), one can see that it stores the result object as a private member variable.
<?php /** * Creates a new Db Iterator object. Allows for fast and memory efficient * handling of a lot of rows that correspond to an object. * @author tandreas <[email protected]> * @author vmc <[email protected]> */ class Db_Iterator implements Iterator { ///< The result set from the database. private $_result = NULL; ///< The object/model that is built on each iteration. private $_object = NULL; ///< The current row. private $_key = 0; /** * The default constructor to build a new Iterator. * @author tandreas <[email protected]> * @param $result The result object. * @param $object An empty object. * @retval Object Returns a new Db_Iterator object. */ public function __construct(Artisan_Db_Result $result, $object) { $this->_result = $result; $this->_object = $object; $this->_key = 0; } /** * Destructs the iterator and frees the result set if its not null. * @author vmc <[email protected]> * @retval NULL Returns NULL. */ public function __destruct() { $this->_object = NULL; if ( NULL !== $this->_result ) { $this->_result->free(); $this->_result = NULL; } } /** * Destructs the iterator and frees the result set if its not null. * @author vmc <[email protected]> * @param $data Sets the data of the object through loadFromArray(). * @retval Object Returns the object for easy write access. */ public function set($data) { if ( true === is_array($data) ) { $this->_object->loadFromArray($data); } return $this->_object; } /** * Returns the current element. * @author tandreas <[email protected]> * @retval Object Returns the current element of the iteration list. */ public function current() { return $this->_load($this->_key); } /** * Returns the last row. * @author tandreas <[email protected]> * @retval Object Returns the last row. */ public function last() { $num_rows = $this->_result->numRows()-1; return $this->_load($num_rows); } /** * Returns the key of the current element. * @author tandreas <[email protected]> * @retval int Returns the integer key of the current element. */ public function key() { return $this->_key; } /** * Moves to the next element. * @author tandreas <[email protected]> * @retval int Returns the next key's value. */ public function next() { $this->_key++; return $this->_key; } /** * Rewinds to the first row of the result. * @author tandreas <[email protected]> * @retval boolean Returns true. */ public function rewind() { $this->_key = 0; $this->_result->row(0); return true; } /** * Determines if the next() or current() calls are valid. * @author tandreas <[email protected]> * @author vmc <[email protected]> * @retval bool Returns true if they are valid, false otherwise. */ public function valid() { return ( $this->_key != $this->_result->numRows() ); } /** * Loads up the specified object during iteration. * @author vmc <[email protected]> * @param $i The key/index to load from. * @retval Object Returns the built object. */ private function _load($i) { $this->_result->row($i); $row = $this->_result->fetch(); if ( true === is_array($row) ) { $this->_object->loadFromArray($row); } return $this->_object; } }
When initially building this, the largest problem faced was the desire to not have each iteration require a new query to load up that object. To us, that defeated the entire reason for the class as it required a database call on each iteration when a single one could be made instead. The decision to add the set() method, and corresponding loadFromArray() method was made. On each iteration, loadFromArray() is called in _load() to set the data from the row to the object. In turn, loadFromArray() identifies the primary key of the object, id, and the rest of the data as the model. This is a very fast way to load up an object without the use of another query.
In addition to loadFromArray(), set() can be used to add new elements to the list. Because set() returns an instance of the object originally passed into the Db_Iterator instance, one can immediately call write() on the returned object to write it to the datastore. Furthermore, if the $comment_data array has a valid id entry, the object will be updated rather than inserted. This creates a very easy interface for adding and updating new objects from a single point of entry.
Testing this implementation is simple and intuitive.
<?php require_once 'Object/LN.php'; require_once 'Object/Model.php'; require_once 'Object/Iterator.php'; require_once 'Object/User.php'; require_once 'Object/Comment.php'; require_once 'Artisan/Db/Adapter/Mysqli.php'; require_once 'Artisan/Config/Array.php'; require_once 'Artisan/Functions/Array.php'; /** * Set up some database configuration data. */ $config_db = new Artisan_Config_Array(array( 'server' => 'localhost', 'username' => 'username', 'password' => 'password', 'database' => 'iterator_test', 'debug' => false ) ); LN::init($config_db); /** * Create a new User object, and load up all of the comments * user ID #1 has made. */ $user = new User(1); $comment_list = $user->getCommentList(); /** * Loop through all of the comments. $comment is an object of type * User_Comment. */ foreach ( $comment_list as $comment ) { echo $comment . '<br>'; } /** * Insert a new comment. $comment_data could easily come from a form submission. * If there was a field name 'id' in the array below, and it had a valid ID, * the comment would be updated rather than inserted when write() is called. */ echo '<br>'; echo 'Adding a new comment....<br>'; $comment_data = array( 'from_id' => 1, 'to_id' => 2, 'comment' => 'thanks for the reply, @joeperry!' ); $comment_list = $user->getCommentList()->set($comment_data)->write(); /** * Force a reload of the comments. true must be passed to the method. */ echo '<br>'; echo 'Reloading the comments....<br><br>'; $comment_list = $user->getCommentList(true); foreach ( $comment_list as $comment ) { echo $comment . '<br>'; } echo '<hr>'; echo round((memory_get_peak_usage()/(1024*1024)), 4) . 'MB'; LN::cleanup();
Using this overall solution begs a question: what happens on the datastore’s end when holding a result set in memory during the page execution. Fortunately, not much. The Db_Iterator class handles the result set variable. PHP uses copy-on-write for its variable’s value, and because the result variable is never written to, the variable that’s passed into the Db_Iterator instance is destroyed by the iterator’s destructor.
The initial implementation is very trivial. Further applications exist for the iterator, such as filtering results and easy pagination. Using these methods, developers can quickly and easily manage their objects from a central object. This technique was used extensively in the Prospect Vista project. Similar to this article, a single User object controls many different iterators of children objects the User object had created. For example, one User type can create: Comments, Contacts, Fans, Payments, Statistics, Statuses, and Videos. With this code, loading up a list of Contacts is as simple as: $user->getContactList().
Iterators are a very powerful and surprisingly simple way to reconfigure your application. One can cut down on the amount of SQL queries executed and the total memory used, and clean up the design of their classes with little effort. The code in this article was adopted from Artisan System, Leftnode’s PHP5 framework.