Blog Post

Using PHP Generators for Control Flow

December 21, 2013

As you may know from my last post covering Ember, I have become very interested in expanding my javascript skill set. I posted my 10 part Ember video series to /r/javascript and was glad to see it get some upvotes. I noticed a post about Koa.js sitting next to mine. Koa is touted as a next generation framework for node from the Express team. I have heard of Express but haven't done anything with it. Upon seeing there was something even newer, I was intrigued and decided to check it out.

What the heck is this voodoo?

I was really taken aback when I came to the Cascading section. The example showed a asterisk right in the middle of a closure definition and the use of a yield keyword.

// copy/pasted directly from Koa's docs
var koa = require('koa');
var app = koa();

// x-response-time

app.use(function *(next){
  var start = new Date;
  yield next;
  var ms = new Date - start;
  this.set('X-Response-Time', ms + 'ms');
});

// logger

app.use(function *(next){
  var start = new Date;
  yield next;
  var ms = new Date - start;
  console.log('%s %s - %s', this.method, this.url, ms);
});

// response

app.use(function *(){
  this.body = 'Hello World';
});

app.listen(3000);

I had never seen that before. Upon some further investigation I learned that they are generators. Generators allow you to do some really crazy iterations. You can stop the iteration and give the calling code a value by using the yield keyword. You can also send the generator data back in the middle of an iteration. They're pretty mind bottling at first glance. I was curious to know if php has generators and guess what, they're new in 5.5!

I encourage you to get slightly familiar with them before reading on!

Anthony Ferrara has great article on generators here.

SitePoint also has a pretty good post about them here.

A Wikipedia article about generators.

Re-purposing generators

After I got a handle on what generators are I went back to the Koa website to review what they were doing. It didn't looking like they were using them for iteration though. It looks like they're using them to pass control to another generator, where execution halts in the first generator and continues in the next generator, which the first generator is completely unaware of. Once there are no more generators to continue on, flow unwinds back to the yield line in each generator. Koa's using them for a sort of cascading middleware. Look at Koa's docs if what I said didn't make any sense.

I found this concept to be incredibly clever and interesting. I wonder if I could implement the same idea in php...

Mental Gymnastics

This turned out to be a really fun challenge. Man...programming has really warped my idea of fun. Surprisingly, I was able to achieve the same effect. It took me a little while to figure it out and I didn't cheat and look at Koa's source! I'm not even sure with my javascript skills it would've helped much anyway.

Right this way

Let me show you an example.

$obj = new \stdClass;
$obj->result = '';

$first = function() use ($obj) {
    $obj->result .= ' 1 ';
    yield ControlFlow::NEXT;
    $obj->result .= ' 2 ';
};

$second = function() use ($obj){
    $obj->result .= ' 3 ';
    yield ControlFlow::NEXT;
    $obj->result .= ' 4 ';
};

$third = function() use ($obj){
    $obj->result .= ' 5 ';
    return ControlFlow::NEXT;
};

$fourth = function() use ($obj){
    $obj->result .= ' 6 ';
    return ControlFlow::NEXT;
};

$flow = new ControlFlow;
$flow->queue($first()) //notice the parenthesis for generators
     ->queue($second())
     ->queue($third)
     ->queue($fourth);

$flow->run();

echo trim($obj->result); // '1 3 5 6 4 2'

My ControlFlow object accepts two object types in the queue() method, \Generator's and \Closure's. It will always execute each queued action when the run() method is called but the order of execution is dependent on the contents of each generator/closure.

In the above example, the flow occurs like so:

  1. execution starts in the $first generator
  2. ' 1 ' is appended to $obj->result
  3. the $first generator then attempts to stop it's execution and yield/pass control to an unknown piece of code
  4. the ControlFlow then passes control to the next generator/closure which happens to be $second
  5. ' 3 ' is appended
  6. the $second generator then attempts to stop it's execution and yield/pass control to an unknown piece of code
  7. the ControlFlow then passes control to the next generator/closure which happens to be $third
  8. ' 5 ' is appended
  9. the ControlFlow then passes control to the next generator/closure which happens to be $fourth
  10. ' 6 ' is appended
  11. the ControlFlow attempts to pass control to the next generator/closure but none is found so it starts to unwind
  12. control is then relinquished back to the last yield, which is in $second
  13. ' 4 ' is appended
  14. control is then relinquished back to the previous yield, which is in $first
  15. ' 2 ' is appended
  16. '1 3 5 6 4 2' is echoed

