Iterator for collections in PHP

In this article, I’ll try to explain how to use PHP’s Iterator and IteratorAggregate interfaces to create objects that can act as array in your code, simplify loading and protect your data. We’ll also create an example on how to create “loadable” collection – a collection or array that loads only when it’s needed. But first, we have to create our base classes that will provide the base functions for our Collection class.

Creating the Collection class

First we need to create the iterator for our collection. We have to use Iterator interface provided by PHP, to make sure we define all methods required. Here’s an interface in general:</>

Iterator extends Traversable {
  /* Methods */
  abstract public mixed current ( void )
  abstract public scalar key ( void )
  abstract public void next ( void )
  abstract public void rewind ( void )
  abstract public boolean valid ( void )
}

When we follow this interface, we create our working CollectionIterator class.

class CollectionIterator implements Iterator {
  /**
   * This is our collection class, defined later in article.
   */
  private $Collection = null;
  /**
   * Current index
   */
  private $currentIndex = 0;
  /**
   * Keys in collection
   */
  private $keys = null;

  /**
   * Collection iterator constructor
   *
   */
  public function __construct(Collection $Collection){
    // assign collection
    $this->Collection = $Collection;
    // assign keys from collection
    $this->keys = $Collection->keys();
  }

  /**
   * Implementation of method current
   *
   * This method returns current item in collection based on currentIndex.
   */
  public function current(){
    return $this->Collection->get($this->key());
  }

  /**
   * Get current key
   *
   * This method returns current items' key in collection based on currentIndex.
   */
  public function key(){
    return $this->keys[$this->currentIndex];
  }

  /**
   * Move to next idex
   *
   * This method increases currentIndex by one.
   */
  public function next(){
    ++$this->currentIndex;
  }

  /**
   * Rewind
   *
   * This method resets currentIndex by setting it to 0
   */
  public function rewind(){
    $this->currentIndex = 0;
  }

  /**
   * Check if current index is valid
   *
   * This method checks if current index is valid by checking the keys array.
   */
  public function valid(){
    return isset($this->keys[$this->currentIndex]);
  }
}

Iterator is now defined. As you see in the code, there are few methods that have to be defined in our Collection class for this to be working. Let’s create our Collection class and start play with it. Based on interface IteratorAggregate we need to define the method getIterator to satisfy interface requirements.
In following code we’ll also define two exceptions that you might use. First is ECollectionKeyInUse, which will be thrown, when you try to insert an item in collection with key that already exists and the second one is ECollectionKeyInvalid, which will be thrown, when key you search for cannot be found in collection.

/**
 * Our ECollectionKeyInUse exception
 *
 * Thrown when you try to insert new item with key that already exists in collection.
 */
class ECollectionKeyInUse extends Exception {

  public function __construct($key){
    parent::__construct('Key ' . $key . ' already exists in collection');
  }
}

/**
 * Our ECollectionKeyInvalid exception
 *
 * Exception that will be thrown when you try to get an item with the key
 * that does not exist in collection.
 */
class ECollectionKeyInvalid extends Exception {

  public function __construct($key){
    parent::__construct('Key ' . $key . ' does not exist in collection');
  }
}

/**
 * Our Collection class implementation
 */
class Collection implements IteratorAggregate {

  /**
   * This is our array with data (collection)
   */
  private $data = array();

  /**
   * Get iterator for this collection
   *
   * This method will return <b>CollectionIterator</b> object we wrote before.
   */
  public function getIterator(){
    return new CollectionIterator($this);
  }

  /**
   * Add item to collection
   *
   * This method will add item to collection.
   *
   * If you do now provide the key, key will be next available.
   */
  public function add($item, $key = null){
    if ($key === null){
      // key is null, simply insert new data
      $this->data[] = $item;
    }
    else {
      // key was specified, check if key exists
      if (isset($this->data[$key]))
        throw new ECollectionKeyInUse($key);
      else
        $this->data[$key] = $item;
    }
  }

  /**
   * Get item from collection by key
   *
   */
  public function get($key){
    if (isset($this->data[$key]))
      return $this->data[$key];
    else
      throw new ECollectionKeyInvalid($key);
  }

  /**
   * Remove item from collection
   *
   * This method will remove item from collection.
   */
  public function remove($key){
    // check if key exists
    if (!isset($this->data[$key]))
      throw new ECollectionKeyInvalid($key);
    else
      unset($this->data[$key]);
  }

  /**
   * Get all items in collection
   */
  public function getAll(){
    return $this->data;
  }

  /**
   * Get all the keys in collection
   *
   */
  public function keys(){
    return array_keys($this->data);
  }

  /**
   * Get number of entries in collection
   */
  public function length(){
    return count($this->data);
  }

  /**
   * Clear the collection
   *
   * This method removes all the item from the collection
   */
  public function clear(){
    $this->data = array();
  }

  /**
   * Check if key exists in collection
   */
  public function exists($key){
    return isset($this->data[$key]);
  }
}

