O'Reilly logo

Node.js for PHP Developers by Daniel Howard

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Advanced Callbacks

In the previous chapter, the concept and use of linearity was explained. Making PHP code linear makes it easier to convert to Node.js code. But what if PHP code cannot be made linear?

A good example of PHP code that cannot simply be made linear is a for statement that writes to multiple files. The PHP code remains stubbornly linear because the for statement cannot be eliminated and the blocking PHP API calls cannot be moved outside of it:

for ($i=0; $i < $n; ++$i) {
  $fp = fopen('fp.txt'+$i, 'w');
  fwrite($fp, $data);
  fclose($fp);
}

The Node.js “callback chain” illustrates how the Node.js code will work. A callback chain is formed where each subsequent nonblocking Node.js API call is nested inside the callback of the previous nonblocking Node.js API call, forming an arbitrarily long chain:

fs.open('fp.txt0', 'w', 0666, function(error, fp) {
  fs.write(fp, data, null, 'utf-8', function() {
    fs.close(fp, function(error) {
      fs.open('fp.txt1', 'w', 0666, function(error, fp) {
        fs.write(fp, data, null, 'utf-8', function() {
          fs.close(fp, function(error) {
            // and on and on for a total of "n" times
          });
        });
      });
    });
  });
});

Although this example illustrates how the Node.js code needs to function, it is not practical because the value of the n variable might only be known at runtime. To actually implement the callback chain in Node.js, the code must use three relatively sophisticated JavaScript features: anonymous functions, lambdas, and closures.

Anonymous Functions, Lambdas, and Closures

An anonymous function is a function without a name. Named functions are general-purpose tools, ready and available for other JavaScript code to invoke at any time. In contrast, anonymous functions are usually special purpose and specific to the code that they are declared in.

How can you call a function without a name? An anonymous function is often also a lambda function. A lambda function is a function that can be assigned to a variable and that variable functions very much like any other variable. A lambda function can also be invoked using the usual function call syntax with the variable that the lambda function is assigned to. An anonymous function is assigned to a variable (or passed to a function where it is assigned to one of the function arguments) and can be invoked using the variable and the function call syntax. For example:

var f = function(a) {
  // lambda function
  return a + 1;
};

var b = f(4); // b takes the value of 5

The word “lambda" may sound highfalutin and complicated but it hides the simple concept that functions are first-class values, like numbers and strings, and can mostly be treated and manipulated in the same ways that numbers and strings can.

To make anonymous functions easier and more capable, JavaScript supports closure. Closure is a language feature where, when the function is defined, the outer context of a function is preserved and is made available to the function when it is invoked. The value of any variables in the saved context is persistent over time and there is only one value at any given time; all invocations of the function share the same context and refer to the same variables. An example of closure is:

function f() {
  var b = 6;
  function g() {
    // b is closed over by this function
    ++b;
    return b;
  }
  return g;
}

var h = f();
var c = h(); // c is 7
var d = h(); // d is 8

In this example, the b variable, even though it is not a local variable of the g() function, is still available to the g() function even after the f() function has exited. The b variable (also called an “upvalue” in closure terminology) behaves sort of like a private global variable. It behaves like a global variable to the g() function but is hidden from all other code that is outside of the f() function.

Anonymous functions, lambdas, and closures are needed to implement a callback chain that can be arbitrarily long:

