X
Popular Searches

How to Write Your Own Iterable Objects in PHP

PHP Logo

PHP allows you to create iterable objects. These can be used within loops instead of scalar arrays. Iterables are commonly used as object collections. They allow you to typehint that object while retaining support for looping.

Simple Iteration

To iterate over an array in PHP, you use a foreach loop:

foreach (["1", "2", "3"] as $i) {
    echo ($i . " ");
}

This example would emit 1 2 3.

You can also iterate over an object:

$cls = new StdClass();
$cls -> foo = "bar";
foreach ($cls as $i) {
    echo $i;
}

This example would emit bar.

The Collection Problem

For basic classes with public properties, a plain foreach works well. Let’s now consider another class:

class UserCollection {
 
    protected array $items = [];
 
    public function add(UserDomain $user) : void {
        $this -> items[] = $user;
    }
 
    public function containsAnAdmin() : bool {
        return (count(array_filter(
            $this -> items,
            fn (UserDomain $i) : bool => $i -> isAdmin()
        )) > 0);
    }
 
}

This class represents a collection of UserDomain instances. As PHP doesn’t support typed arrays, classes like this are necessary when you want to typehint an array which can only hold one type of value. Collections also help you create utility methods, like containsWithAdmin, that facilitate natural interaction with array items.

Unfortunately, trying to iterate over this collection won’t give the desired results. Ideally, iterating should operate on the $items array, not the class itself.

Implementing Iterator

Natural iteration can be added using the Iterator interface. By implementing Iterator, you can define PHP’s behaviour when instances of your class are used with foreach.

Iterator has five methods which you’ll need to implement:

  • current() : mixed – Get the item at the current position in the iteration.
  • key() : scalar – Get the key at the current position in the iteration.
  • next() : void – Move to the next position in the iteration.
  • rewind() : void – Rewind the position to the start of the iteration.
  • valid() : bool – Get whether the current position has a value.
Advertisement

These methods might be confusing at first. Implementing them is straightforward though – you’re specifying what to do at each stage of a foreach execution.

Each time your object is used with foreach, rewind() will be called. The valid() method is called next, informing PHP whether there’s a value at the current position. If there is, current() and key() are called to get the value and key at that position. Finally, the next() method is called to advance the position pointer. The loop returns to calling valid() to see if there’s another item available.

Here’s a typical implementation of Iterator:

class DemoIterator {
 
    protected int $position = 0;
 
    protected array $items = ["cloud", "savvy"];
 
    public function rewind() : void {
        echo "Rewinding";
        $this -> position = 0;
    }
 
    public function current() : string {
        echo "Current";
        return $this -> items[$this -> position];
    }
 
    public function key() : int {
        echo "Key";
        return $this -> position;
    }
 
    public function next() : void {
        echo "Next";
        ++$this -> position;
    }
 
    public function valid() : void {
        echo "Valid";
        return isset($this -> items[$this -> position]);
    }
 
}

Here’s what would happen when iterating DemoIterator:

$i = new DemoIterator();
foreach ($i as $key => $value) {
    echo "$key $value";
}
 
// EMITS:
// 
// Rewind
// Valid Current Key
// 0 cloud
// Next
// 
// Valid Current Key
// 1 savvy
// Next
// 
// Valid

Your iterator needs to maintain a record of the loop position, check whether there’s an element at the current loop position (via valid()) and return the key and value at the current position.

PHP won’t try to access the key or value when the loop position is invalid. Returning false from valid() immediately terminates the foreach loop. Typically, this will be when you get to the end of the array.

IteratorAggregate

Writing iterators quickly gets repetitive. Most follow the exact recipe shown above. IteratorAggregate is an interface which helps you quickly create iterable objects.

Advertisement

Implement the getIterator() method and return a Traversable (the base interface of Iterator). It will be used as the iterator when your object is used with foreach. This is usually the easiest way to add iteration to a collection class:

class UserCollection implements IteratorAggregate {
 
