Quick Links
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
-
key() : scalar
-
next() : void
-
rewind() : void
-
valid() : bool
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.
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.
DirectoryIterator
FilesystemIterator
GlobIterator
and various recursive iterators. Here's some of the most useful generic iterators.
LimitIterator
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.
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);
}
}
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.