Programster's Blog

Tutorials focusing on Linux, programming, and open source

PHP - Generators

Generators have been available since PHP 5.5 and the currently minimum supported (security) version of PHP is 5.6 so you should definitely have access to them.

Generators are a simple way to implement iterators which can be used in foreach statements. You may be familiar with the range statement which can be used to generate a list of numbers, such as the code below that will generate an array of numbers as shown below:

print_r( range(0,10) ); // array of numbers 0 -> 10
print_r( range(0,10, 1) ); // array of numbers 0 -> 10
print_r( range(0,10, 3) ); // array of numbers 0, 3, 6, 9 

You could use this instead of a for loop like so:

// print numbers 0 to 100 each on their own line.
foreach (range(0,10) as $number)
{
    print $number . PHP_EOL;
}

// same as above
for ($i=0; $i < 11; $i++)
{
    print $i . PHP_EOL;
}

It is only one step further with a generator. Here is a generator that does the same thing:

function myGenerator($start, $stop) 
{
    for ($i=$start; $i <= $stop; $i++)
    {
        yield $i;
    }
}

foreach (myGenerator(0,10) as $number)
{
    print "myGenerator: " . $number . PHP_EOL;
}

That doesn't look pretty or more concise than a for loop but something like this may be useful in certain situations:

// This may feel more "natural"
$myGenerator = function(){
    for ($i=0; $i <= 10; $i++)
    {
        yield $i;
    }
};


foreach ($myGenerator() as $number)
{
    print '$myGenerator: ' . $number . PHP_EOL;
}

Not Just Numbers!

$fruitGenerator = function(){
    yield "apple";
    yield "pear";
    yeild "pineapple";
};


foreach ($fruitGenerator() as $fruit)
{
    print $fruit . PHP_EOL;
}

Nested Generators

Generators can call other generators like so which will output the times tables. e.g. 0->10, then 0,2,4->20, then 0,3,6->30, etc.

function timesTableGenerator($start, $stop)
{
    for ($i=$start; $i <= $stop; $i++)
    {
        foreach(multiplier(0, 10, $i) as $num)
        {
            yield $num;
        }
    }
}

function multiplier($start, $stop, $factor)
{
    for ($i=$start; $i <= $stop; $i++)
    {
        yield $i * $factor;
    }
}

foreach (timesTableGenerator(1,10) as $number)
{
    print $number . PHP_EOL;
}

The code above is more confusing than if one had just used a nested for loop, but perhaps your codebase wants to use the multiplier generator on its own as well.

Not an Iterator or Traversable Object.

Unfortunately, $myGenerator in the examples above is a closure that does not implement the Iterator or Traversable interfaces and won't work as you might expect without the () as shown below:


$myGenerator = function() {
    for ($i=0; $i <= 5; $i++)
    {
        yield $i;
    }
};

// This won't print anything due to no "()"
foreach ($myGenerator as $number)
{
    print 'number is: ' . $number . PHP_EOL;
}

// This also won't work
class MyClass
{
    
    public function __construct(Traversable $generator)
    {
        foreach ($generator as $number)
        {
             print $number . PHP_EOL;
        }
    }
}

$obj = new MyClass($myGenerator);

Also, generators can't run "business logic" as well, and are only for yielding a result. For example, the following won't work.

$myGenerator = function(){
    for ($i=0; $i <= 10; $i++)
    {
        print "yielding $i" . PHP_EOL;  // this breaks it
        yield $i;
    }
};


foreach ($myGenerator() as $number)
{
    print '$myGenerator: ' . $number . PHP_EOL;
}

What's The Benefit Of Generators?

So what's the point of generators then? Why not just use arrays and the the range function? In a word: memory. Sometimes you may need to loop over a massive set of numbers, in which case storing them in an array in memory may not be practical. You could just skip the pre-population step and go straight to the iterating.