var f = function() {
};
for (var i=n-1; i >= 0; --i) {
  f = function(g, j) {
    return function() {
      fs.open('fp.txt'+j, 'w', 0666, function(error, fp) {
        fs.write(fp, data, null, 'utf-8', function() {
          fs.close(fp, function(error) {
            g();
          }
        });
      });
    };
  } (f, i);
}
f();

The Node.js code is quite complicated. The overall algorithm is to build the callback chain in reverse order, from the most deeply nested call to the outermost call, and then to kick off the callback chain by calling the first one. Let us go through it, step by step.

A do-nothing, default lambda function is created that will be the “no-op” (empty) callback for the innermost fs.close() call:

var f = function() {
};

In the PHP code, the for statement goes from 0 to n-1 but, since callbacks need to be built from the inside out, Node.js requires the for statement to go from n-1 to 0:

for (var i=n-1; i >= 0; --i) {
  // create the callback
}

The current callback is held in the f variable to capture the value during this specific iteration of the for loop. The callback just outside the current callback will be created by using a new lambda function that does the work for the outer callback and then calls the current (inner) callback. The new lambda function is a return value for syntax reasons that will be explained next:

return function() {
  fs.open('fp.txt'+j, 'w', 0666, function(error, fp) {
    fs.write(fp, data, null, 'utf-8', function() {
      fs.close(fp, function(error) {
        g(); // current callback
      }
    });
  });
}

The newly created callback function is returned as an anonymous function. An anonymous function is an unnamed function that can be used as a value. The inner anonymous function takes zero arguments and is used as the return value for an outer function that takes two arguments. The outer function is needed to defeat closure.

In this case, closure must be defeated so that the values of the f and i variables during this iteration of the loop are preserved, instead of their values at some later time, such as when the f function is invoked. Closure is defeated by making new variables, actually, arguments, that will always have the same values. The g variable holds the value of the f variable during this iteration of the for loop; likewise, the j variable holds the value of the i variable during this iteration of the for loop. The outer anonymous function (which takes two arguments) is invoked immediately, as indicated by the (f, i) string at the end of the function definition. When invoked, it captures the f and i values and returns the inner anonymous function. The inner anonymous function actually contains the callback code:

f = function(g, j) {
  return function() {
    // implement outer callback
    // call g() to invoke inner callback
  };
} (f, i);

The for statement loops from n-1 to 0, creating inner callbacks and nesting them into outer callbacks. When the for statement exits, the f variable contains the outermost callback, which will properly invoke its subsequent callback, which will invoke its subsequent callback, and so on, all in the correct order. All that is left is to invoke the outermost callback:

f();

This Node.js code works but it has two drawbacks. First, the PHP code looks nothing like the Node.js code. If callback chains occur often (which is common), maintaining and improving the PHP version and the Node.js version simultaneously is going to be very difficult if they are very different. Second, the Node.js code by itself is pretty complicated and requires three advanced JavaScript concepts: lambdas, anonymous functions, and closure.

PHP 5.3

PHP 5.3 introduced anonymous functions, lambdas, and closure to the PHP language. To make the PHP similar to the Node.js code, the PHP code could be refactored from its step-by-step algorithm to the complicated algorithm used by the Node.js function.

Lambdas and anonymous functions in PHP 5.3 are very similar to lambdas in Node.js. The only difference is syntactic. PHP variables use the dollar sign ($) while Node.js variables do not; Node.js variables are required to be declared using the var keyword:

$f = function($a) {
  // lambda function
  return $a + 1;
};

$b = $f(4); // $b takes the value of 5

PHP 5.3 also supports closure. Unlike Node.js, the entire outer context is not automatically preserved in PHP 5.3 like it is in Node.js. In PHP 5.3, the use keyword must be used to indicate the specific variables (upvalues) that will be captured (closed over) by the function. Variables can be captured in two ways: they can be captured by value or by reference.

If a PHP variable is captured by value, each invocation of the lambda function will reset the variable to its value when the lambda function was created. A variable captured by value freezes its value in time.

If a PHP variable is captured by reference, each invocation of the lambda function shares the same variable. If one invocation modifies the variable, future invocations will see the modification. Capturing by reference might be considered real or true closure and is equivalent to how closure works in Node.js:

function f() {
  $b = 6;
  $g = function() use (&$b) {
    // b is closed over by this function
    ++$b;
    return $b;
  }
  return $g;
}

$h = f();
$c = $h(); // c is 7
$d = $h(); // d is 8

This example captures the $b variable by reference. Since the $b variable will be preserved across invocations, the $d variable is assigned the value of 8. Initially, the $b variable is set to 6. Then, when the $c variable is assigned, the $b variable is incremented to 7. Finally, when the $d variable is assigned, the $b variable is incremented to 8.

If the $b variable was captured by value instead of by reference, any call to the $g function would always return 7. In that case, when the $c variable is assigned, the $b variable is incremented to 7. But when the $d variable is assigned, the $b variable is reset to 6 and then incremented to 7. The $d variable would receive a value of 7 instead of a value of 8.

What is the purpose of capturing by value if it is not really closure? Capturing by value is how PHP 5.3 defeats closure.

Earlier, it was shown that Node.js needs to defeat closure in order to take the current value of a variable when a function is defined, instead of its current value at the time of invocation. Here’s a simple example of defeating closure in Node.js:

for (var i=0; i < n; ++i) {
  f[i] = function(j) {
    return function() {
       return j;
    };
  } (i);
}

In Node.js, an outer function and an inner function are used to defeat closure. The outer function, which takes a single argument, is declared and then immediately invoked as indicated by the (i) string at the end of the function definition. When it is invoked, the value of the i variable is “frozen” in the j argument. The inner function captures the j variable instead of the i variable. Even though the j variable is captured (essentially by reference), a new j variable is declared each time through the loop. The j variable is many variables that take the value of the i variable when the function is defined; the i variable itself is a single variable with a single value that is shared across all invocations.

Consider alternative Node.js code where closure is not defeated:

for (var i=0; i < n; ++i) {
  f[i] = function() {
    return i;
  };
}

In this Node.js example, a single i variable is shared across all the functions. The i variable breaks out of the loop when the i variable reaches the value of n. Since the i variable now has the value of n, all the functions that were generated will return the current value of i, which is n.

PHP 5.3 is much simpler. Instead of declaring two functions where an outer function is required to defeat closure in the inner function, PHP 5.3 allows closure values to be captured by value, instead of reference. Defeating closure is a language feature that results in simpler code:

for ($i=0; $i < $n; ++$i) {
  $f[$i] = function() use ($i) {
    return $i;
  };
}

Closure (i.e., true closure) is enabled by using the ampersand (&) in the use clause of the code; removing the ampersand (&) defeats closure. The extra complication of outer and inner functions and immediate invocation in Node.js is not needed in PHP 5.3.

Using anonymous functions, lambdas, and closure by value (i.e., defeating closure), the Node.js callback chain algorithm can be implemented in PHP 5.3. The new PHP 5.3 code is more complex than the original code, but by design, it is very similar to the Node.js code:

$f = function() {
};
for ($i=$n-1; $i >= 0; --$n) {
  $f = function() use ($f, $i) {
    $fp = fopen('fp.txt'+$i, 'w');
    fwrite($fp, $data);
    fclose($fp);
    $f();
  }
}
$f();

The corresponding Node.js code is:

var f = function() {
};
for (var i=n-1; i >= 0; --i) {
  f = function(g, j) {
    return function() {
      fs.open('fp.txt'+j, 'w', 0666, function(error, fp) {
        fs.write(fp, data, null, 'utf-8', function() {
          fs.close(fp, function(error) {
            g();
          }
        });
      });
    };
  } (f, i);
}
f();

Since the PHP 5.3 code now looks so similar to the Node.js code, it is much easier to improve the code and add features while maintaining both the PHP and Node.js versions simultaneously.

The PHP 5.3 code can be improved to parallel the Node.js code more closely. The definition of the f function inside the for statement looks like this:

$fp = fopen('fp.txt'+$i, 'w');
fwrite($fp, $data);
fclose($fp);
$f();

The corresponding Node.js code looks like this:

fs.open('fp.txt'+j, 'w', 0666, function(error, fp) {
  fs.write(fp, data, null, 'utf-8', function() {
    fs.close(fp, function(error) {
      g();
    }
  });
});

Notice anything familiar? The linearity-based conversion recipes from Chapter 3 can be applied here! The PHP 5.3 implementation of the f function can be made linear and converted to Node.js code easily and independently. The outer context of the function is irrelevant and is totally orthogonal to converting the implementation of function f.

One annoyance is that the PHP 5.3 code uses $i and $f, the original names of the variables captured using closure by value, whereas the Node.js renames these variables to j and g. This annoyance is easily remedied by creating temporary variables, $j and $g, in the PHP 5.3 code:

$g = $f;
$j = $i;
$fp = fopen('fp.txt'+$j, 'w');
fwrite($fp, $data);
fclose($fp);
$g();

The final PHP 5.3 implementation of the callback chain algorithm is:

$f = function() {
};
for ($i=$n-1; $i >= 0; --$n) {
  $f = function() use ($f, $i) {
    $g = $f;
    $j = $i;
    $fp = fopen('fp.txt'+$j, 'w');
    fwrite($fp, $data);
    fclose($fp);
    $g();
  }
}
$f();

So, a useful conversion recipe is to rewrite PHP 5.3 code from a step-by-step algorithm to a callback chain algorithm.

A pattern is developing. Converting PHP code to Node.js code often involves refactoring the PHP code so it is more like Node.js, not simply taking the PHP code as is and grinding through a bunch of conversion steps to output Node.js code at the other end. Refactoring the PHP code can even be seen as converting Node.js features and algorithms to PHP such that when the “official” conversion process begins, the PHP code is already primed for easy conversion to Node.js code. The conversion process is two-way: the PHP code is converted to Node.js, but in some sense, the Node.js code is converted to PHP code. An acceptable medium is developed between the two languages and the two codebases. Most likely, the developer will decide to compromise the PHP code the most but there will be some cases where the Node.js code makes some minor compromises to keep the PHP code from being too difficult.

PHP 4

PHP 5.3 introduced anonymous functions, lambdas, and closure, which are all needed to easily create Node.js-style callback chain algorithms. What about PHP 4 and other PHP versions below PHP 5.3? Does PHP code that is converted to Node.js have to drop support for PHP versions below 5.3?

Many web hosting companies no longer support or provide PHP 4 or PHP versions below 5.3. Similarly, as cloud computing increases in popularity and virtual machines become the de facto standard for delivering cloud computing, the user can choose any version of PHP that he wants to run and there really is not any reason to choose a PHP version less than 5.3. If your PHP code is targeted for specific users, perhaps you can require those users to install PHP 5.3 and above. If your PHP code is targeted at the general public, perhaps it is not a serious burden or inconvenience to require them to install PHP 5.3 or above or to insist that your users select only services that support PHP 5.3 and above.

Still, there are some good reasons to support PHP 4 and PHP versions below 5.3. One reason is to remove the PHP version as a requirement. Why have a requirement when you do not have to? If your PHP code supports PHP versions below 5.3, you can loosen the requirement from “PHP 5.3 and above” to just “PHP.” The user no longer needs to figure out the PHP version; he only needs to know that he can serve PHP code. Another reason is that the codebase will rely on fewer language features. If you decide to convert your PHP code to even more languages, do you really want to require those languages to support advanced language features like anonymous functions, lambdas, and closure and assume that they are supported in a similar way? In the past, software was written and functioned in languages without these advanced features, so it is probably doable. It may even be easier for novice developers to follow and understand your code. A final reason is that there may be some legacy or custom requirement that forces your users to use a PHP version below 5.3.

For the rest of this chapter, I will present a way to support PHP 4 and PHP versions below 5.3 that will still allow conversion to Node.js code. With this method, the code can still be improved and features can be added while maintaining both the PHP and Node.js versions simultaneously. For simplicity, I will use the phrase “PHP 4” to loosely refer to “PHP 4 and PHP versions below 5.3.”

Anonymous functions, lambdas, and closure are not supported in PHP 4. However, these language features can be simulated in PHP 4 and even the simulations of these features can be converted to Node.js code. Instead of using its built-in versions of anonymous functions, lambdas, and closure, the Node.js code can convert the algorithms that implements the simulations from PHP and run them. Both the PHP and Node.js code will eschew language support for anonymous functions, lambdas, and closure so their code can closely parallel each other.

A simulation of a language feature always uses a subset of the language. Basic features of the language are built upon to accomplish what the more advanced feature could accomplish simply and directly. The Node.js code will always be able to use its basic features to simulate its more advanced features. With trivial differences, it will always be possible for the PHP code that simulates an anonymous function, a lambda, or closure to be directly converted to Node.js code that supports it in the same way. The basic features of PHP and Node.js are nearly identical in functionality, though not in syntax, such that it can confidently be said that “whatever can be built with PHP’s basic features can be built with Node.js’ basic features.” As a result, the PHP code that simulates anonymous functions, lambdas, and closures can be converted and will work the same way in Node.js.

In Node.js, this anonymous function increments its argument and returns the incremented value as its return value:

function(a) {
  return a + 1;
};

In PHP 4, this anonymous function can be simulated by giving it a name. It is now a “simulated anonymous PHP function”:

function anon_increment($a) {
  return $a + 1;
}

This simulated anonymous PHP function can be converted back to Node.js:

function anon_increment(a) {
  return a + 1;
}

If the Node.js code has 20 anonymous functions, they can be simulated by making 20 named functions in PHP and those 20 named PHP functions can be converted back to Node.js as 20 named Node.js functions. For now, it may seem pointless but it can be done.

A lambda function, which is generally an anonymous function, is stored in a lambda variable. The f variable stores a lambda function in the following Node.js code:

var f = function(a) {
  return a + 1;
};

In PHP 4, a lambda variable can be simulated using a PHP array. A PHP array can store all the information to “freeze” a function call. Instead of passing around a lambda variable, a PHP array variable can be passed around. If supported properly, the PHP array can do everything that a lambda variable can do:

function anon_increment($a) {
  return $a + 1;
}

$f = array('func'=>'anon_increment');

The $f variable above encapsulates a lambda variable by storing the name of the function that it represents. The func key indicates which lambda function it is pointing to.

This simulated lambda PHP variable and function can be converted back to Node.js:

function anon_increment(a) {
  return a + 1;
}

var f = {'func':'anon_increment'};

In Node.js, the PHP array becomes a Node.js object. Node.js objects work similarly to PHP arrays and have a similar syntax.

In Node.js, a lambda function can be invoked:

var f = function(a) {
  return a + 1;
};

var b = f(4); // b takes the value of 5

To simulate the invocation of a lambda function in PHP 4, the target function must accept a call object, a PHP array that represents the lambda function call. The invoking code will create this call object before it invokes the simulated lambda function. The call object combines both the lambda variable and the details of the call, such as the argument values.

The call object will contain several properties. These properties are:

func

A string with the name of the function to invoke

args

An array of objects that are the arguments to the call

ret

An object that is the return value of the call

Each function that will be a target of a simulated lambda function call will need to have a version of itself that accepts a call object instead of ordinary arguments and return values. Accepting a call object will give the utmost flexibility to the target function in terms of handling the call. By convention, the lf_ prefix is added to the function name to indicate that it is a “lambda function” handler. The lambda function handler will unpack the call arguments and repack the return value into the call object.

A side-by-side comparison of the PHP anon_increment() function and the PHP lf_anon_increment() lambda function will demonstrate how the $a argument is unpacked from the lambda call object, the $lc argument, and how the return value is repacked into the same:

function anon_increment($a) {
  return $a + 1;
}

function lf_anon_increment($lc) {
  $a = $lc['args']['a']; // unpack the arguments
  $ret = $a + 1;
  $lc['ret'] = $ret; // pack the return value
};

At the start of the function, the argument array is unpacked into individual variables that have the same name. When the function exits, the return value is packed into the call object.

Of course, the simulated lambda function can be converted to Node.js:

function anon_increment(a) {
  return a + 1;
}

function lf_anon_increment(lc) {
  var a = lc['args']['a']; // unpack the arguments
  var ret = a + 1;
  lc['ret'] = ret; // pack the return value
};

A helper function is needed to translate the simulated lambda call object into an actual function call. This PHP function works like a very poor man’s JavaScript eval() function call. It takes a string and turns it into running code. In our case, it takes a string and turns it into a single function call:

function lc_call($lc) {
  if ($lc['func'] == 'anon_increment') {
    lf_anon_increment($lc);
  }
  return $lc;
}

For convenience of the callers, the lc_call() function returns the same call object, which is passed as an argument. The calling code can sometimes have more compact code by taking advantage of the returned call object.

The lc_call() function will grow as you add new lambda functions. You simply add more if…else clauses. If you have many lambda functions, the lc_call() function will become very long.

The lc_call() function can be converted to Node.js:

function lc_call(lc) {
  if (lc['func'] == 'anon_increment') {
    lf_anon_increment(lc);
  }
  return lc;
}

To simulate a lambda function call, the call object is created by merging the function object with the arguments. The PHP array_merge() function takes the $f array variable, which simulates the lambda variable and merges or adds an array that represents the function arguments to be applied. Then, the lc_call() function is called on the call object to execute the call. The lc_call() function finds the function to call and calls it. The target function, the lf_anon_increment() function in our previous example, unpacks the arguments, increments the argument, and then packs the return value in the ret key. The lf_anon_increment() function returns to lc_call(), which returns the call object to the caller. Since the call object is returned from the lc_call() function, the ret key can be accessed inline to get the return value. Here’s the “long” form of the code to invoke the simulated lambda function:

$b = lc_call(array_merge($f, array('args'=>array('a'=>4))))['ret'];

Although this PHP code makes it as straightforward as possible to trace the code through the lc_call() function call then through the lf_anon_increment() function call and back again, it is unnecessarily verbose in practice. A helper function, the lf_call() function, can shorten the code for lambda functions to be turned into calls:

function lf_call($lf, $a) {
  $lf['args'] = $a;
  return lc_call($lf);
}

$b = lf_call($f, array('a'=>4))['ret'];

Instead of calling the array_merge() PHP API, assigning the arguments array to the args key of the lambda variable works just as well. In practice, it is harmless to reuse a lambda function as a call object. lf_call() is much simpler than lc_call().

Of course, this PHP code can be converted to Node.js code easily:

function lf_call(lf, a) {
  lf['args'] = a;
  return lc_call(lf);
}

var b = lf_call(f, array('a'=>4))['ret'];

We now have everything needed to simulate the Node.js example code. If you will recall, the Node.js code created an anonymous function, assigned it to the f lambda variable, invoked the f lambda function, and assigned the return value to the b variable:

var f = function(a) {
  // anonymous function
  return a + 1;
};

var b = f(4); // b takes the value of 5

The four pieces of the simulated lambda call are: the lf_anon_increment() function, the lc_call() helper function, the lf_call() convenience function, and the calling code where the $f lambda variable is invoked. The following code puts all four pieces together for a complete demonstration of how PHP 4 can support lambda (callback) functions:

function lf_anon_increment($lc) {
  // anonymous function
  $a = $lc['args']['a'];
  $ret = $a + 1;
  $lc['ret'] = $ret;
};

function lc_call($lc) {
  if ($lc['func'] == 'anon_increment') {
    lf_anon_increment($lc);
  }
  return $lc;
}

function lf_call($lf, $a) {
  $lf['args'] = $a;
  return lc_call($lf);
}

$f = array('func'=>'anon_increment');
$b = lf_call($f, array('a'=>4))['ret'];

This PHP 4 code can be converted to Node.js in a straightforward way:

function lf_anon_increment(lc) {
  // anonymous function
  var a = lc['args']['a'];
  var ret = a + 1;
  lc['ret'] = ret;
};

function lc_call(lc) {
  if (lc['func'] == 'anon_increment') {
    lf_anon_increment(lc);
  }
  return lc;
}

function lf_call(lf, a) {
  lf['args'] = a;
  return lc_call(lf);
}

var f = {'func':'anon_increment'};
var b = lf_call(f, {'a':4})['ret'];

This example shows definitively that Node.js anonymous functions and lambda variables can work in PHP 4 code and be converted to Node.js code. Now, let us turn our attention to the real-world example presented at the start of this chapter. In PHP, writing multiple files was straightforward:

for ($i=0; $i < $n; ++$i) {
  $fp = fopen('fp.txt'+$i, 'w');
  fwrite($fp, $data);
  fclose($fp);
}

Converting this PHP code to Node.js was problematic. The code was nonlinear and could not be made linear. As a result, the Node.js code had to be completely rewritten with a callback chain algorithm:

var f = function() {
};
for (var i=n-1; i >= 0; --i) {
  f = function(g, j) {
    return function() {
      fs.open('fp.txt'+j, 'w', 0666, function(error, fp) {
        fs.write(fp, data, null, 'utf-8', function() {
          fs.close(fp, function(error) {
            g();
          }
        });
      });
    };
  } (f, i);
}
f();

To effectively maintain and improve the PHP and Node.js code simultaneously, it was unacceptable that the two codebases be so dramatically different. Since the PHP algorithm, a simple for statement, could not be converted into Node.js code, the Node.js callback chain algorithm was converted to PHP 5.3 code:

$f = function() {
};
for ($i=$n-1; $i >= 0; --$n) {
  $f = function() use ($f, $i) {
    $g = $f;
    $j = $i;
    $fp = fopen('fp.txt'+$j, 'w');
    fwrite($fp, $data);
    fclose($fp);
    $g();
  }
}
$f();

To convert this PHP 5.3 code to PHP 4, the first step is to identify and implement the anonymous functions. At the top of the code, there is a “do nothing” anonymous function; a good name for this function is lf_anon_noop() because it is a “no operation” function or “noop” for short. Inside the for statement, there is a “write file” anonymous function; a good name for this function is lf_anon_writefile(). Here’s the code for these functions:

function lf_anon_noop($lc) {
}

function lf_anon_writefile($lc) {
  $j = $lc['args']['j'];
  $g = $lc['args']['g'];
  $fp = fopen('fp.txt'+$j, 'w');
  fwrite($fp, $data);
  fclose($fp);
  lc_call($g);
}

In the PHP 4 if_anon_writefile() function, the arguments are unpacked from the lambda call object. After that, the file is written; the PHP 4 code for that is unsurprising. Finally, the $g lambda variable is invoked to execute the next step in the callback chain.

These PHP 4 anonymous functions can be converted to Node.js:

function lf_anon_noop(lc) {
}

function lf_anon_writefile(lc) {
  var j = lc['args']['j'];
  var g = lc['args']['g'];
  fs.open('fp.txt'+j, 'w', 0666, function(error, fp) {
    fs.write(fp, data, null, 'utf-8', function() {
      fs.close(fp, function(error) {
        lc_call(g);
      }
    });
  });
}

Notice how the implementation of the lf_anon_writefile() function in Node.js can have the linearity-based conversion rules from Chapter 3 applied independently of the outer context of the function. A callback chain algorithm works because it forces the nonblocking Node.js API calls into an anonymous function that prevents those APIs from affecting the surrounding code. The anonymous function serves as a “prison” for the nonlinear code, breaking it apart from the for statement that causes it to be nonlinear. Since it is effectively no longer in a for statement, the for statement becomes linear.

The second step to make the PHP 4 code work is to implement the lc_call() and lf_call() functions. The lc_call() function is unsurprising; it is modified from the example to execute the lf_anon_noop() and lf_anon_writefile() functions instead of the lf_anon_increment() function from the previous example. The lf_call() function is unchanged. It is completely independent of the specifics of the rest of the PHP 4 code:

function lc_call($lc) {
  if ($lc['func'] == 'anon_noop') {
    lf_anon_noop($lc);
  } else if ($lc['func'] == 'anon_writefile') {
    lf_anon_writefile($lc);
  }
  return $lc;
}

function lf_call($lf, $a) {
  $lf['args'] = $a;
  return lc_call($lf);
}

The PHP 4 lf_anon_noop() and lf_anon_writefile() functions are converted to Node.js with very few changes:

function lc_call(lc) {
  if (lc['func'] == 'anon_noop') {
    lf_anon_noop(lc);
  } else if (lc['func'] == 'anon_writefile') {
    lf_anon_writefile(lc);
  }
  return lc;
}

function lf_call(lf, a) {
  lf['args'] = a;
  return lc_call(lf);
}

The third and final step is to convert the PHP 5.3 calling code into PHP 4. To more clearly see the PHP 5.3 calling code, the implementations of the anonymous functions have been removed and replaced with comments here:

$f = function() {
  // the lf_anon_noop() anonymous function
};
for ($i=$n-1; $i >= 0; --$n) {
  $f = function() use ($f, $i) {
    // the lf_anon_writefile() anonymous function
  }
}
$f();

This PHP 5.3 calling code will be translated line by line to PHP 4 and Node.js.

The following PHP 5.3 code assigns a “noop” (no operation) lambda function to the $f lambda variable:

$f = function() {
  // the lf_anon_noop() anonymous function
};

In PHP 4, the $f lambda variable becomes an array variable that points to the lf_anon_noop() simulated anonymous function:

$f = array('func'=>'anon_noop');

Next, in PHP 5.3, the for statement builds the callback chain by repeatedly redefining the $f lambda variable in terms of its initial value or its value from the last time through the loop. The use keyword is used in such a way to defeat closure for the $f and $i variables. The $f lambda variable is used inside the anonymous function (i.e., lf_anon_writefile()) as the next link in the chain:

for ($i=$n-1; $i >= 0; --$n) {
  $f = function() use ($f, $i) {
    // the lf_anon_writefile() anonymous function
  }
}

In PHP 4, the for statement remains the same. The inside of the for statement is very interesting, though:

for ($i=$n-1; $i >= 0; --$n) {
  $f = array('func'=>'anon_writefile', 'args'=>array('j'=>$i, 'g'=>$f));
}

The $f lambda variable is redefined as a call object that passes the current value of the $i index variable and the value of the $f lambda variable from the previous time through the loop as arguments. But in the PHP 5.3 code, these variables are not passed as arguments; they are passed to the use keyword in such as a manner as to defeat closure. Closure and arguments are two different things. What happened to closure?

With Node.js and PHP 5.3 lambda variables, only a function can be stored in a variable, not any predefined arguments. Arguments can only be applied when the lambda variable is invoked, not at the time of definition. In PHP 4, though, lambda variables are simulated using an array. Since lambda variables are just arrays, arguments can be added at the time of definition or at the time of invocation or any other time. In some sense, simulated lambda variables are more powerful than real lambda variables because they can add their arguments at any time, not just at the time of invocation.

In light of this idea, closure can be seen as a way to add arguments at the time of definition. When using closure, arguments are added to the lambda function by reference. When defeating closure, arguments are added by value. Simulating a lambda variable using an array and manipulating the arguments at definition time is a drop-in replacement for defeating closure.

To simulate the PHP 5.3 use clause in PHP 4, a call object is assigned to the $f lambda variable with the use variables passed as arguments. It is that simple:

$f = array('func'=>'anon_writefile', 'args'=>array('j'=>$i, 'g'=>$f));

Once the callback chain has been created, it needs to be triggered. A PHP 4 lc_call() call starts the callback chain:

lc_call($f);

Put together, here’s the PHP 4 calling code:

$f = array('func'=>'anon_noop');
for ($i=$n-1; $i >= 0; --$n) {
  $f = array('func'=>'anon_writefile', 'args'=>array('j'=>$i, 'g'=>$f));
}
lc_call($f);

The Node.js version is similar:

f = {func':'anon_noop'};
for (var i=n-1; i >= 0; --n) {
  f = {'func':'anon_writefile', 'args':{'j':i, 'g':f}};
}
lc_call(f);

Although using arrays directly will work, some convenience functions would be nice for PHP 4 production code.

A PHP 4 lf_create() convenience function is useful for creating lambda functions instead of direct array definition:

function lf_create($name) {
  return array('func'=>$name);
}

Here’s the Node.js version of the lf_create() function:

function lf_create(name) {
  return {'func':name};
}

A PHP 4 lc_create() convenience function is useful for creating call objects:

function lc_create($name, $args) {
  return array('func'=>$name, 'args'=>$args)
}

The lc_create() function is easily converted to Node.js:

function lc_create(name, args) {
  return {'func':name, 'args':args};
}

With the three PHP 5.3 steps converted to PHP 4, plus the two convenience functions, the PHP 4 code can be put together.

Here’s the PHP 5.3 version:

$f = function() {
};
for ($i=$n-1; $i >= 0; --$n) {
  $f = function() use ($f, $i) {
    $g = $f;
    $j = $i;
    $fp = fopen('fp.txt'+$j, 'w');
    fwrite($fp, $data);
    fclose($fp);
    $g();
  }
}
$f();

Here’s the PHP 4 version, which also works on PHP 5.3:

function lf_anon_noop($lc) {
}

function lf_anon_writefile($lc) {
  $j = $lc['args']['j'];
  $g = $lc['args']['g'];
  $fp = fopen('fp.txt'+$j, 'w');
  fwrite($fp, $data);
  fclose($fp);
  lc_call($g);
}

function lf_create($name) {
  return array('func'=>$name);
}

function lf_call($lf, $a) {
  $lf['args'] = $a;
  return lc_call($lf);
}

function lc_create($name, $args) {
  return array('func'=>$name, 'args'=>$args)
}

function lc_call($lc) {
  if ($lc['func'] == 'anon_noop') {
    lf_anon_noop($lc);
  } else if ($lc['func'] == 'anon_writefile') {
    lf_anon_writefile($lc);
  }
  return $lc;
}

$f = lf_create('anon_noop');
for ($i=$n-1; $i >= 0; --$n) {
  $f = lc_create('anon_writefile', array('j'=>$i, 'g'=>$f));
}
lc_call($f);

Here’s the PHP 4 version converted to Node.js:

function lf_anon_noop(lc) {
}

function lf_anon_writefile(lc) {
  var j = lc['args']['j'];
  var g = lc['args']['g'];
  fs.open('fp.txt'+j, 'w', 0666, function(error, fp) {
    fs.write(fp, data, null, 'utf-8', function() {
      fs.close(fp, function(error) {
        lc_call(g);
      }
    });
  });
}

function lf_create(name) {
  return {'func':name};
}

function lf_call(lf, a) {
  lf['args'] = a;
  return lc_call(lf);
}

function lc_create(name, args) {
  return {'func':name, 'args':args};
}

function lc_call(lc) {
  if (lc['func'] == 'anon_noop') {
    lf_anon_noop(lc);
  } else if (lc['func'] == 'anon_writefile') {
    lf_anon_writefile(lc);
  }
  return lc;
}

var f = lf_create('anon_noop');
for (var i=n-1; i >= 0; --n) {
  f = lc_create('anon_writefile', {'j':i, 'g':f});
}
lc_call(f);

The simulated anonymous functions and lambda variables are compatible with everything: PHP 4, PHP 5.3, and Node.js. These simulation techniques allow arbitrary PHP 4 code to be refactored such that it can be converted to Node.js and both the PHP and Node.js codebases kept closely synchronized while both are improved and new features added.

Some optional improvements are worth mentioning.

Although implementing simulated anonymous functions as standalone functions works fine, there may be cases when anonymous functions either need to be attached to existing classes or a class full of anonymous functions is useful for organizational reasons.

Adding an obj key to the lambda variable allows simulated anonymous functions to be added to classes. The lfo_create() and lco_create() convenience functions, where the “o” stands for “object,” are object-based versions of the lf_create() and lc_create() convenience functions. When these alternative functions are used, the lc_call() function can invoke simulated anonymous functions that are attached to objects. Here’s the PHP 4 code:

function lfo_create($obj, $name) {
  return array('obj'=>$obj, 'func'=>$name);
}

function lco_create($obj, $name, $args) {
  return array('obj'=>$obj, 'func'=>$name, 'args'=>$args);
}

function lc_call($lc) {
  if ($lc['func'] == 'anon_noop') {
    $lc['obj']->lf_anon_noop($lc);
  } else if ($lc['func'] == 'anon_writefile') {
    $lc['obj']->lf_anon_writefile($lc);
  }
  return $lc;
}

These PHP 4 functions can be converted to Node.js:

function lfo_create(obj, name) {
  return {'obj':obj, 'func':name};
}

function lco_create(obj, name, args) {
  return {'obj':obj, 'func':name, 'args':args};
}

function lc_call(lc) {
  if (lc['func'] == 'anon_noop') {
    lc['obj'].lf_anon_noop($lc);
  } else if (lc['func'] == 'anon_writefile') {
    lc['obj'].lf_anon_writefile(lc);
  }
  return lc;
}

Chapter 8 shows how to convert PHP classes to Node.js classes. The implementation and usefulness of adding simulated anonymous functions to classes will become more apparent in that chapter.

In the next chapter, the differences between sending an HTTP response in PHP and in Node.js will be addressed. As might be expected, HTTP responses in PHP are sent using a blocking technique whereas HTTP responses in Node.js are sent using a nonblocking technique. The linearity-based conversion recipes from Chapter 3 and the simulation-based conversion recipes from this chapter will be the foundation for refactoring PHP HTTP response code to be easily converted to Node.js.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required