One of the advanced topics in programming languages is the generator concept which is descendant from iterators and this feature included since PHP 5.5.0.
What is Generators?
Generator is a function that return an array of data, but instead of returning the data using a return statement, generators use a special keyword which is the yield keyword. Keep in mind that generators is a special type of iterators so we can iterate over them using a foreach or while loops.
Let’s describe the generator concept by writing a simple php function to return an array of numbers like this example:
generator.php
function numbers($min, $max) { $ret = []; for($i = $min; $i <= $max; $i++) { $ret[] = $i; } return $ret; }
This function generate an array of numbers from $min to $max value. To use this function we can use foreach like so:
foreach(numbers(1, 100) as $number) { echo $number . "\n"; }
Execute the script in console with:
php generator.php
Now you will see the generated numbers output to the console. Let’s modify the function to be a generator function:
function numbers($min, $max) { for($i = $min; $i <= $max; $i++) { yield $i; } }
Now run the program again, you will see the same result.
So how this works? The secret lies in the yield keyword, the generator function depends upon the yield keyword. When php encounters a yield keyword it detects that this is a generator function. Generator functions return an object of type Generator. This object can be iterated as we will see below.
You can check the type of above function like so:
var_dump(gettype(numbers(1, 10000))); // string(6) "object" var_dump(get_class(numbers(1, 10000))); //string(9) "Generator"
In contrast with the normal return statement, generator functions doesn’t executed until it’s called in a loop, once called via a foreach loop PHP will attempt to call the object’s iteration methods each time it needs a value thereby saving the generator state when yielding a value and then it can be resumed when next value is required.Â
The generator exit execution once there is no more values to be yielded. So we can conclude that yield is superfast compared to return statement as it’s executed only when the calling needs a value.
Because the generator object implements the Iterator interface we can use methods such as current() and next() and valid() to iterate over it:
using while:
$numbers = numbers(1, 100); while($numbers->valid()) { echo $numbers->current() . "\n"; $numbers->next(); }
using foreach:
foreach(numbers(1, 100) as $number) { echo $number . "\n"; }
So what is the benefit of using generators? In fact in this trivial example it makes no sense but what do you expect in the context of the previous example if we want to generate numbers from 1 to 1000000. If we used the return statement in such scenario the result is that the script will consume most of the server’s memory and hang out.Â
But if we used the yield statement it will work smoothly without exhausting server memory because generator functions provide a huge performance boost when working with large array data.
A real world example is to scan all files on a folder that contains a large number of files as shown in this example:
function listFiles($directory) { foreach(glob($directory . "/*.{jpg,gif,png,txt,htm}", GLOB_BRACE) as $filename) { yield $filename; } } echo "<ul>"; foreach(listFiles("/home") as $file) { echo "<li>" . pathinfo($file, PATHINFO_FILENAME) . " - filesize (" . filesize($file) . ") bytes</li>\n"; } echo "</ul>";
Also If you want to include keys while using yield we can use this syntax yield $key => $value like so:
function countryCapitals($data) { foreach($data as $key => $value) { yield $key => $value; } } $data = [ 'Washington' => 'USA', 'London' => 'England', 'Berlin' => 'Germany' ]; foreach(countryCapitals($data) as $key => $value) { echo $key . " -> " . $value . "\n"; }
Using Generator::send() method
Using Generator::send() method we can send a command that can be captured with the yield expression for example we can exit and stop the execution of the generator function by sending a command like in this example:
function numbers($min, $max) { for($i = $min; $i <= $max; $i++) { $command = (yield $i); if($command == 'terminate') { return; } } } $numbers = numbers(1, 10); foreach($numbers as $n) { if($n >= 6) $numbers->send('terminate'); echo $n . "\n"; }
As you see the value we pass into the send() method captured using (yield $i).
Using Generator::getReturn() method
As we mentioned earlier that generators doesn’t return values instead it yields values when invoking them using loops, however if there are a return statement in the generator function we can return it. The Generator class provides the getReturn() method if you want to get the return value of generator once finishes execution:
function numbers($min, $max) { for($i = $min; $i <= $max; $i++) { yield $i; } return 'finished'; } $numbers = numbers(1, 10); foreach($numbers as $n) { echo $n . "\n"; } echo $numbers->getReturn();