Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get the number of parameters of a run-time determined callable?

NOTE: By virtue of writing this quesiton, I've already figured out that I was being overly enthousiastic about using a new language feature. The far cleaner solution was using a Strategy Pattern instead... still, I'm curious if there's a proper way to go about this problem.

TL;DR: Can you reflect on a generic Callable in PHP without resorting to manually typechecking all kinds of callable?

In PHP 5.4 we've got a new typehint: callable. This seems like a lot of fun. I thought I'd make use of this through the following:

<?php
    public function setCredentialTreatment(callable $credentialTreatment) {
       // Verify $credentialTreatment can be used (ie: accepts 2 params)
       ... magic here ...
    }
?>

So far my line of thought has been to do a series of type-checks on the callable, and inferring from that which Reflection* class to use:

<?php
if(is_array($callable)) {
    $reflector = new ReflectionMethod($callable[0], $callable[1]);
} elseif(is_string($callable)) {
    $reflector = new ReflectionFunction($callable);
} elseif(is_a($callable, 'Closure') || is_callable($callable, '__invoke')) {
    $objReflector = new ReflectionObject($callable);
    $reflector    = $objReflector->getMethod('__invoke');
}

// Array of ReflectionParameters. Yay!
$parameters = $reflector->getParameters();
// Inspect parameters. Throw invalidArgumentException if not valid.
?>

Now, to me, this feels overly complicated. Am I missing some kind of shortcut way to achieving what I'm trying to do here? Any insight would be welcomed :)

like image 926
kander Avatar asked Oct 25 '12 14:10

kander


2 Answers

I created a shorter version that works much like call_user_func() and tested it on all different types in the PHP manual for callbacks/callables. This way you can use it almost anywhere. call_user_func() doesn't take objects only, and it didn't make sense to me that this function should either, since it is only handling callbacks.

Tests and suggestions on how to use it are included below and I hope this helps:

function getNrOfParams($callable)
{
    $CReflection = is_array($callable) ? 
    new ReflectionMethod($callable[0], $callable[1]) : 
    new ReflectionFunction($callable);
    return $CReflection->getNumberOfParameters();
}

The test and its result in its entirety:

<?php   
class Smart
{
    public function __invoke($name)
    {

    }

    public function my_callable($one, $two, $three)
    {

    }

    public static function myCallableMethod($one, $two) 
    {

    }

    public static function who()
    {
            echo "smart the parent class";
    }
}

class Smarter extends Smart
{
    public static function who()
    {
        echo "smarter";
    }
}

function my_ca($one)
{

}

function getNrOfParams($callable)
{
    $CReflection = is_array($callable) ? new ReflectionMethod($callable[0], $callable[1]) : new ReflectionFunction($callable);
    return $CReflection->getNumberOfParameters();
}
// Test 1 Callable Function
echo "Test 1 - Callable function:" . getNrOfParams('my_ca');

// Test 2 Static method
echo " Test 2 - Static class method:" . getNrOfParams(array('Smart', 'myCallableMethod'));

// Test 3 Object method
$smart = new Smart();
echo " Test 3 - Object method:" . getNrOfParams(array($smart, 'my_callable'));

// Test 4 Static method call (As of PHP 5.2.3)
//echo " Test 4 - Static class method call:" . getNrOfParams('Smart::myCallableMethod');
// Calling a static method this way does not work in ReflectionFunction.
// However, Test 2 provides a solution.

// Test 5 Relative static method (As of PHP 5.3.0)
//echo " Test 5 - Relative static class method:" . getNrOfParams(array('Smarter', 'parent::who'));
// Calling a relative static method doesn't work either. ReflectionMethod lacks support.
// All other tests work.

// Tesy 6 __invoke
echo " Test 6 - __invoke:" . getNrOfParams(array($smart, '__invoke'));

// Test 7 Closure
$closure = function($one, $two, $three)
{
    // Magic
};
echo " Test 7 - Closure:" . getNrOfParams($closure);
like image 184
Sceluswe Avatar answered Oct 10 '22 21:10

Sceluswe


TL;DR I don't think so. You need to check for all callable types if you want a generic solution.

The following function can be used to get a ReflectionFunctionAbstract instance for any generic callable in PHP:

function reflectionOf(callable $callable): ReflectionFunctionAbstract
{
    if ($callable instanceof Closure) {
        return new ReflectionFunction($callable);
    }
    if (is_string($callable)) {
        $pcs = explode('::', $callable);
        return count($pcs) > 1 ? new ReflectionMethod($pcs[0], $pcs[1]) : new ReflectionFunction($callable);
    }
    if (!is_array($callable)) {
        $callable = [$callable, '__invoke'];
    }
    return new ReflectionMethod($callable[0], $callable[1]);
}

Then it is possible to get the number of parameters as follows:

reflectionOf($func)->getNumberOfParameters();

Hope this helps to someone. This answer might be late to the party, but none of the other solutions provide a full coverage for generic callables.

like image 36
dakujem Avatar answered Oct 10 '22 21:10

dakujem