Categories
PHP

Using Generators and Yield

PHP generators are a powerful feature that allow you to create iterators without writing a class that implements the Iterator interface.

This tutorial covers the following topics:

  1. Generator function as an iterator
  2. Yielding with keys
  3. Reference yield
  4. Return a value
  5. Generator delegation: yield from
  6. Generator class
  7. How to generate sequential unique id with generator function

Any function containing yield is a generator function. A generator is used to generate a series of values that returned with yield statement. The yield statement saves the state of the generator function, allowing it to continue from where it left off when it is called again:

<?php
function getUniqueId() {
 $i = 1;
 while (true) {
  yield $i;
  $i++;
 }
}

$getId = getUniqueId();
echo $getId->current(); //Prints: 1

$getId->next();
echo $getId->current(); //Prints: 2

$getId->next();
echo $getId->current(); //Prints: 3

Generator function as an iterator

We can create iterator objects using generators to save processing time and memory without needing to calculate the entire data set (such as a big array). A generator function can be used with foreach loop. The loop continues until the generator has no more values to yield:

<?php
function Generator() {
 yield 'a';
 yield 'b';
 yield 'c';
 yield 'd';
}

$itr = Generator();
foreach ($itr as $val){
 echo "$val\n";
}

The above example will output:

a
b
c
d

Iterating with keys:

<?php
function Generator() {
 yield 'a';
 yield 'b';
 yield 'c';
 yield 'd';
}

$itr = Generator();
foreach ($itr as $key => $val){
 echo "$key. $val\n";
}

The above example will output:

0. a
1. b
2. c
3. d

Yielding with keys

It is possible to yield key-value pairs similar to associative arrays:

<?php
function Generator() {
 yield 'key1' => 'val 1';
 yield 'key2' => 'val 2';
 yield 'key3' => 'val 3';
}

$gen = Generator();
foreach ($gen as $k => $v) {
 echo "$k => $v\n";
}

The above example will output:

key1 => val 1
key2 => val 2
key3 => val 3

Reference yield

Generator functions are able to yield values by reference as well as by value. To yield a reference from a generator, use the reference operator & in both the function declaration and when assigning the returned value to a variable:

<?php
function &Generator() {
 $val = 1;
 yield $val;
 yield $val;
 yield $val;
}

$gen = Generator();
foreach ($gen as &$v) {
 echo "$v\n";
 $v++;
}

The above example will output:

1
2
3

Return a value

The getReturn() returns the generator’s return value once it has finished executing. Prior to PHP 7, a generator cannot return a value. An empty return statement can be used and this will terminate the generator. PHP 7 made it possible for generators to return expressions:

<?php
function Generator() {
 yield 1;
 yield 'a';
 return true;
}

$itr = Generator();
var_dump ( $itr->getReturn() );

Calling getReturn() when the generator has not yet returned will throw an exception, above example will show the following error:

Fatal error: Uncaught Exception: Cannot get return value of a generator that hasn't returned...

Calling the getReturn() function on the generator, while it is still open (not iterated over), will result in a fatal error.

In the following example, we received the return value after iterating the generator function:

<?php
function Generator() {
 yield 1;
 yield 'a';
 return true;
}

$itr = Generator();

foreach ($itr as $i){
 echo "$i\n";
}

//prints: bool(true)
var_dump ( $itr->getReturn() );

Generator delegation: yield from

The yield from syntax (or generator delegation) allows a generator to yield other generators or arrays as sub-generator:

<?php
function Gen1(){
 yield 'a';
 yield 'b';
}
function Gen2(){
 yield from Gen1();
 yield 'c';
 yield 'd';
}
function Generator(){
 yield from Gen2();
 yield from ['e','f','g'];
}

$itr = Generator();
foreach ($itr as $i){
 echo "$i. ";
}

Above example prints the following output:

a. b. c. d. e. f. g.

Generator class

We can use the following function while working with a generator:

current()
Returns the yielded value.

key()
Returns the key of yielded value.

next()
Resume execution of the generator.

rewind()
Rewind the iterator.

send()
Send a value to generator.

throw()
Throw an exception into the generator.

valid()
Check if the iterator has been closed.

__wakeup()
Serialize callback.

<?php
function Generator(){
 yield 'a';
 yield 'a' => 'apple';
 return 'something';
}

$g = Generator();

while ( $g->valid() ){
 echo $g->key();
 echo $g->current();
 echo "\n";
 $g->next();
}

/*Usage without loop*/
/*echo $g->current(); //a.
echo $g->key(); // 0

var_dump($g->valid()); // bool(true)

$g->next();
echo $g->current(); //apple.
echo $g->key(); //b.


$g->next();
var_dump($g->valid()); // bool(false)

var_dump( $g->current() ); //NULL*/

Generate sequential unique id using generator function

We already created the getUniqueId() generator (the first example on this page). The getUniqueId generator only generates numbers in sequence and you can not change the sequence, for example, if you need to start id from a specific number or an alphabet, then the following generate will not be helpful:

<?php
function getUniqueId() {
 $i = 1;
 while (true) {
  yield $i;
  $i++;
 }
}

$getId = getUniqueId();
echo $getId->current(); //Prints: 1

$getId->next();
echo $getId->current(); //Prints: 2
...
...
...
$getId->next();
echo $getId->current(); //Prints: 100

To solve this problem we’ll use the send() function. The send() function sends the given value to the generator as the result of the current yield expression and resumes execution of the generator. Let’s create the modified generator:

<?php
function getUniqueId() {
 $i = 1;
 while (true) {
  $v = (yield $i);
  if ($v !== null){
   $i = $v;
  }
  else {
   $i++;
  }
 }
}

See the $v = (yield $i) expression in above example. Generator yield the $i and assigned a value to $v if sent by send() function. If the $v is not null then it assign to $i which then yield not next iteration:

$g = getUniqueId();

echo $g->current(); // 1
$g->next();

echo $g->current(); // 2
$g->next();

The above example outputs 1 and 2. Let’s change the sequence using send() function:

$g->send(10); //change the sequence

echo $g->current(); // 10
$g->next();

echo $g->current(); // 11
$g->next();

We send the value 10 to generator which then assigned to $i so we receive the 10 and 11. Let’s start the sequence with an alphabet:

$g->send('a'); //change the sequence

echo $g->current(); // a
$g->next();

echo $g->current(); // b
$g->next();

echo $g->current(); // c
$g->next();

Now the sequence started with an alphabet character a. Following is the complete version of the example:

<?php
function getUniqueId() {
 $i = 1;
 while (true) {
  $v = (yield $i);
  if ($v !== null){
   $i = $v;
  }
  else {
   $i++;
  }
 }
}

$g = getUniqueId();

echo $g->current(); // 1
$g->next();

echo $g->current(); // 2
$g->next();

$g->send(10); //change the sequence

echo $g->current(); // 10
$g->next();

echo $g->current(); // 11
$g->next();

$g->send('a'); //change the sequence

echo $g->current(); // a
$g->next();

echo $g->current(); // b
$g->next();

echo $g->current(); // c
$g->next();

For details, visit http://php.net/manual/class.generator.php


PHP OOP Tutorials: