开发者

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

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 开发者_如何学Cvariable 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";

-


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;
}


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