Now we have a working Collection class with methods like addremoveclear(), etc.
Simple example script for this Collection class:

/**
 * Example code
 *
 * Creates new collection with three items,display collection
 * remove one and display collection again.
 */

$Collection = new Collection;

$Collection->add('Circle'); // 0
$Collection->add('Square'); // 1
$Collection->add('Line');   // 2

foreach($Collection as $key => $item){
  echo "Item($key): $itemn";
}

// remove square
$Collection->remove(1);

foreach($Collection as $item){
  echo "Item($key): $itemn";
}

When you run this example, you should see the output like this:

# ./collection.php
Item(0): Circle
Item(1): Square
Item(2): Line
Item(0): Circle
Item(1): Line

Creating the loadable Collection class

You might have a scenario, where you have a lot of objects flying around and you don’t really know, which you’ll use and which to load, etc. One way of dealing with that is to simply load all, which might be very memory consuming. A much better solution is to have loadable collection class. Basically that means, when you request an item from collection, or any collection property, the collection will first load it self. Here’s a simple extension to Collection class, which enables this functionality.

/**
 * Simple loadable Collection class.
 */
/**
 * Requires Collection class
 */
require('Collection.class.php');

class LoadableCollection extends Collection {

  /**
   * Callback for loading memebers
   * @var callback
   */
  private $onLoad = null;
  /**
   * Is collection already loaded
   * @var bool
   */
  private $isLoaded = false;

  /**
   * Set load items callback
   *
   * This method is called, if collection was not yet loaded.
   * If you don't set the callback, default load method
   * of object will be used.
   *
   * @param callback $callback
   * @throws BECollectionCallbackInvalid if provided function/method cannot be called
   */
  public function setLoadCallback($callback){
    if (!is_callable($callback, false, $callableName))
      throw new ECollectionCallbackInvalid($callableName . ' is not callable as a parameter to onLoad');
    }
    else {
      $this->onLoad = $callback;
    }
  }

  /**
   * Check load callback
   *
   * This method is called by each method in this class, to see
   * if it needs to call the load callback set by {@link LoadableCollection:setLoadCallback}.
   */
  protected function checkCallback(){
    if (!$this->isLoaded){
      $this->isLoaded = true;
      if ($this->onLoad === NULL){
        /**
         * If no callback was set, set it to $object->load method.
         */
        if (method_exists($this, 'load')){
          $this->onLoad = array($this, 'load');
        }
        else {
          throw new ECollectionCallbackInvalid('No valid callback set and no load() method found');
        }
      }
      call_user_func($this->onLoad, $this);
    }
  }

  /**
   * Override all methods and add callback check.
   */
  /**
   * Add item to collection
   *
   * @param mixed $item
   * @param mixed $key
   */
  public function addItem($item, $key = null){
    $this->checkCallback();
    parent::addItem($item, $key);
  }

  /**
   * Get item
   *
   * @param mixed $key
   */
  public function getItem($key){
    $this->checkCallback();
    return parent::getItem($key);
  }

  /**
   * Get all items
   *
   * @return array
   */
  public function getItems(){
    $this->checkCallback();
    return parent::getItems();
  }

  /**
   * Remove item
   *
   * @param mixed $key
   */
  public function removeItem($key){
    $this->checkCallback();
    parent::removeItem($key);
  }

  /**
   * Check if key exists
   *
   * @param mixed $key
   */
  public function exists($key){
    $this->checkCallback();
    return parent::exists($key);
  }

  /**
   * Get keys
   */
  public function keys(){
    $this->checkCallback();
    return parent::keys();
  }

  /**
   * Get items count
   */
  public function length(){
    $this->checkCallback();
    return parent::length();
  }
  /**
   * Clear the collection
   */
  public function clear(){
    $this->checkCallback();
    return parent::clear();
  }

  /**
   * Unload collection
   */
  public function unload(){
    $this->clear();
    $this->isLoaded = false;
    $this->onLoad = null;
  }
}

Here’s an example class and script to test the functionality.

require('LoadableCollection.class.php');

class MyCars extends LoadableCollection {

  public function load(){
    // load the entire collection
    echo "Loading collection ...n";
    $this->addItem('Audi');
    $this->addItem('BMW');
    $this->addItem('Mercedes');
    echo "Collection loadedn";
  }

}

echo "Creating MyCars objectn";
$c = new MyCars;
echo "MyCars object createdn";
echo "Note that object was not loaded yet.n";

foreach($c as $key => $value){
  echo "$key: $valuen";
}
echo "Done.n";

When you run the script, you should see something like this:

# ./example.php
Creating MyCars object
MyCars object created
Note that the object was not loaded yet
Loading collection ...
Collection loaded ...
0: Audi
1: BMW
2: Mercedes
Done.

That’s it for iterator for collections in PHP. Hope it will help you.


Tags: , , , , ,

 
 
 

By continuing to use the site, you agree to the use of cookies. more information

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this.

Close