PHP 8.3.4 Released!

Generator syntax

Una función generadora es igual que una función normal, con la diferencia de que en vez de devolver un valor, un generador invoca yield tantas veces como necesita.

Cuando se llama a una función generadora, devuelve un objeto que puede ser iterado. Cuando se itera sobre ese objeto (por ejemplo, con un bucle foreach), PHP llamará a la función generadora cada vez que necesite un valor, y guardará el estado del generador cuando este provea un valor con yield para que ese estado pueda ser recuperado cuando el próximo valor sea requerido.

Cuando no hay más valores que se puedan proporcionar, la función generadora puede simplemente terminar, y el código desde el que se la llama continuará como si un array se hubiera quedado sin valores.

Nota:

Un generador no puede retornar un valor: hacerlo resultaría en un error de compilación. Un return vacío es válido en cuanto a sintaxis dentro de un generador y terminará el generador.

yield keyword

La clave de una función generadora es la palabra reservada yield. En su forma más simple, la sentencia yield es parecida a la sentencia return, excepto en que en vez de detener la ejecución de la función y devolver un valor, yield facilita el valor al bucle que itera sobre el generador y pausa la ejecución de la función generadora.

Ejemplo #1 Ejemplo sencillo de facilitar valores con yield

<?php
function gen_one_to_three() {
for (
$i = 1; $i <= 3; $i++) {
// Observe que $i es preservado entre yields
yield $i;
}
}

$generator = gen_one_to_three();
foreach (
$generator as $value) {
echo
"$value\n";
}
?>

El resultado del ejemplo sería:

1
2
3

Nota:

Internamente, las claves enteras secuenciales serán asociadas con los valores sobre los que se usa yield, como un array no asociativo.

Precaución

Si se utiliza yield en el contexto de una expresión (por ejemplo, en el lado derecho de una asignación), se debe poner la sentencia yield entre paréntesis en PHP 5. Por ejemplo, esto es válido:

$data = (yield $value);

Pero esto no lo es, y resultará en un error de análisis en PHP 5:

$data = yield $value;

Las restricciones parentéticas no se aplican en PHP 7.

Esta sintaxis podría usarse junto con el método Generator::send().

Utilizar yield para facilitar valores con claves

PHP soporta arrays asociativos, y los generadores no son menos. Además de facilitar valores simples, como se muestra arriba, también se puede facilitar una clave al mismo tiempo.

La sintaxis para facilitar un par clave-valor es muy similar a la utilizada para definir un array asociativo, como se muestra a continuación.

Ejemplo #2 Facilitar un par clave-valor

<?php
/*
* La entrada son campos separados por punto y coma, con el primer
* campo siendo la ID utilizada como clave.
*/

$input = <<<'EOF'
1;PHP;Likes dollar signs
2;Python;Likes whitespace
3;Ruby;Likes blocks
EOF;

function
input_parser($input) {
foreach (
explode("\n", $input) as $line) {
$fields = explode(';', $line);
$id = array_shift($fields);

yield
$id => $fields;
}
}

foreach (
input_parser($input) as $id => $fields) {
echo
"$id:\n";
echo
" $fields[0]\n";
echo
" $fields[1]\n";
}
?>

El resultado del ejemplo sería:

1:
    PHP
    Likes dollar signs
2:
    Python
    Likes whitespace
3:
    Ruby
    Likes blocks
Precaución

Como en el ejemplo anterior, facilitar un par clave-valor en contexto de expresión requiere que la sentencia yield sea puesta entre paréntesis:

$data = (yield $key => $value);

Facilitar valores nulos

Yield puede ser invocado sin argumentos para facilitar un valor null con una clave automática.

Ejemplo #3 Yielding nulls

<?php
function gen_three_nulls() {
foreach (
range(1, 3) as $i) {
yield;
}
}

var_dump(iterator_to_array(gen_three_nulls()));
?>

El resultado del ejemplo sería:

array(3) {
  [0]=>
  NULL
  [1]=>
  NULL
  [2]=>
  NULL
}

Facilitar por referencia

Las funciones generadoras son capaces de facilitar valores por referencia igual que lo hacen por valor. Esto se hace de la misma forma que devolviendo referencias desde funciones: poniendo un ampersand (signo &) delante del nombre de la función.

Ejemplo #4 Facilitar valores por referencia

<?php
function &gen_reference() {
$value = 3;

while (
$value > 0) {
yield
$value;
}
}

/*
* Observe que es posible cambiar $number desde dentro del bucle, y
* dado que el generador está facilitando referencias, $value
* dentro de gen_reference() cambia.
*/
foreach (gen_reference() as &$number) {
echo (--
$number).'... ';
}
?>

