Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing for a PHP Closure without referring to the Closure internal class

Tags:

closures

php

The PHP manual for anonymous functions (ie, Closures) states that:

Anonymous functions are currently implemented using the Closure class. This is an implementation detail and should not be relied upon.

(Emphasis is my own)

Is it possible to test a variable, such that the test returns true only if the variable is a Closure, without referring to the Closure class?

In other words, how can I rewrite the following such that it will raise an error when $bar is anything but an anonymous function:

function foo(Closure $bar) {
    $bar();
}

EDIT: Based on the answers received, here is an example test.

Notes:

  1. It seems there is no way to differentiate between Functors and Closures, and that the test is probably just as 'implementation specific' as using the Closure class.
  2. The (seemingly obvious) ReflectionFunction::isClosure() method seems to be be almost useless: by the time you've done the checks required to make sure that ReflectionFunction can actually be instantiated (can't take a Class except for a Closure), you've eliminated all other options.
  3. In 5.3.0 you ReflectionClass($closure)->hasMethod('__invoke') returned false, so this could be used as a test against Functors, however (I'm told) this has changed since. This highlights the frailty of the solution too.
  4. Follow up from Gordon - As of PHP 5.4 you can rely on Closure being a Closure: php.net/manual/en/class.closure.php

Code:

/**
 * Return true if and only if the passed argument is a Closure.
 */
function testClosure($a) {
    // Must be Callback, Labmda, Functor or Closure:
    if(!is_callable($a)) return false;

    // Elminate Callbacks & Lambdas
    if(!is_object($a)) return false;

    // Eliminate Functors
    //$r = new ReflectionFunction($a); <-- fails if $a is a Functor
    //if($r->isClosure()) return true;

    return false;
}

Test case:

//////////// TEST CASE /////////////

class CallBackClass {
    function callBackFunc() {
    }
}

class Functor {
    function __invoke() {
    }
}

$functor = new Functor();
$lambda = create_function('', '');
$callback = array('CallBackClass', 'callBackFunc');
$array = array();
$object = new stdClass();
$closure = function() { ; };

echo "Is it a closure? \n";
echo "Closure: " . (testClosure($closure) ? "yes" : "no") . "\n";
echo "Null: "  . (testClosure(null) ? "yes" : "no") . "\n";
echo "Array: " . (testClosure($array) ? "yes" : "no") . "\n";
echo "Callback: " . (testClosure($callback) ? "yes" : "no")  . "\n";
echo "Labmda: " .(testClosure($lambda) ? "yes" : "no") . "\n";
echo "Invoked Class: " . (testClosure($functor) ? "yes" : "no")  . "\n";
echo "StdObj: " . (testClosure($object) ? "yes" : "no") . "\n";

-

like image 766
Hamish Avatar asked Nov 08 '10 20:11

Hamish


2 Answers

You can also use

ReflectionFunctionAbstract::isClosure — Checks if closure

Example:

$poorMansLambda = create_function('', 'return TRUE;');
$rf = new ReflectionFunction($poorMansLambda);
var_dump( $rf->isClosure() ); // FALSE

$lambda  = function() { return TRUE; };   
$rf = new ReflectionFunction($lambda);
var_dump( $rf->isClosure() ); // TRUE

$closure = function() use ($lambda) { return $lambda(); };    
$rf = new ReflectionFunction($closure);
var_dump( $rf->isClosure() ); // TRUE

Note that the above will only return TRUE for PHP 5.3 Lambdas and Closures. If you just want to know whether an argument can be used as a callback, is_callable will perform better.


EDIT If you want to include Functors as well, you can do (as of PHP 5.3.3)

$rf = new ReflectionObject($functorOrClosureOrLambda);
var_dump( $rf->hasMethod('__invoke') ); // TRUE

or

method_exists($functorOrClosureOrLambda, '__invoke');

with the latter being the faster alternative.

A Closure instance is basically just a class that has an __invoke function which you fed the method body on the fly. But since this is testing for an implementation detail, I'd say it is as unreliable as testing for the Closure Class Name.


EDIT Since you mention you cannot reliably test via the Reflection API due to it raising an error when passing a Functor to ReflectionFunctionAbstract::isClosure, try if the following solution suits your needs:

function isClosure($arg)
{
    if(is_callable($arg, FALSE, $name)) {
        is_callable(function() {}, TRUE, $implementation);
        return ($name === $implementation);
    }
}

This will check if the passed argument is callable. The $name argument stores the callable name. For closures, this is currently Closure::__invoke. Since this will be the same for any Closures/Lambdas, we can compare the name of the passed argument against an arbitrary other Closure/Lambda. If they are equal, the argument must be a Closure/Lambda. Determining the callable name at runtime has the added benefit that you dont have to hardcode assumptions about the implementation details into your sourcecode. Passing a functor will return FALSE, because it wont have the same callable name. Since this does not rely on the Reflection API, it is also likely a bit faster.

The above could be more elegantly written as

function isClosure($arg) {
    $test = function(){};
    return $arg instanceof $test;
}
like image 119
Gordon Avatar answered Oct 10 '22 12:10

Gordon


is_callable and !is_array might help you along. Note that you cannot rely on PHP's type hinting/checking this way, since you would have to check the variable inside the function and throw something, e.g. an InvalidArgumentException yourself.

like image 38
janmoesen Avatar answered Oct 10 '22 12:10

janmoesen