    protected array $items = [];
 
    public function add(UserDomain $User) : void {
        $this -> items[] = $user;
    }
 
    public function getIterator() : Traversable {
        return new ArrayIterator($this -> items);
    }
 
}
 
$users = new UserCollection();
$users -> add(new UserDomain("James"));
$users -> add(new UserDomain("Demo"));
 
foreach ($users as $user) {
    echo $user -> Name;
}

When working with collections, three lines of code are usually all you need to setup iteration! An ArrayIterator is returned as the Traversable. This is a class which automatically creates an Iterator out of an array. You can now iterate over the logical values within your object, instead of the object’s direct properties.

Using Prebuilt PHP Iterators

You’ll need to write your own iterators if you have complex logic. It’s rarely necessary to start from scratch as PHP ships with several advanced iterators provided by SPL.

The built-in classes include DirectoryIterator, FilesystemIterator, GlobIterator and various recursive iterators. Here’s some of the most useful generic iterators.

LimitIterator

The LimitIterator lets you iterate over a subset of an array. You don’t need to splice the array first or manually keep track of position inside your foreach.

$arr = new ArrayIterator(["a", "b", "c", "d"]);
foreach (new LimitIterator($arr, 0, 2) as $val) {
    echo $val;
}

This example would emit a b. Note that LimitIterator accepts another Iterator, not an array. The example uses ArrayIterator, the built-in class which constructs an Iterator from an array.

InfiniteIterator

InfiniteIterator never terminates the loop, so you’ll need to break from it manually. Otherwise, the iterator automatically wraps back to the start of the array when it reaches the end.

Advertisement

This iterator is particularly useful when working with time-based values. Here’s an easy way of building a three-year calendar, which also uses the LimitIterator described above:

$months = [
    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
];
$infinite = new InfiniteIterator(new ArrayIterator($months));
foreach (new LimitIterator($infinite, 0, 36) as $month) {
    echo $month;
}

This emits three years’ worth of months.

FilterIterator

FilterIterator is an abstract class which you must extend. Implement the accept method to filter out unwanted values which should be skipped during iteration.

class DemoFilterIterator extends FilterIterator {
 
    public function __construct() {
        parent::__construct(new ArrayIterator([1, 10, 4, 6, 3]));
    }
 
    public function accept() {
        return ($this -> getInnerIterator() -> current() < 5);
    }
 
}
 
$demo = new DemoFilterIterator();
 
foreach ($demo as $val) {
    echo $val;
}

This example would emit 1 4 3.

The array values which are higher than 5 are filtered out and do not appear in the foreach.

The “iterable” type

Sometimes you might write a generic function that uses a foreach loop but knows nothing about the values it’ll be iterating over. One example is an abstract error handler which simply dumps the values it receives.

You can typehint iterable in these scenarios. This is a pseudo-type which will accept either an array or any object implementing Traversable:

function handleBadValues(iterable $values) : void {
    foreach ($values as $value) {
        var_dump($value);
    }
}
Advertisement

The iterable type is so vague that you should think carefully before using it. Nonetheless, it can be useful as a last-resort type if you need to guarantee an input value will work with foreach.

Conclusion

Making use of iterators helps you write clean code that’s more modular. You can move methods that act on arrays of objects into dedicated collection classes. These can then be typehinted individually while remaining fully compatible with foreach.

Most of the time, adding iteration support can be achieved by implementing IteratorAggregate and returning an ArrayIterator configured with the items in your collection. PHP’s other iterator types, which tend to go unused by developers, can greatly simplify more specific loops. They offer complex logical behaviours without requiring manual pointer tracking in your foreach statement.

James Walker James Walker
James Walker is a CloudSavvy IT contributor. He is the founder of Heron Web, a UK-based digital agency providing bespoke software development services to SMEs. He has experience managing complete end-to-end web development workflows with DevOps, CI/CD, Docker, and Kubernetes. Read Full Bio »

The above article may contain affiliate links, which help support CloudSavvy IT.