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.
First published: 16th August 2018