tl;dr
Use ReflectionMethod::getClosure() to retrieve a closure representing the method. If that closure is invoked directly, it will correctly return a reference, if that was the behaviour of the original method.
This is only possible for PHP version 5.4 or later.
There is no mention of this in the PHP manual, but ReflectionMethod::invokeArgs() is not capable of returning a reference.
The only way to get the returned reference via reflection is to use ReflectionMethod::getClosure(), which returns a Closure instance that represents the original method. If called directly this closure will correctly return the reference, if that was the behaviour of the original method.
I say "if called directly" since the call_user_func_array() function is also incapable to returning references, so you cannot use that function to invoke the closure with an array of arguments.
Note that the ReflectionMethod::getClosure() method is only available for PHP version 5.4 or later.
PHP 5.6+
You can pass an array of arguments to the closure simply by using argument unpacking:
<?php
function &reflectionInvokeByRef($object, $method, $args = array())
{
$reflection = new \ReflectionMethod($object, $method);
$reflection->setAccessible(true);
$closure = $reflection->getClosure($object);
$result = &$closure(...$args);
return $result;
}
$a = new A();
$items = &reflectionInvokeByRef($a, '_getItemStorage');
$items[] = 'yolo';
var_dump($a);
This will ouput
object(A)#1 (1) {
["items":protected]=> &array(1) {
[0]=> string(4) "yolo"
}
}
PHP 5.4 - 5.5
Argument unpacking is not available, so passing an array of arguments has to be done differently.
By exploiting the fact that PHP functions and methods can be given more arguments than is expected (i.e. more than the number of declared parameters), we can pad the arguments array to a preset maximum (ex. 10) and pass the arguments in the traditional way.
<?php
function &reflectionInvokeByRef($object, $method, $args = array())
{
$reflection = new \ReflectionMethod($object, $method);
$reflection->setAccessible(true);
$args = array_pad($args, 10, null);
$closure = $reflection->getClosure($object);
$result = &$closure($args[0], $args[1], $args[2], $args[3], $args[4], $args[5],
$args[6], $args[7], $args[8], $args[9]);
return $result;
}
$a = new A();
$items = &reflectionInvokeByRef($a, '_getItemStorage', array('some', 'args'));
$items[] = 'yolo';
var_dump($a);
This will ouput
object(A)#1 (1) {
["items":protected]=> &array(1) {
[0]=> string(4) "yolo"
}
}
It's not pretty, I admit. This is a dirty solution and I wouldn't be posting it here if there was any other way (that I could think of). You'd have to decide on the maximum number of arguments to pad the arguments array as well - but I think 10 is a safe maximum.
&, which, by the way, is not a bitwise operator, but means "reference. In this case, it's there to indicate that I am assigning the reference, and not the value of the reference.