If you have used a for loop in PHP, the idea of iteration is most likely not foreign to you. You pass an array to a for loop, and perform some logic inside the loop, but did you know that you can actually pass data structures other than arrays to a for loop? That's where Iterator comes into play.
Below is summarised definition of an iterator from Wikipedia:
In computer programming, an iterator is an object that enables a programmer to traverse a container, particularly lists.[...] Note that an iterator performs traversal and also gives access to data elements in a container, but does not perform iteration [...]. An iterator is behaviorally similar to a database cursor.
Some key points to remember here:
Now that we know the definition of Iterator, the concept may still be somewhat obscure, but do not worry, we aren't done yet. We have now established that Iterator works similar to array and it can be loop through in a for loop.
It is helpful to understand how array actually works in a for loop. Let's take a look at the code below:
$data = array(1,2,3,4);
for ($i=0; $i<count($data); $i++) {
$key = $i;
$value = $data[$i];
}
Here is how an array works in a for loop:
We can abstract the steps as simple functions as below:
In abstract level, we can imagine that, as long as an object provides the five functions above, it can be loop through by a for loop.
In fact, an iterator is nothing but a class implements all five steps mentioned above. In PHP, The Standard PHP Library(SPL), which is a collection of interfaces and classes that are meant to solve common problems, provides a standard Iterator interface.
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 )
}
Now that we understand what an iterator is, it's time to build our first one.
Our first iterator represents top 10 stared PHP repositories from Github. We can pass it into a foreach and loop through it just like an array. We will name it TrendingRepositoriesIterator.
First, we need to make our class implement the Iterator interface.
class TrendingRepositoriesIterator implements Iterator
{
public function rewind()
{
}
public function valid()
{
}
public function next()
{
}
public function key()
{
}
public function current()
{
}
}
An iterator must always implement the five methods described above. Final code of TrendingRepositoriesIterator is as follow:
class TrendingRepositoriesIterator implements Iterator
{
private $repos = [];
private $pointer = 0;
public function __construct()
{
$this->populate();
}
public function rewind()
{
$this->pointer = 0;
}
public function valid()
{
return isset($this->repos[$this->pointer]);
}
public function next()
{
$this->pointer++;
}
public function key()
{
return $this->pointer;
}
public function current()
{
return $this->repos[$this->pointer];
}
private function populate()
{
$client = new GuzzleHttp\Client();
$res = $client->request('GET', 'https://api.github.com/search/repositories', [ 'query' => ['q' => 'language:php', 'sort' => 'stars', 'order' => 'desc']]);
$resInArray = json_decode($res->getBody(), true);
$trendingRepos = array_slice($resInArray['items'], 0, 10);
foreach ($trendingRepos as $rep) {
$this->repos[] = $rep['name'];
}
}
}
Let's see the use case of TrendingRepositoriesIterator, which can used just like an array:
$trendingRepositoriesIterator = new TrendingRepositoriesIterator();
foreach ($trendingRepositoriesIterator as $repository) {
echo $repository . "\n";
}
// Output
laravel
symfony
CodeIgniter
DesignPatternsPHP
Faker
yii2
composer
WordPress
sage
cakephp
Awesome! Now we have written our first iterator and as you can see, it is actually very easy and straightforward.
You might still wonder why we need to use iterator. Can't we just use array? The answer is yes and no. In most cases, array is sufficient for the job, although iterator does come with some key advantages, which we will share next. Keep in mind, we are by no means suggesting using iterator in all circumstance.
Encapsulation
In our first iterator, TrendingRepositoriesIterator, the details of traversing Github repositories is completed hidden from outside. We can update how we get the data, where we get the data from, and how we want to traverse the resources. No change is needed from the client code. This, known as Encapsulation, is one of the key concepts of Object- Oriented Programming.
Additional examples include:
To iterate through MySQl results, we can use:
$result = mysql_query("SELECT * FROM books");
// Iterate over the structure
while ( $row = mysql_fetch_array($result) ) {
// do stuff
}
To iterator through content of a text file, we can:
$fh = fopen("books.txt", "r");
// Iterate over the structure
while (!feof($fh)) {
$line = fgets($fh);
// do stuff with the line here
}
With iterator, we can encapsulate the process of traversing the recourse so that the outside world is not aware of the internal operations. In fact, the outside world does not need to know where we get the data from or how it is traversed in a loop. All they need to know is that, they can iterate through it as simply as:
$bookIterator = new BookIterator();
foreach($bookIterator as $book) {
// do stuff with $book
}
Encapsulation is a very powerful concept and it enables us to write clean code.
Efficient memory usage
Efficient memory usage is a key benefit of iterator.
In our TrendingRepositoriesIterator class, we can actually fetch resource dynamically, meaning we will only fetch data from Github API when the next() method is called. This technique is called Lazy Loading. It helps us save a very significant amount of memory as value is only generated when it is needed.
Easy to add additional functionalities
Another benefit of using iterator is that we can decorate it to add additional functionalities. Take our TrendingRepositoriesIterator class for example. We want to exclude "laravel" from the resource. One obvious method is to update our original class, although that is of course not what we would do here.
We can decorate the original iterator using SPL's CallbackFilterIterator and no change is needed for TrendingRepositoriesIterator at all.
$trendingRepositoriesIterator = new TrendingRepositoriesIterator();
return $value != 'laravel';
});
foreach ($newTrendingRepositoriesIterator as $repository) {
echo $repository . "\n";
}
// Output
symfony
CodeIgniter
DesignPatternsPHP
Faker
yii2
composer
WordPress
sage
cakephp
The cool part of this is that there is no duplication of objects. The callback fires only when TrendingRepositoriesIterator hits the next() method, and then the logic it will be applied accordingly. This is a great way to save memory as well as boost performance.
Now that we understand the power and benefits of using iterators, it is good practice to use iterators to solve suitable problems. However if we were to write iterators by ourselves whenever we encounter a new problem, it would be very time consuming since it does require us to implement a set of pre-defined functions.
Luckily PHP has done a good job of providing a set of iterators for solving some common problems. In the following sections, we will work through a set of common iterators provided by SPL. As a refresher, SPL standards for Standard PHP Library was built to provide a collection of interfaces and classes that are meant to solve common problems.
In PHP, array is one of the eight primitive types. PHP provides 79 functions for handling array related tasks (reference). It is completely suitable to use array, however there are times, depending on how much you embrace Object-Oriented programming, that you may want to use array as an object. In this case, PHP provides two classes to make array a first class citizen in Object-Oriented code.
ArrayObject
The first option we have is ArrayObject. This class allows objects to work as arrays.
Let's take a look at its class signature:
ArrayObject implements IteratorAggregate , ArrayAccess , Serializable , Countable{
...
public ArrayIterator getIterator ( void )
...
}
As we have seen above, ArrayObject implements IteratorAggregate. What is IteratorAggregate? It is an interface to create an external iterator. In simple terms, it is a quick way to create an iterator, instead of implementing Iterator interfaces with five methods: rewind,valid,current,key and valu, IteratorAggregate allows you to delegate that task to another iterator. All you need to do is implement a single method getIterator().
IteratorAggregate extends Traversable {
abstract public Traversable getIterator ( void )
}
ArrayObject implements IteratorAggregate. It creates an external ArrayIterator for iterator feature.
As ArrayObject implements IteratorAggregate, we can use it in a foreach loop just as an array.
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsArrayObject = new ArrayObject($books);
foreach ($booksAsArrayObject as $book) {
echo $book . "\n";
}
// Output
Head First Design Patterns
Clean Code: A Handbook of Agile Software Craftsmanship
Domain-Driven Design: Tackling Complexity in the Heart of Software
Agile Software Development, Principles, Patterns, and Practices
The primary reason we want to use ArrayObject is to use array in Object-Oriented fashion.
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsArrayObject->append('The Pragmatic Programmer: From Journeyman to Master'); // --- vs ---
$books[] = 'The Pragmatic Programmer: From Journeyman to Master';
ArrayIterator
ArrayIterator works similar to ArrayObject.
Let's take look at its class signature as well:
ArrayIterator implements ArrayAccess , SeekableIterator , Countable , Serializable {
}
It is almost identical to ArrayObject in terms of interfaces they implement. The only difference is, instead of ArrayIterator interface ArrayObject implements, it implements SeekableIterator.
We use ArrayIterator the same way as we use ArrayObject in a foreach loop:
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsArrayIterator = new ArrayIterator($books);
foreach ($booksAsArrayIterator as $book) {
echo $book . "\n";
}
// Output
Head First Design Patterns
Clean Code: A Handbook of Agile Software Craftsmanship
Domain-Driven Design: Tackling Complexity in the Heart of Software
Agile Software Development, Principles, Patterns, and Practices
Use array in Object-Oriented fashion:
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsArrayIterator = new ArrayIterator($books);
$booksAsArrayIterator->append('The Pragmatic Programmer: From Journeyman to Master'); // --- vs ---
$books[] = 'The Pragmatic Programmer: From Journeyman to Master';
Comparison
You may be wondering when to use ArrayObject and when to use ArrayIterator. It is important to know the difference and the relationship between ArrayObject and ArrayIterator.
As we have already discovered in the ArrayObject section, ArrayObject actually creates ArrayIterator as an external iterator. It is fair to say ArrayIterator does what ArrayObject does, and it provides more functionality, specifically seeking to a position. This is accomplished by implementing SeekableIterator.
Besides moving a pointer from top to bottom as iterator, it allows you to randomly jump to a position.
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsArrayIterator = new ArrayIterator($books);
$booksAsArrayIterator->seek(3);
echo $booksAsArrayIterator->current();
// Output
Agile Software Development, Principles, Patterns, and Practices
At last, ArrayIterator is part of SPL whereas ArrayObject is not.
It is a very common task to list outthe content of a given directory. PHP provides lots of functions for handling a file system. One of them is scandir().
Suppose we are given a task to list out all files in a given directory as below:
---books
| ---book_item_1.txt
| ---book_item_2.txt
| ---book_item_3.txt
| ---book_item_4.txt
We can accomplish it through scandir() as shown below:
$books = scandir("books");
foreach($books as $book) {
echo $book . "\n";
}
// Output
.
..
book_item_1.txt
book_item_2.txt
book_item_3.txt
book_item_4.txt
These are two virtual directories("." and "..") you'll find in each directory of the file system.
As this chapter is about iterators, we are going to introduce some iterators for handling filesystem. Hopefully in your next project, you will be able to utilize some of them. Three iterators come in handy: DirectoryIterator, FilesystemIterator and RecursiveDirectoryIterator.
Before we look into each one of them, it is useful to take a look at their inherit relationship:
DirectoryIterator extends SplFileInfo
FilesystemIterator extends DirectoryIterato
RecursiveDirectoryIterator extends FilesystemIterator
DirectoryIterator
The DirectoryIterator class provides a simple interface for viewing the contents of filesystem directories.
To accomplish the same task, we can use DirectoryIterator:
$books = new DirectoryIterator('books');
foreach($books as $book) {
echo $book->getFilename() . "\n";
}
// Output
.
..
book_item_1.txt
book_item_2.txt
book_item_3.txt
book_item_4.txt
The only parameter needed to create a DirectoryIterator object is a directory's path. Compared to scandir function, instead of the file name as a string, DirectoryIterator returns an object. The object holds various information relating to a file, which we can make use.
FilesystemIterator
To accomplish the same task by using FilesystemIterator, we can use:
$books = new FilesystemIterator('books');
foreach($books as $book) {
echo $book->getFilename() . "\n";
}
// Output
book_item_1.txt
book_item_2.txt
book_item_3.txt
book_item_4.txt
This looks almost the same as DirectoryIterator, except that FilesystemIterator has automatically filtered out the two virtual directories.
Are they really the same? We can use a simple method to tell the differences:
$books = new DirectoryIterator('books');
foreach($books as $key=>$value) {
echo $key . ' is a type of '. gettype($key) . "\n";
echo $value . ' is a type of '. get_class($value) . "\n";
}
echo '-------------------------'."\n";
$books = new FilesystemIterator('books');
foreach($books as $key=>$value) {
echo $key . ' is a type of '. gettype($key) . "\n";
echo $value . ' is a type of '. get_class($value) . "\n";
}
The result of running above script from CLI is:
0 is a type of integer
. is a type of DirectoryIterator
1 is a type of integer
.. is a type of DirectoryIterator
2 is a type of integer
book_item_1.txt is a type of DirectoryIterator
3 is a type of integer
book_item_2.txt is a type of DirectoryIterator
4 is a type of integer
book_item_3.txt is a type of DirectoryIterator
5 is a type of integer
book_item_4.txt is a type of DirectoryIterator
--------------------------------
books/book_item_1.txt is a type of string
books/book_item_1.txt is a type of SplFileInfo
books/book_item_2.txt is a type of string
books/book_item_2.txt is a type of SplFileInfo
books/book_item_3.txt is a type of string
books/book_item_3.txt is a type of SplFileInfo
books/book_item_4.txt is a type of string
books/book_item_4.txt is a type of SplFileInfo
Now we can see they are actually quite different internally:
In fact, FilesystemIterator comes with a bit more flexibility. When creating a FilesystemIterator object, it accepts a directory's path as the first parameter similar to DirectoryIterator. Moreover, you can optionally pass a second parameter as a flag. This flag is able to configure various aspects of this function.
FilesystemIterator::CURRENT_AS_PATHNAME: This flag will make FilesystemIterator return file path instead of SplFileInfo object as the value.
FilesystemIterator::CURRENT_AS_FILEINFO: This flag will make FilesystemIterator return SplFileInfo object as the value. This is the default behavior. You don't have to set it explicitly.
FilesystemIterator::CURRENT_AS_SELF: This flag will make FilesystemIterator return FilesystemIterator itself as the value.
FilesystemIterator::KEY_AS_PATHNAME: This flag will make FilesystemIterator return file path as the key. This is the default behavior. You don't have to set it explicitly.
FilesystemIterator::KEY_AS_FILENAME: This flag will make FilesystemIterator return file name and extension instead of file path as the key.
FilesystemIterator::FOLLOW_SYMLINKS: This flag will make RecursiveDirectoryIterator::hasChildren() follow symlinks.
FilesystemIterator::NEW_CURRENT_AND_KEY: This flag helps set two other flags(FilesystemIterator::KEY_AS_FILENAME and FilesystemIterator::CURRENT_AS_FILEINFO) at once.
FilesystemIterator::SKIP_DOTS: This flag will make FilesystemIterator ignore virtual directories ("." and "..").
FilesystemIterator::UNIX_PATHS: This flag will make FilesystemIterator use Unix style directory separator() despite what system the PHP script runs on.
In this section, we will introduce an iterator with the ability of peeking into next element in an iteration. This feature enables us to do a lot useful things such as, executing something different when iterator reaches the end of the list.
The class with this great power is CachingIterator.
Let's first take a look at it class signature, then, we will go into details of its usage.
CachingIterator extends IteratorIterator
CachingIterator inherits from IteratorIterator. What is IteratorIterator? It is simply a wrapper around another iterator, under the hood. It will forward the five Itertator methods( rewind() , current() , key() , valid() , next() ) calls to the iterator it wraps around. We can also retrieve the inner iterator by calling method getInnerIterator() .
Due to the nature of this class, the inner iterator's pointer always moves one step ahead of CachingIterator, and CachingIterator provides a method hasNext() to tell us if it reaches the end of the list. That is how CachingIterator peeks ahead.
Now, let's put it into action.
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsCachingIterator = new CachingIterator(new ArrayIterator($books));
foreach ($booksAsCachingIterator as $book) {
echo 'current book - ' . $book . PHP_EOL;
if ($booksAsCachingIterator->hasNext()) {
echo '----------------------------' . PHP_EOL;
}
}
Result of running above script in a CLI:
current book - Head First Design Patterns
next book - Clean Code: A Handbook of Agile Software Craftsmanship
----------------------------
current book - Clean Code: A Handbook of Agile Software Craftsmanship
next book - Domain-Driven Design: Tackling Complexity in the Heart of Software
----------------------------
current book - Domain-Driven Design: Tackling Complexity in the Heart of Software
next book - Agile Software Development, Principles, Patterns, and Practices
----------------------------
current book - Agile Software Development, Principles, Patterns, and Practices
Similar to other iterators, to create an CachingIterator instance, we pass in an iterator as the first parameter to the class contractor. As we can see, the real magic behind peeking ahead is provided by method hasNext() . This method is able to tell us if there is an immediate next element.
Beside the first parameter, CachingIterator also optionally accepts a second parameter as a flag.
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsCachingIterator = new CachingIterator(new ArrayIterator($books), CachingIterator::TOSTRING_USE_KEY;
foreach ($booksAsCachingIterator as $key=>$book) {
echo $booksAsCachingIterator . PHP_EOL;
}
// Output 0
1
2
3
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsCachingIterator = new CachingIterator(new ArrayIterator($books), CachingIterator::TOSTRING_USE_CURRENT);
foreach ($booksAsCachingIterator as $key=>$book) {
echo $booksAsCachingIterator . PHP_EOL;
}
// Output
Head First Design Patterns
Clean Code: A Handbook of Agile Software Craftsmanship
Domain-Driven Design: Tackling Complexity in the Heart of Software
Agile Software Development, Principles, Patterns, and Practices
You are now convinced by the benefits of iterators. They encapsulate the details of traversing and they are much more efficient than creating in-memory arrays. However, everything has its price. To create an iterator, we still have to implement the SPL Iterator interface. You might be terrified of iterators and not want to implement those five methods contracted by Iterator interface. It is time consuming and sometimes even complex to implement them.
Starting from PHP 5.5, you won't be intimidated any more. PHP introduces something, Generators, which provide an easy way to implement simple iterators without the overhead or complexity of implementing a class that implements the Iterator interface.
What is exactly a generator? A generator is like a normal PHP function, except that it has a special keyword , "yield", in it.
Below is a simple example of a generator function. We won't have such a generator in the real world application - it is here for demonstration only:
function booksGenerator()
{
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
foreach ($books as $book) {
yield $book;
}
}
foreach (booksGenerator() as $book) {
echo $book . PHP_EOL;
}
// Output
Head First Design Patterns
Clean Code: A Handbook of Agile Software Craftsmanship
Domain-Driven Design: Tackling Complexity in the Heart of Software
Agile Software Development, Principles, Patterns, and Practices
Internally PHP realizes a generator function when it spots the yield keyword. When a generator function is called for the first time, PHP creates a Generator object. This Generator object is an instance of an internal class Generator and Generator class implements the Iterator interface. This way, users are able to create iterators without writing the contracted code, all thanks to PHP generator.
The yield is called when we need to provide the step values. Think of it as return in a function or current method in a regular iterator.
Let's turn one of our first iterator class TrendingRepositoriesIterator to a generator function:
function trendingRepositoriesGenerator()
{
$client = new GuzzleHttp\Client();
$res = $client->request('GET', 'https://api.github.com/search/repositories', [ 'query' => ['q' => 'language:php', 'sort' => 'stars', 'order' => 'desc'] ]);
$resInArray = json_decode($res->getBody(), true);
$trendingRepos = array_slice($resInArray['items'], 0, 10);
foreach ($trendingRepos as $rep) {
yield $rep['name'];
};
}
It turns out to be much less code with a generator. We can also use it in a foreach loop, in the same way as we did with TrendingRepositoriesIterator:
foreach (trendingRepositoriesGenerator() as $repo) {
echo $repo . PHP_EOL;
}
Note that generators themselves do not provide anything special - they just make creating iterators simpler. In other words, they are definitely not replacements for iterators.
Hopefully this simple tutorial helped you with your development. If you like our post, please follow us on Twitter and help spread the word. We need your support to continue. Did we miss out anything? Do leave a comment below to let us know.