PHP5: Anonymous Functions
Lately I had to do some work for the status.arrowquick.com website that involved learning about anonymous functions.
What I needed to do was add the appropriate CSS class to each hyperlink inside a bit of HTML. To do this, I chose to use preg_replace_callback(), which replaces a matching string with the results of a specified function.
$cat_class_callback_info = null; $cat_class_callback_iter = 0; function AddCategoryClass($html, $cat_info) { global $cat_class_callback_info, $cat_class_callback_iter; $cat_class_callback_info = $cat_info; $cat_class_callback_iter = 0; return preg_replace_callback( '#rel="[^"]*"#', 'AddCategoryClassCallback', $html ); }
What AddCategoryClass() does is take a snippet of HTML and modify it with the results of the AddCategoryClassCallback() function. It also accepts a WordPress array of categories that matches each hyperlink in $html at a 1:1 ratio.
Here is the AddCategoryClassCallback() function:
function AddCategoryClassCallback($matches) { global $cat_class_callback_info, $cat_class_callback_iter; switch ($cat_class_callback_info[$cat_class_callback_iter]->cat_ID) { case 3: // info $class = 'info'; break; case 4: // scheduled maintenance $class = 'maintenance'; break; case 5: // service disruption $class = 'service-disruption'; break; case 6: // service outage $class = 'service-outage'; break; case 1: // uncategorized default: $class = 'unknown'; break; } $cat_class_callback_iter++; return $matches[0] . ' class="' . $class . '"'; }
The details aren’t important; basically it looks at the next category and appends the appropriate CSS class to the matching HTML snippet.
You’ll notice extensive use of of the $cat_class_callback_* variables in this code. These global variables are used because preg_replace_callback() dictates that only 1 argument is passed to the callback function: the matching string. Therefore, the array of categories has to be stored somewhere where both functions can access it — whether it’s a global variable, or static or member variable. So AddCategoryClass() saves its 2nd parameter into this global and the callback function accesses it later.
Because the categories is an array, we also need to store a pointer to the “current” element so we can iterate through the array. So there are actually 2 global variables. This is a lot of work for something so simple.
Now, PHP4 does allow you to create anonymous functions using create_function(). However, create_function() would only allow us to anonymize the AddCategoryClassCallback() function so it didn’t clutter up the namespace.
function AddCategoryClass($html, $cat_info) { global $cat_class_callback_info, $cat_class_callback_iter; $cat_class_callback_info = $cat_info; $cat_class_callback_iter = 0; return preg_replace_callback( '#rel="[^"]*"#', create_function( '$matches', 'global $cat_class_callback_info, $cat_class_callback_iter; switch [...etc...]' ), $html ); } // no AddCategoryClassCallback() function defined!
It wouldn’t help with the fundamental problem: preg_replace_callback() would still only pass a single argument, so the global variables would still have to be used.
In addition, the parameter list and body of the anonymous function has to be declared in a string. This is very messy — you have to escape everything so that variables aren’t replaced with their values, quotes are nested correctly, etc. And of course IDEs won’t check your code if it’s inside a string. No, create_function() is not good for nontrivial functions.
PHP 5.3 introduced a new type of anonymous function called a closure. These constructs are closer to what you would see in Javascript and functional languages:
- You can write them inline using normal PHP syntax (no string encapsulation).
- Like create_function(), you can save closures to a variable for later, repeated uses.
- Closures can inherit variables from their parent’s scope.
- Closures are basically first-class functions, so anything you can do with a normal declared function can be done with closures as well.
Being able to inherit variables is a particular lifesaver in this situation. Here is the final AddCategoryClass() functions in its entirety:
function AddCategoryClass($html, $cat_info) { return preg_replace_callback( '#rel="[^"]*"#', function($matches) use (&$cat_info) { static $index = 0; switch ($cat_info[$index]->cat_ID) { case 3: // info $class = 'info'; break; case 4: // scheduled maintenance $class = 'maintenance'; break; case 5: // service disruption $class = 'service-disruption'; break; case 6: // service outage $class = 'service-outage'; break; case 1: // uncategorized default: $class = 'unknown'; break; } $index++; return $matches[0] . ' class="' . $class . '"'; }, $html ); }
As you can see, it’s much more readable. The function is declared inline using the “function()” syntax. The category array is explicitly passed using the “use (…)” syntax — here I use “&” to pass it by reference. I found this syntax to be a simple, straightforward method of inheriting variables, especially compared to the relative amount of confusion generated by Javascript closures.
A simple static variable is used to keep track of the array pointer. (This is a limitation of preg_replace_callback(), not closures.)
Anonymous functions are most useful for callbacks, but they can be useful for small or one-shot functions, and in other situations.
|