I heart foreach

My programming life is not very glamorous. Most of my time is spent in loops, usually working over arrays of objects that are loose wrappers around records stored in a database. Pretty much the bread and butter of all web applications.

Which is how I became fast friends with the foreach control structure in PHP:

$letters = array('a','b','c');

foreach ($letters as $letter) {
  print $letter;
}
//outputs: abc

The corresponding for-loop is ghastly by comparison:

for ($i = 0; $i < count($letters); $i++) {
  print $letters[$i];
}

Because of this, I’ve written a lot code that returns large arrays of objects, only to be iterated over using a foreach. The problem with this method is that each object has to be instantiated in advance and shoved into an array before any work can be done on it. The longer the array of objects, the more memory and time required.

What I needed was a way to use foreach to instantiate a new object at the beginning of each iteration—and then discard it at the end—so that at no point would more than one object exist in memory.

It was looking like I’d have to leave my precious foreach behind (for a while-loop) when I discovered that in PHP5, I can define a class that implements PHP’s internal Iterator interface—giving it the crucial methods that allow a foreach to iterate over an object (rewind, next, current, key, valid), giving me the power to decide when the individual objects in the collection are instantiated.

Meaning my code can continue to use the elegantly readable foreach, but instead of passing it an array of objects, I can pass it a custom Collection object with the ability to instantiate each child only when foreach requests it. Here’s the code:

<?php

class Collection implements Iterator
{
  private $class_name;
  private $rst;
  private $key = -1;
  private $value;
  private $length = false;

  public function __construct($class_name, $sql)
  {
    $this->class_name = $class_name;

    if (strtolower(substr(trim($sql), 0, 6)) == 'select') {
      // this is here for illustrative purposes
      // you probably want to wrap this in a DB class 
      $conn = @mysql_pconnect(DB_SERVER, DB_USER, DB_PASSWORD);
      @mysql_select_db(DB_NAME, $conn);
      $this->rst = @mysql_query($sql, $conn);
      
      $this->rewind();
    } else {
      // throw some kind of error
    }
  }

  public function rewind() 
  {
    if ($this->key != 0) {
      $this->key = 0;
      @mysql_data_seek($this->rst, 0);
      $this->cacheNext();
    }
  }

  private function cacheNext()
  {
    if ($row = mysql_fetch_assoc($this->rst)) {
      $this->value = new $this->class_name($row['id']);
    } else {
      $this->value = false;
    }
  }

  public function current() 
  {
    return $this->value;
  }

  public function key() 
  {
    return $this->key;
  }

  public function next()
  {
    $this->key++;
    $this->cacheNext();
    return $this->current();
  }

  public function valid() 
  {
    return $this->current() !== false;
  }

  public function length()
  {
    if ($this->length === false) {
      $this->length = mysql_num_rows($this->rst);      
    }
    return $this->length;
  }
}

?>

One possible improvement: Each constructor of our model classes accepts an id parameter which is used to load the rest of the fields for that record from the DB. But since the Collection object already has to execute a select query to get the id from the DB, it seems like it might as well grab the rest of the fields for that record at the same time—and then use them to instantiate a child object without an additional database select. What I don’t know is how the performance savings of select * from table compares to the memory savings of select id from table + select * from table where id = $id.

Update: Load object by id + database call or by array?

Feel free to if you found this useful.

1 Comment

Made some significant edits for readability and understandability. Code remains the same.

Care to Comment?

Or if you'd prefer to get in touch privately, please send me an email.

Name

Email (optional)

Blog (optional)