If the $third closure did not return ControlFlow::NEXT, execution would have gone like this(the difference starts on step 9):

  1. execution starts in the $first generator
  2. ' 1 ' is appended
  3. the $first generator then attempts to stop it's execution and yield/pass control to an unknown piece of code
  4. the ControlFlow then passes control to the next generator/closure which happens to be $second
  5. ' 3 ' is appended
  6. the $second generator then attempts to stop it's execution and yield/pass control to an unknown piece of code
  7. the ControlFlow then passes control to the next generator/closure which happens to be $third
  8. ' 5 ' is appended
  9. $third doesn't direct flow to another piece of code, so it starts to unwind
  10. control is then relinquished back to the last yield, which is in $second
  11. ' 4 ' is appended
  12. control is then relinquished back to the previous yield, which is in $first
  13. ' 2 ' is appended
  14. control is then passed to the next action it hasn't executed yet, $fourth
  15. ' 6 ' is appended
  16. '1 3 5 4 2 6' is echoed

As you can see, each action will always be executed, but their order is entirely dependent on the contents of the actions.

It's worth mentioning, in the first scenario, that if there was a $fifth generator action that looked like the others(append ' 7 ' , yield next, append ' 8 '), both ' 7 ' and ' 8 ' would be appended at the same time because it would be the last action in the queue. It would then start to unwind.

I showed my coworker my example and he likened it to using templates, which is a great analogy. You have a template that has a placeholder for other markup to be rendered into. That base template has no idea what's going to get rendered into to but as soon as that happens it can continue on and render the rest of its own markup. I'm applying this same principle to php code.

Is this useful?

I have no idea! It's a pretty neat trick though. It seems like it could be a really confusing way to do some sort of pub/sub event propagation considering that generators are not required to yield anything. They must have the yield keyword in them but it could be wrapped up in a an if-statement that may not allow the execution to get there. Generators are strange beasts.

I would love to see someone smarter than me take this idea and run with it. I would be very curious to see what you come up with!

ControlFlow

Here's my ControlFlow class. It should probably be named something like YoYo.

class ControlFlow
{
    const NEXT = 'NEXT';
 
    /**
     * @var array
     */
    protected $queued = [];
 
    /**
     * @var array
     */
    protected $initiated = [];
 
    /**
     * Queue an action
     *
     * @param \Closure|\Generator $action
     * @return $this
     * @throws \InvalidArgumentException
     */
    public function queue($action)
    {
        if ( ! $action instanceof \Closure && ! $action instanceof \Generator) {
            throw new \InvalidArgumentException('Queued action must be a \Closure or \Generator');
        }
 
        $this->queued[] = $action;
        return $this;
    }
 
    /**
     * Run all queued actions
     *
     * @return void
     */
    public function run()
    {
        foreach ($this->queued as $action) {
            $this->runAction($action);
        }
 
 	//flush everything because generators can't be reopened
        $this->flush();
    }
 
    /**
     * Run a queued action
     *
     * @param \Closure|\Generator $action
     * @return void
     */
    private function runAction($action)
    {
        if (in_array($action, $this->initiated, true)) return;
 
        $this->initiated[] = $action;
 
        if ($action instanceof \Generator) {
            $this->runGenerator($action);
        } elseif ($action instanceof \Closure) {
            $this->runClosure($action);
        }
    }
 
    /**
     * Run a queued closure
     *
     * @param \Closure $closure
     * @return void
     */
    private function runClosure(\Closure $closure)
    {
        if ($closure() === self::NEXT) {
            $this->initiateNext();
        }
    }
 
    /**
     * Run a queued generator
     *
     * @param \Generator $generator
     * @return void
     */
    private function runGenerator(\Generator $generator)
    {
        foreach ($generator as $result) {
 
            if ($result === self::NEXT) {
                $this->initiateNext();
            }
        }
    }
 
    /**
     * Initiate the next action
     *
     * @return void
     */
    private function initiateNext()
    {
        $nextAction = current($this->queued);
        next($this->queued);
        $this->runAction($nextAction);
    }
 
    /**
     * Remove the actions
     *
     * @return void
     */
    public function flush()
    {
        $this->queued = [];
        $this->initiated = [];
    }
}
David Adams
comments powered by Disqus