PHP allows you to create iterable objects. These can be used within loops in place of scalar arrays. Iterables are often used as object collections. They allow you to type in that object while retaining looping support.
Simple repetition
To iterate over an array in PHP, use a foreach
loop:
foreach (["1", "2", "3"] as $i) { echo ($i . " "); }
This example would broadcast 1 2 3
You can also repeat an object:
$cls = new StdClass(); $cls -> foo = "bar"; foreach ($cls as $i) { echo $i; }
This example would broadcast bar
The collection problem
For base classes with public properties, a plain foreach
works well. Now let’s take a look at 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
cases. Since PHP does not support typed arrays, classes like these are needed if you want to type an array that can contain only one type of value. Collections also help you create utilities, such as containsWithAdmin
, which facilitate natural interaction with array items.
Unfortunately, trying to repeat this collection will not produce the desired results. Ideally, iteration should work on it $items
array, not the class itself.
Implementing Iterator
Natural iteration can be added with the Iterator
couple. By implementing Iterator
, you can control PHP behavior when using instances of your class foreach
Iterator
has five methods you should 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
– Go to the next position in the iteration.rewind() : void
– Rewind the position to the start of the iteration.valid() : bool
– See if the current position has a value.
These methods can be confusing at first. However, implementing it is simple: you specify what you want at each stage of a foreach
performance.
Every time your item is used foreach
rewind()
will be called. The valid()
The next method is called, which informs PHP if there is a value at the current position. If there is, current()
and key()
are called to get the value and key in that position. finally, the next()
method is called to advance the position pointer. The loop returns to calling valid()
to see if there is another item available.
Here’s a typical implementation of this 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]); } }
This is what would happen on repeat 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 should keep track of the loop position, check if there is an element at the current loop position (via valid()
) and return the key and value at the current position.
PHP will not try to access the key or value if the loop position is invalid. Return false
from valid()
ends it immediately foreach
loop. This is usually the case when you get to the end of the array.
IteratorAggregate
Writing iterators quickly becomes repetitive. Most follow the exact recipe above. IteratorAggregate
is an interface that allows you to quickly create iterable objects.
Implement it getIterator()
method and return a Traversable
(the basic interface of Iterator
It is used as an iterator when your object is used 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 enough to set up iteration! A ArrayIterator
will be returned if the Traversable
This is a class that automatically creates a Iterator
from an array. You can now iterate over the logical values within your object, instead of the direct properties of the object.
Using prebuilt PHP iterators
You have to write your own iterators if you have complex logic. It is rarely necessary to start from scratch as PHP comes with several advanced iterators provided by SPL.
The built-in classes include DirectoryIterator
FilesystemIterator
GlobIterator
and various recursive iterators. Here are some of the most useful generic iterators.
LimitIterator
The LimitIterator
lets you iterate over a subset of an array. You don’t need to split the array or manually position it within your foreach
$arr = new ArrayIterator(["a", "b", "c", "d"]); foreach (new LimitIterator($arr, 0, 2) as $val) { echo $val; }
This example would broadcast a b
Notice that LimitIterator
accepts another Iterator
, not an array. The example uses ArrayIterator
, the built-in class that a Iterator
from an array.
InfiniteIterator
InfiniteIterator
never ends the loop, so you should break
out manually. Otherwise, the iterator will automatically return to the beginning of the array when the end is reached.
This iterator is especially useful when working with time-based values. Here’s an easy way to put together a three-year calendar that also includes 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; }
That yields three years worth of months.
FilterIterator
FilterIterator
is an abstract class that you need to extend. Implement it accept
method to filter out unwanted values to 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 broadcast 1 4 3
The matrix values greater than 5 are filtered out and do not appear in the foreach
The “iterable” type
Sometimes you write a generic function that contains a foreach
loop but knows nothing about the values it will loop. An example is an abstract error handler that simply dumps the received values.
You can type a hint iterable
in these scenarios. This is a pseudo-type that one array
or any object being implemented 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. Nevertheless, it can be useful as a last resort if you want to ensure that an input value works foreach
Conclusion
By using iterators you can write clean code that is modular. You can move methods that work on arrays of objects to special collection classes. These can then be individually typed while remaining fully compatible with foreach
Usually iteration support can be added by implementing IteratorAggregate
and returning a ArrayIterator
configured with the items in your collection. PHP’s other iterator types, not usually used by developers, can greatly simplify more specific loops. They provide complex logical behaviors without manual pointer tracking in your foreach
pronunciation.
Source link