PHP Callables

Fixing the $callable() Paradox

June 20, 2015

PHP has long supported variable functions, which allows code to dynamically call a function referenced by a variable.

A variable can reference a function in four different ways:

  1. The name of the function as a string. This works for regular and built-in functions, such as strlen().
  2. A two-element array, with an object instance or class name as a string at [0] and the method name at [1].
  3. A string of the form 'ClassName::methodName'.
  4. The variable can contain a Closure, created using the $callable = function () { ... } syntax.

Assuming a valid function is referenced, all four of methods will return true from is_callable() and be accepted by the callable type declaration. All four methods may be used with call_user_func() or call_user_func_array().

The $callable() Syntax Paradox

The more appealing, readable, and faster $callable() syntax may also be used to invoke a function referenced by the variable $callable. Except there’s a problem with this syntax: if $callable is a string of the form 'ClassName::methodName', attempting to call the function using $callable() will result in the following error:

Fatal error: Call to undefined function ClassName::methodName()

This is odd behavior, especially if the variable value was confirmed to be callable by either is_callable() or the callable type declaration. This is because the PHP engine uses a one method to determine if a variable value is callable, and another method to actually invoke the function referenced by the variable.

This poses a problem for code accepts a callback function. Using the more elegant $callable() syntax will be faster and more readable, but also could result in a fatal error if given a string referencing a static method. Many programmers are not aware of the problem, since using a static method as a callback function is not terribly common. There is also a work-around for the problem by using an array to reference the static method: ['ClassName', 'methodName'].

This solution isn’t perfect though. Telling users of your code that they can’t use a particular method that should be perfectly valid is far from optimal. However, reducing the performance and readability of your code to support a seldom-used syntax also seems like a poor choice.

I am working on a library called Icicle for writing asynchronous code in PHP. The library invokes callback functions that when events occur or tasks complete. Since callback function invocation is very common, using the $callable() syntax was a must, but came at the cost of limiting how users could specify a callback.

Fixing the Paradox

When a PHP file is parsed, it is converted to a set of opcodes that can then be executed by the PHP virtual machine. In PHP 7, The $callable() syntax will produce the opcode ZEND_INIT_DYNAMIC_CALL (this is partly why this syntax is so much faster than call_user_func(), because it avoids the additional function call). To fix the $callable() paradox, the code in the PHP virtual machine (VM) handling this opcode needed to be updated to recognize 'ClassName::methodName' strings as being callable.

When the variable referenced by $callable is a string, the PHP VM assumed that the string referenced a regular function name, such as strlen. To fix the issue, I simply modified the VM to first search for :: within the function name and then split the string into the class name and method name if a :: is found in the string. Once split into the class name and method name, the call is handled in the same way as a callable defined using the ['ClassName', 'methodName'] syntax.

For those that are curious, the code described above that handles the ZEND_INIT_DYNAMIC_CALL is found in the PHP 7 source in Zend/zend_vm_def.h at line 3229 (at the time of this writing, search for ZEND_INIT_DYNAMIC_CALL if necessary).

Use $callable() Everywhere!

In PHP 7 the $callable() syntax should be used for every dynamic function call. There is no reason to use call_user_func() or call_user_func_array() anymore. Remember that the splat operator (...) can be combined with $callable() to use an array as parameters to a function: $callable(...$args).

Now that the $callable() syntax supports 'ClassName::methodName' dynamic callbacks in PHP 7 can be fast and support all methods for defining a callable.