by Adam Brown (Software Development Profession)
Recently, I attended the annual PHP Scotland conference. Among the topics covered, was a talk by Ian Littman on the subject of PHP generators. Littman looked at how they relate to the subject of concurrency and asynchronous PHP. Generators themselves are not always widely understood, so in this article, I examine some of the basic concepts behind working with generators.
Generators were designed as a simple and easy way to implement iterators in PHP. Before Generators, say, if you wanted to iterate over a file, it would require implementing the iterator class and that meant writing a lot of code to iterate over something. Anthony Ferrara in his article What Generators Can Do For You provides a good example illustrating the efficiency of using generators from previous methods. Generators do not provide any additional capability but they do help us make things simpler.
So what exactly is a generator?
Using a generator is similar to writing a function but instead of using the
return keyword, you use the
yield can be used multiple times in the same function and is read in sequence from the top to bottom (returning as a sequence of values).
By using a generator, you are invoking the iterator class and that means you are working with a generator object. The generator object, as we will see later, allows you to access a number of helpful methods and also works to track where in the iteration the generator is, this has implications when iterating over large or infinite arrays.
Let’s examine a very basic syntactic example of a generator in action:
Here, each time
gen_one_to_three() is iterated over, it yields two values. These are executed in order, first
yield $i and second
yield “abc". Because our for loop counts up to 3, we get the resulting
yield output three times.
Let’s look at another, example:
We’ve set the counter to count up to
PHP_INT_MAX, or put another way, asked the counter to count to 9223372036854775808 (assuming it’s run on a 64 bit system). Traditionally, trying to create an array and iterate over such a large number, would fail or you would have to significantly increase the memory allocation available to PHP. However, running the above will work and it will output each number, line by line, always continuing from where it last yielded, until it reaches
PHP_INT_MAX. How does the generator know where it last yielded? This works because the generator has created an iterator object and can track its current state internally, using significantly less memory than had it tried putting the entire array into memory.
This leads to all sorts of possibilities, from improving memory usage when iterating over large arrays, to creating concurrency in your application.
Generator objects and coroutines
As we’ve seen, generators implement the PHP iterator class. That means, generators themselves are objects, with methods you can call on and use.
In the above example
$generator = gen_one_to_three(), we saw the common use case of assigning a generator object to a variable. By doing so, we created an instance of PHP’s internal generator class.
So with the
$generator object now instantiated as a variable, we can now access object’s methods, such as
$generator->current() to get the current yield value or
$generator->send() that allows you to pass values back to the generator as a yield expression.
Have a look at this example from the PHP manual, using the
As the generator function is executed, it first runs the echo statement and then processes the yield expressions in order of the
Being able to send values back to the generator is a powerful concept, allowing for coroutines and multitasking. Nikita Popov provides an excellent blog article entitled Cooperative multitasking using coroutines (in PHP!) on the subject.
Lastly, I want to look at the
getReturn() generator method introduced in PHP 7. This allows you to return a value after the generator has run and is particularly useful for modular programming in PHP. PHP’s documentation on
getReturn provides the following example:
Here the generator returns the number 3 after both yields (1 and 2) are returned and the generator has finished running. Calling
getReturn() when the generator has not yet returned or closed, will throw an exception. One way you can check if the generator has finished, before calling
getReturn(), is by using the
valid() method. It will return
false if the generator has been closed.
For more general information on all the methods you can call when using a generator visit the PHP manual on generators.
The final feature of generators I’d like to cover, is around the use of yield statements. PHP 7 introduced a number of new features for generators, one being generator delegation, that allows you to yield values from one generator to another using the keyword
Let’s look at a example of delegation from the documentation:
Here we see the
count_to_ten() function being called. The yield statements are then executed, one by one, from the top to the bottom. As each is called, they return both values and other functions with their own yield statements.
In this way, the generator is able to delegate to either an array, or object, or function. Once that object or function has run, it will delegate back to the original function, in our case,
count_to_ten() will continue, executing until there are no more yield statements left to execute.
This back and forth communication, forms the basics underlining coroutines used by non-blocking concurrency frameworks.
I’ve only touched on some of basic concepts around generators and the advantages they bring. They really can be fundamental and useful to programming in modern PHP. Whether you are using them to save on memory usage or to write more advanced programming structures, such as coroutines and asynchronous PHP, it is worth spending some time learning what they offer.
If you’d like to come and work with us, please check current vacancies on our job board!