periklis
periklis

Reputation: 10188

Passing a reference using call_user_func_array with variable arguments

I have the following code:

<?php
class Testme {
    public static function foo(&$ref) {
        $ref = 1;
    }
}
call_user_func_array(array('Testme', 'foo'), array(&$test));
var_dump($test);

And correctly displays "1". But I want to do the same, using an "Invoker" method, like the following:

<?php
class Testme {
    public static function foo(&$ref) {
        $ref = 1;
    }
}

class Invoker {
    public static function invoke($func_name) {
        $args = func_get_args();
        call_user_func_array(array('Testme', $func_name), array_slice($args,1));
    }
}

$test = 2;
Invoker::invoke('foo', $test);
var_dump($test);

This throws a strict standards error (PHP 5.5) and displays "2"

The question is, is there a way to pass arguments by reference to Testme::foo, when using func_get_args()? (workarounds are welcome)

Upvotes: 1

Views: 2040

Answers (3)

Jon
Jon

Reputation: 437386

The problem

This is not possible to do easily because func_get_args does not deal in references, and there is no alternative that does.

The idea

If you are willing to limit yourself to a maximum known number of arguments and don't mind working with the dark arts, there is a horrible workaround that I believe works correctly in all cases.

First, declare the invoker as accepting an able number of parameters, all of them by reference and having default values (the exact default does not really matter):

public static function invoke(callable $callable, &$p1 = null, &$p2 = null, ...);

Then, inside invoke determine what type of callable you are dealing with. You need to do this in order to create an appropriate instance of ReflectionFunctionAbstract that describes the invocation target. This is important because we absolutely need to determine how many parameters the target requires, and it also enables amenities like detecting a call with an incorrect number of arguments.

After assembling an array of arguments, use call_user_func_array like you were intending to in the first place.

This approach is based on the same idea that invisal uses, but there is an important difference: using reflection allows you to always correctly determine how many arguments to pass (invisal's solution uses a guard value), which in turn does not limit the values that can be passed to the invocation target (with invisal's solution you cannot ever pass the guard value to the invocation target as a legitimate parameter).

The code

public static function invoke(callable $callable, &$p1 = null, &$p2 = null)
{
    if (is_string($callable) && strpos($callable, '::')) {
        // Strings are usually free function names, but they can also
        // specify a static method with ClassName::methodName --
        // if that's the case, convert to array form
        $callable = explode('::', $callable);
    }

    // Get a ReflectionFunctionAbstract instance that will give us
    // information about the invocation target's parameters
    if (is_string($callable)) {
        // Now we know it refers to a free function
        $reflector = new ReflectionFunction($callable);
    }
    else if (is_array($callable)) {
        list ($class, $method) = $callable;
        $reflector = new ReflectionMethod($class, $method);
    }
    else {
        // must be an object -- either a closure or a functor
        $reflector = new ReflectionObject($callable);
        $reflector = $reflector->getMethod('__invoke');
    }

    $forwardedArguments = [];
    $incomingArgumentCount = func_num_args() - 1;
    $paramIndex = 0;

    foreach($reflector->getParameters() as $param) {
        if ($paramIndex >= $incomingArgumentCount) {
            if (!$param->isOptional()) {
                // invocation target requires parameter that was not passed,
                // perhaps we want to handle the error right now?
            }

            break; // call target will less parameters than it can accept
        }

        $forwardedArguments[] = &${'p'.(++$paramIndex)};
    }

    return call_user_func_array($callable, $forwardedArguments);
}

See it in action.

Upvotes: 1

bubba
bubba

Reputation: 3847

There is no way to get a reference out of func_get_args() because it returns an array with a copy of the values passed in. See PHP Reference.

Additionally, since runtime pass by reference is no longer supported, you must denote the reference in each method/function signature. Here is an example that should work around the overall issue of having an Invoker that does pass by reference, but there is no work around for func_get_args().

<?php

class Testme {
    public static function foo(&$ref) {
        $ref  = 1;
    }
}

class Invoker {
    public static function invoke($func_name, &$args){
           call_user_func_array(array('Testme', $func_name), $args);
    }
}

$test  = 10;

$args[] = &$test;

Invoker::invoke('foo', $args);

var_dump($test);

If you know you want to invoke by reference, this can work for you and perhaps have two invokers, one Invoker::invokeByRef an another normal Invoker::invoke that does the standard invoking by copy.

Upvotes: 3

invisal
invisal

Reputation: 11171

It is not possible because func_get_args() returns copy value not reference. But there is an ugly workaround for it by using many optional parameters.

class Testme {
    public static function foo(&$ref, &$ref2) {
        $ref = 1;
        $ref2 = 2;
    }
}

class Invoker {
    public static function invoke($func_name, 
        &$arg1 = null, &$arg2 = null, &$arg3 = null, 
        &$arg4 = null, &$arg5 = null, &$arg6 = null) 
    {

        $argc = func_num_args();
        $args = array();
        for($i = 1; $i < $argc; $i++) {
            $name = 'arg' . $i;
            if ($$name !== null) {
                $args[] = &$$name;
            }
        }
           call_user_func_array(array('Testme', $func_name), $args);
    }
}

$test = 5;
$test2 = 6;
Invoker::invoke('foo', $test, $test2);

var_dump($test);
var_dump($test2);

Upvotes: 3

Related Questions