El resultado del ejemplo sería:

2... 1... 0... 

Delegación de generadores mediante yield from

En PHP 7, la delegación de generadores permite producir valores desde otro generador, objeto Traversable, o array mediante la palabra reservada yield from. El generador externo producirá entonces todos los valores desde el generador interno, object, o array hasta que este ya no sea válido, después de lo cual la ejecuión continuará en el generador externo.

Si un generador se emplea con yield from, la expresión yield from también devolverá cualquier valor devuelto por el generador interno.

Precaución

Almacenamiento en un array (p.ej. con iterator_to_array())

yield from no reinicia las claves. Preserva las claves devueltas por el objeto Traversable o array. Por tanto, algunos valores podrían compartir una clave en común con otro yield o yield from, los cuales, en el momento de la inserción en el array, sobrescribirán los valores antiguos con esa clave.

Un caso común donde esto importa es cuando iterator_to_array() devuelve un array con claves por defecto, conduciendo a posibles resultados inesperados. iterator_to_array() tiene un segundo parámetro, use_keys, que puede ser establecido a false para recoger todos los valores mientras ignora las claves devueltas por el Generator.

Ejemplo #5 yield from con iterator_to_array()

<?php
function from() {
yield
1; // clave 0
yield 2; // clave 1
yield 3; // clave 2
}
function
gen() {
yield
0; // clave 0
yield from from(); // claves 0-2
yield 4; // clave 1
}
// pasar false como segundo parámetro para obtener un array [0, 1, 2, 3, 4]
var_dump(iterator_to_array(gen()));
?>

El resultado del ejemplo sería:

array(3) {
  [0]=>
  int(1)
  [1]=>
  int(4)
  [2]=>
  int(3)
}

Ejemplo #6 Uso básico de yield from

<?php
function contar_hasta_diez() {
yield
1;
yield
2;
yield from [
3, 4];
yield from new
ArrayIterator([5, 6]);
yield from
siete_ocho();
yield
9;
yield
10;
}

function
siete_ocho() {
yield
7;
yield from
ocho();
}

function
ocho() {
yield
8;
}

foreach (
contar_hasta_diez() as $num) {
echo
"$num ";
}
?>

El resultado del ejemplo sería:

1 2 3 4 5 6 7 8 9 10 

Ejemplo #7 yield from y valores devueltos

<?php
function contar_hasta_diez() {
yield
1;
yield
2;
yield from [
3, 4];
yield from new
ArrayIterator([5, 6]);
yield from
siete_ocho();
return yield from
nueve_diez();
}

function
siete_ocho() {
yield
7;
yield from
ocho();
}

function
ocho() {
yield
8;
}

function
nueve_diez() {
yield
9;
return
10;
}

$gen = contar_hasta_diez();
foreach (
$gen as $num) {
echo
"$num ";
}
echo
$gen->getReturn();
?>

El resultado del ejemplo sería:

1 2 3 4 5 6 7 8 9 10
add a note

User Contributed Notes 9 notes

up
122
Adil lhan (adilmedya at gmail dot com)
10 years ago
For example yield keyword with Fibonacci:

function getFibonacci()
{
$i = 0;
$k = 1; //first fibonacci value
yield $k;
while(true)
{
$k = $i + $k;
$i = $k - $i;
yield $k;
}
}

$y = 0;

foreach(getFibonacci() as $fibonacci)
{
echo $fibonacci . "\n";
$y++;
if($y > 30)
{
break; // infinite loop prevent
}
}
up
47
info at boukeversteegh dot nl
9 years ago
[This comment replaces my previous comment]

You can use generators to do lazy loading of lists. You only compute the items that are actually used. However, when you want to load more items, how to cache the ones already loaded?

Here is how to do cached lazy loading with a generator:

<?php
class CachedGenerator {
protected
$cache = [];
protected
$generator = null;

public function
__construct($generator) {
$this->generator = $generator;
}

public function
generator() {
foreach(
$this->cache as $item) yield $item;

while(
$this->generator->valid() ) {
$this->cache[] = $current = $this->generator->current();
$this->generator->next();
yield
$current;
}
}
}
class
Foobar {
protected
$loader = null;

protected function
loadItems() {
foreach(
range(0,10) as $i) {
usleep(200000);
yield
$i;
}
}

public function
getItems() {
$this->loader = $this->loader ?: new CachedGenerator($this->loadItems());
return
$this->loader->generator();
}
}

$f = new Foobar;

# First
print "First\n";
foreach(
$f->getItems() as $i) {
print
$i . "\n";
if(
$i == 5 ) {
break;
}
}

# Second (items 1-5 are cached, 6-10 are loaded)
print "Second\n";
foreach(
$f->getItems() as $i) {
print
$i . "\n";
}

