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.