How to Create and Properly Use Generators in PHP

Generators are very useful features in PHP that allow you to generate and return large amounts of data based on requirements or conditions without taking up too much memory. I feel that despite their practicality, they aren’t very popular among PHP developers. That’s why I’ve outlined how to create and use them in this blog. I hope this article will help the programming community understand PHP generators and motivate them to use them more often.

First, something about arrays and iterators

Each program uses an array – the simplest list of values. We can fill it, reduce it and go through it, “iterate” it using foreach. The entire array exists in memory, and the larger the array, the more memory it takes up. 

The array is not the only one that can be iterated through foreach. In PHP, there’s an Iterator interface that we can use to iterate through foreach as well. In addition, we can also incorporate some logic into iterations. 

For example, if we wanted to send the contents of files in a certain folder somewhere, when using an array, we would load the list of files into the array and then, via foreach, reload and send them again. The code looks like this:

<?php
$dir = '/path/to/dir/';
$listFiles = scandir($dir);
foreach ($listFiles as $fileName) {
   $file = $dir . $file;
   if (is_file(#file)) {
       $fileContent = file_get_contents($dir . $file);
       sendFileSomewhere($fileContent);
   }   
}

When using the Iterator interface, the code is a bit simpler:

<?php
 $dir = '/path/to/dir/';
 $directoryIterator = new DirectoryIterator($dir);
foreach ($directoryIterator as $fileInfo) {
   if ($fileInfo->isFile()) {
       $fileContent = file_get_contents($fileInfo->getRealPath());
       sendFileSomewhere($fileContent);
   }   
}

How to create generators in PHP?

Iterators are used in PHP generators. These are functions that allow you to return a series of values from another function without having to create an entire array of values in memory. Unlike common functions that return values using return, generators use the keyword yield.

We can create a generator simply by using the function clause and the yield keyword mentioned above. A simple example of a generator that returns several values:

<?php

 function generator() {
  yield "hodnota 1";
  yield "hodnota 2";
  yield "hodnota 3";
}

This generator, when called, creates and returns three different values. In this case, they are “value 1”, “value 2” and “value 3”. Each value is returned using the keyword yield. We can use this generator in the foreach cycle to get each of the values it generates:

<?php
  foreach (generator() as $value) {
  echo $value . "<br>";
}

This code writes “value 1”, “value 2” and “value 3” on the screen.

However, generators in PHP aren’t limited to generating fixed values. We may also use them to generate values based on conditions or requirements. 

Example of a generator that returns random values between 1 and 10:

<?php
 function random_numbers() {
  while (true) {
      yield rand(1, 10);
   }
 }

This generator will return random numbers between 1 and 10 until it’s finished. Therefore, when calling the generator, we’ll receive various random numbers.

Nested generators from PHP 7

Since PHP 7, the yield from upgrade is also available, allowing for an additional level of generator nesting. Simply put, it allows us to use nested generators and simplifies the work with multiple iterators.

An example of how we can use yield from to create a nested generator:

<?php
 function myGenerator() {
    yield 'foo';
    yield 'bar';
}
 function myNestedGenerator() {
    yield 'hello';
    yield from myGenerator();
    yield 'world';
}
 foreach (myNestedGenerator() as $value) {
    echo "$value ";
}

In this example, we’ve defined two generators: myGenerator() and myNestedGenerator(). The myNestedGenerator() uses yield from myGenerator() to return elements from the myGenerator() nested generator along with other elements. Using yield from allows us to simplify the code and unify the output from multiple generators.

Using yield from can also be useful for processing recursive data structures or for processing tree structures where we need to navigate multiple levels. 

Thus the use of yield from allows us to work with multiple generators within a single generator, thus simplifying and streamlining our code.

Obtaining a return value

The getReturn() method is used to retrieve a value that was returned from the generator when the generator ends. This value is usually returned using the return in the generator.

Using this method allows us to get the return value from the generator without having to iterate through all its elements. This property can be useful when we want to get the return value of the generator, but we aren’t interested in the output sequence of the elements.

An example of how we can use getReturn() to get the return value from the generator:

<?php
function myGenerator() {
    yield 'foo';
    yield 'bar';
    return 'baz';
}
$gen = myGenerator();
foreach ($gen as $value) {
    echo "$value ";
}
echo $gen->getReturn();

In this example, we’ve defined a myGenerator() that returns three elements, “foo”, “bar” and “baz” using yield and return. Then we created an instance of the generator and used foreach to iterate through the elements. Finally, we used the getReturn() method to get the “baz” value that was returned from the generator.

The getReturn() method should be called only after the iteration through the generator has ended, otherwise it may cause unexpected behavior.

Using this method allows us to get the return value from the generator without having to iterate through all its elements. 

How to use generators in PHP

Generators in PHP can be used in a variety of ways. As we’ve already mentioned, we can use the foreach cycle to go through the individual values that the generator returns. However, we can also use the yield keyword within the function that calls the generator to generate values based on requirements or conditions.

An example of a function that calls a generator based on the number of generated values:

<?php
function generate_numbers($count) {
  $generator = random_numbers();
  for ($i = 0; $i < $count; $i++) {
    yield $generator->current();
    $generator->next();
  }
}

This function creates a new generator using the random_numbers() function. Subsequently, using the for cycle, it calls the generator as many times as required and returns each value that the generator returns. After each generator call, we call the next() function, which moves the internal indicator position of the generator to the next value.

The generator can also be used to generate large amounts of data. For example, if we wanted to generate a million random numbers between 1 and 10, we could do it this way:

<?php
function generate_numbers($count) {
  $generator = random_numbers();
  for ($i = 0; $i < $count; $i++) {
    yield $generator->current();
    $generator->next();
  }
}
foreach (generate_numbers(1000000) as $number) {
  echo $number . "<br>";
}

This code generates a million random numbers between 1 and 10 and writes them on the screen. Interestingly, this code uses only a small amount of memory. If we used an array that contains a million random numbers instead of a generator, the memory requirement would be much greater, because we would need to allocate space for the entire array at once.

Thanks to generators in PHP, we can easily work with files and efficiently load large files into memory. We can create a generator that runs a file by rows and generates each row individually, thereby minimizing memory demands.

For example, we can create a generator that runs a file by a row and returns each row as a string:

<?php
function readLines($filename) {
    $file = fopen($filename, 'r');
    while (!feof($file)) {
        yield fgets($file);
    }
    fclose($file);
}

In this example, we created a generator that loads a file and runs through it by rows using the fgets() function. It returns each row as a string using yield. This approach allows us to load large files and work with them simply and efficiently.

You can also use the generator to write data to a file. For example, we can create a generator that accepts an array of values and writes them to a file one by one:

<?php
function writeData($filename, $data) {
    $file = fopen($filename, 'w');
    foreach ($data as $value) {
        fwrite($file, $value . "\n");
        yield;
    }
    fclose($file);
}

In this example, we created a generator that accepts an array of $data values and writes them into a file using the fwrite() function. Finish each entry in the file with yield. This approach allows us to write large amounts of data to the file gradually and efficiently.

Therefore, the use of generators allows us to easily work with files and minimize memory requirements, which is very useful when working with large files.

Using iterators in PHP, we can browse and process data in parts, saving memory resources and increasing the efficiency of our code. The use of iterators in combination with generators can be very useful for processing large files or datasets.

One way to create an iterator using a generator is to create a generator that returns individual elements of a dataset, and then define the class of iterator that will iterate and process those elements.

An example of how to create an iterator using a generator:

<?php
class MyIterator implements Iterator {
    private $data;
    public function __construct($data) {
        $this->data = $data;
    }
    public function rewind() {
        reset($this->data);
    }
    public function current() {
        return current($this->data);
    }
    public function key() {
        return key($this->data);
    }
    public function next() {
        return next($this->data);
    }
    public function valid() {
        return ($this->current() !== false);
    }
}
function myGenerator($data) {
    foreach ($data as $item) {
        yield $item;
    }
}
$data = array(1, 2, 3, 4, 5);
$generator = myGenerator($data);
$iterator = new MyIterator($generator);
foreach ($iterator as $key => $value) {
    echo "Key: $key, Value: $value\n";
}

In this example, we have created a MyIterator class that implements the Iterator interface. This class allows iteration through data and its processing. Subsequently, we created myGenerator, which returns individual elements from the array. Finally, we used a generator to create an iterator and iterated through all the elements using a foreach cycle.

The use of generators in combination with iterators allows us to create flexible and powerful iterable classes and is very useful for processing large files where memory overflow problems could otherwise occur. In addition, the use of iterators allows us to process data flexibly and efficiently in parts, thereby achieving better results when processing large data sets.

Other benefits of using generators

It’s also very useful to use the send() generator method, which allows values to be passed back to the generator as yield. This changes the one-way communication (from a generator to a caller) to a two-way channel between them. 

Example of using a data logging tool via the send() function:

<?php
 function createLogger($file)): Generator {    $f = fopen($file, 'a');
     while (true) {
         $line = yield;
         fwrite($f, $line. "\n");
     }
 }
 $log = createLogger('/path/to/log/file.log');
 $log->send("First");
 $log->send("Second");
 $log->send("Third");

 

Two-way communication between the caller and the generator using the send() method can be used to implement cooperative multitasking. This basically means switching between multiple tasks – starting one task at a time and switching it with another task. I recommend a great blog on how to implement cooperative multitasking using co-routines using PHP generators.

It’s also important to emphasize that generators aren’t suitable for every situation – we should use the right tools for the right tasks. For example, when we need to store a larger amount of data in memory, it may be more convenient to use an array or a list instead of a generator.

I would also like to mention that the use of generators can affect the performance of the code, as the generator must be created and controlled during the running of the program. Therefore, it’s important to remember that generators should be used with common sense.

Conclusion

Generators in PHP are very useful for generating large amounts of data that we don’t need to store in memory at once. They have very low memory requirements and can be used to generate values based on requirements or conditions. Therefore, the use of generators in PHP allows us to generate large amounts of data efficiently and with low memory requirements.