# Third (all items are cached and returned instantly)
print "Third\n";
foreach(
$f->getItems() as $i) {
print
$i . "\n";
}
?>
up
20
Hayley Watson
8 years ago
If for some strange reason you need a generator that doesn't yield anything, an empty function doesn't work; the function needs a yield statement to be recognised as a generator.

<?php

function gndn()
{
}

foreach(
gndn() as $it)
{
echo
'FNORD';
}

?>

But it's enough to have the yield syntactically present even if it's not reachable:

<?php

function gndn()
{
if(
false) { yield; }
}

foreach(
gndn() as $it)
{
echo
'FNORD';
}

?>
up
10
zilvinas at kuusas dot lt
8 years ago
Do not call generator functions directly, that won't work.

<?php

function my_transform($value) {
var_dump($value);
return
$value * 2;
}

function
my_function(array $values) {
foreach (
$values as $value) {
yield
my_transform($value);
}
}

$data = [1, 5, 10];
// my_transform() won't be called inside my_function()
my_function($data);

# my_transform() will be called.
foreach (my_function($data) as $val) {
// ...
}
?>
up
13
Harun Yasar harunyasar at mail dot com
8 years ago
That is a simple fibonacci generator.

<?php
function fibonacci($item) {
$a = 0;
$b = 1;
for (
$i = 0; $i < $item; $i++) {
yield
$a;
$a = $b - $a;
$b = $a + $b;
}
}

# give me the first ten fibonacci numbers
$fibo = fibonacci(10);
foreach (
$fibo as $value) {
echo
"$value\n";
}
?>
up
11
christophe dot maymard at gmail dot com
9 years ago
<?php
//Example of class implementing IteratorAggregate using generator

class ValueCollection implements IteratorAggregate
{
private
$items = array();

public function
addValue($item)
{
$this->items[] = $item;
return
$this;
}

public function
getIterator()
{
foreach (
$this->items as $item) {
yield
$item;
}
}
}

//Initializes a collection
$collection = new ValueCollection();
$collection
->addValue('A string')
->
addValue(new stdClass())
->
addValue(NULL);

foreach (
$collection as $item) {
var_dump($item);
}
up
5
Shumeyko Dmitriy
10 years ago
This is little example of using generators with recursion. Used version of php is 5.5.5
[php]
<?php
define
("DS", DIRECTORY_SEPARATOR);
define ("ZERO_DEPTH", 0);
define ("DEPTHLESS", -1);
define ("OPEN_SUCCESS", True);
define ("END_OF_LIST", False);
define ("CURRENT_DIR", ".");
define ("PARENT_DIR", "..");

function
DirTreeTraversal($DirName, $MaxDepth = DEPTHLESS, $CurrDepth = ZERO_DEPTH)
{
if ((
$MaxDepth === DEPTHLESS) || ($CurrDepth < $MaxDepth)) {
$DirHandle = opendir($DirName);
if (
$DirHandle !== OPEN_SUCCESS) {
try{
while ((
$FileName = readdir($DirHandle)) !== END_OF_LIST) { //read all file in directory
if (($FileName != CURRENT_DIR) && ($FileName != PARENT_DIR)) {
$FullName = $DirName.$FileName;
yield
$FullName;
if(
is_dir($FullName)) { //include sub files and directories
$SubTrav = DirTreeTraversal($FullName.DS, $MaxDepth, ($CurrDepth + 1));
foreach(
$SubTrav as $SubItem) yield $SubItem;
}
}
}
} finally {
closedir($DirHandle);
}
}
}
}

$PathTrav = DirTreeTraversal("C:".DS, 2);
print
"<pre>";
foreach(
$PathTrav as $FileName) printf("%s\n", $FileName);
print
"</pre>";
[/
php]
up
-3
christianggimenez at gmail dot com
4 years ago
Module list of a number from 1 to 100.

<?php

function list_of_modulo(){

for(
$i = 1; $i <= 100; $i++){

if((
$i % 2) == 0){
yield
$i;
}
}
}

$modulos = list_of_modulo();

foreach(
$modulos as $value){

echo
"$value\n";
}

?>
up
-46
denshadewillspam at HOTMAIL dot com
9 years ago
Note that you can't use count() on generators.

/**
* @return integer[]
*/
function xrange() {
for ($a = 0; $a < 10; $a++)
{
yield $a;
}
}

function mycount(Traversable $traversable)
{
$skip = 0;
foreach($traversable as $skip)
{
$skip++;
}
return $skip;
}
echo "Count:" . count(xrange()). PHP_EOL;
echo "Count:" . mycount(xrange()). PHP_EOL;
To Top