Skip to Content
Course content

124: The Iterator and Generator Interfaces in PHP

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A lot of developers I've mentored usually hit a wall when they first encounter the Iterator interface. They tell me, "Why would I bother implementing five different methods just to loop through some data? I can just return an array and use a foreach loop."

The "Just Return an Array" Fallacy

The problem with returning an array is that you're forcing PHP to load every single element into memory before the loop even starts. If you're dealing with a few dozen database rows, sure, it doesn't matter. But what happens when you're parsing a 500MB log file or streaming a million records from a legacy API?

// The "wrong" way for large datasets
function getLargeLogFile($filename) {
    $lines = file($filename); // This loads the WHOLE file into RAM.
    return $lines; 
}

// If the file is 1GB and your memory limit is 128MB, 
// this crashes before the first iteration of the loop.
foreach (getLargeLogFile('access.log') as $line) {
    echo $line;
}

This is where the Iterator interface comes in. It doesn't give you the data all at once; it gives you a mechanism to fetch the next piece of data only when the loop actually asks for it.

Custom Traversal via the Iterator Interface

To make an object "iterable," you implement the Iterator interface. This requires five methods: current(), key(), next(), rewind(), and valid(). It’s a bit of a chore to write, but it gives you total control over the pointer.

class LogFileIterator implements Iterator {
    private $handle;
    private $currentLine;
    private $position = 0;

    public function __construct($filename) {
        $this->handle = fopen($filename, 'r');
    }

    public function rewind(): void {
        rewind($this->handle);
        $this->position = 0;
    }

    public function current(): mixed {
        return $this->currentLine;
    }

    public function key(): mixed {
        return $this->position;
    }

    public function next(): void {
        $this->currentLine = fgets($this->handle);
        $this->position++;
    }

    public function valid(): bool {
        return !feof($this->handle);
    }
}

Now, when you use this in a foreach, PHP calls these methods internally. The file is read line-by-line. Your memory usage stays flat regardless of whether the log file is 10KB or 10GB. I've seen this single change save production servers from constant Out-of-Memory (OOM) crashes.

Cutting the Boilerplate with Generators

Implementing Iterator is powerful, but let's be honest: writing those five methods every time is tedious. That's why PHP introduced Generators. A Generator is essentially a simplified way to create an Iterator without the class boilerplate, using the yield keyword.

When a function contains yield, it no longer returns a value immediately. Instead, it returns a Generator object. When the loop asks for the next value, the function execution resumes exactly where it left off.

function getLogLinesGenerator($filename) {
    $handle = fopen($filename, 'r');
    try {
        while (($line = fgets($handle)) !== false) {
            yield $line; // The function "pauses" here and returns the value
        }
    } finally {
        fclose($handle);
    }
}

// This looks like a simple array loop, but it's using a Generator under the hood.
foreach (getLogLinesGenerator('access.log') as $line) {
    if (str_contains($line, 'ERROR')) {
        echo $line;
    }
}

The yield keyword handles the current(), next(), and valid() logic for you automatically. It's cleaner, faster to write, and just as memory-efficient. If you find yourself building a custom class just to iterate over a resource, ask yourself if a Generator could do the job in five lines instead of thirty.




πŸ“‹ Practical Task

Exercise: Memory-Efficient User Activity Filter

You are tasked with processing a massive CSV file containing user activity logs. The file is too large to be loaded into an array. You need to create a generator that reads the file and only yields rows where the "action" column (the second column) is equal to 'purchase'.

Requirements:

  • Create a function called filterPurchases($filename).
  • The function must use yield to return rows one by one.
  • It must open the file using fopen() and read it using fgetcsv() to ensure memory efficiency.
  • The generator should only yield the row if the second element of the CSV array is 'purchase'.
  • Ensure the file handle is closed after the loop finishes (hint: use a try...finally block).

Test your code with this logic:

$purchases = filterPurchases('activity_log.csv');
foreach ($purchases as $row) {
    echo "User {$row[0]} made a purchase!\n";
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.