PHP: Checking a callback function return type
This was an interesting problem to resolve. The background is that we have a find and replace function which behaves differently depending on whether a supplied replacement value is a (callback) function or a string.
The problem is that it can get tripped up by strings such as "Tan" - which can be a name as well as a built-in function - and functions are generally passed as strings.Using is_callable to check for function
Without going into details, our methods can be called as follows:
ClassName::addPattern($find, $replace);
$new_text = ClassName::applyPatterns($text);
Or, in practice:
ClassName::addPattern("%FIRSTNAME%", "Elaine");
ClassName::addPattern("%LASTNAME%", "Tan");
$invoice_final = ClassName::applyPatterns($invoice_template);
And within the applyPatterns method:
...
$fn = is_callable($replace) ? 'preg_replace_callback' : 'preg_replace';
$text = $fn($find, $replace, $text);
...
This works perfectly fine in most situations, but fails on an edge case when the replacement string is also a built-in function. In that case an error is logged:
PHP Warning: tan() expects parameter 1 to be float
We are not concerned about tripping over any locally defined functions as we use a namespace to separate them from 'global' functions.
Using the ReflectionFunction class
It is not enough for a function just to be callable as is_callable('tan') resolves to TRUE while tan($string) returns an error. We need to make sure our callback function accepts and returns a string value.
To check the return type of a function we can use an instance of the ReflectionFunction class.
Let's see how that works with the tan() function:
$reflect = new \ReflectionFunction('tan');
var_dump($reflect->getReturnType());
var_dump("string" == $reflect->getReturnType());
The output for this is not entirely promising:
NULL
bool(false)
But what if we feed it a local function that does return a string?
function doubleText($text)
{
return "{$text}{$text}";
}
$reflect = new \ReflectionFunction(__NAMESPACE__ . '\doubleText');
var_dump($reflect->getReturnType());
var_dump("string" == $reflect->getReturnType());
For details on using __NAMESPACE__ when passing callback functions see our article here.
Unfortunately, the results are the same as before. What's going on?
Declaring a function return type
It turns out that in order for the ReflectionFunction class to do it's job, we have to first define the return type of our function. This is done as follows:
function doubleText($text) : string
{
return "{$text}{$text}";
}
Now we can see that the ReflectionFunction output has changed:
object(ReflectionNamedType)#7 (0) { }
bool(true)
But that first value is not what we expected - it turns out that a recent update changed the return value of getReturnType to an object and not a string.
To get the actual type name you need a further function call:
var_dump($reflect->getReturnType()->getName());
Which will output the expected value:
string(6) "string"
But as you can see we were already able to compare the object return value to "string" as the comparison invokes the objects internal toString method.
Putting it all together
Using everything we've learned we are now able to identify and use only functions where the return value has been defined as being a string as callback functions (preg_replace_callback). Anything else will be treated as a string (preg_replace):
<?PHP
...
if(is_callable($replace)) {
/* parameter is a callable function */
$reflect = new \ReflectionFunction($replace);
if("string" == $reflect->getReturnType()) {
/* function return type is string */
$fn = 'preg_replace_callback';
} else {
$fn = 'preg_replace';
}
} else {
$fn = 'preg_replace';
}
$text = $fn($find, $replace, $text);
...
?>
You will note that we are essentially excluding most/all built-in functions as they will not have a return type defined. But that's ok as the intended usage was to accept either a string or a local function.
If we ever do want to pass a built-in function to use as a callback in this way we can always write a local wrapper.
Extra checks could be done on the supplied function using ReflectionFunction to make sure that it accepts the correct number and type of variables.