Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP Reflection Method > Returning Reference Impossible

I needed to test if my abstract class can correctly return a reference to an internal array from a protected method. The only way to test a protected method is via a reflection. However, it doesn't seem to be possible to have a reference returned by ReflectionMethod::invokeArgs(). Or am I doing something wrong here?

class A
{
   protected $items;

    protected function &_getItemStorage()
    {
        return $this->items;
    }
}

function &reflectionInvokeByRef($object, $method, $args = array())
{
    $reflection = new \ReflectionMethod($object, $method);
    $reflection->setAccessible(true);
    $result = &$reflection->invokeArgs($object, $args);

    return $result;
}

$a = new A();
$items = &reflectionInvokeByRef($a, '_getItemStorage');

This results in the following error: Only variables should be assigned by reference
On line: $result = &$reflection->invokeArgs($object, $args);

This error is normally thrown when trying to assign a return value of a function by reference, whereas the function is not declared as returning by reference. Makes my functionality un-testable using any known method.

like image 449
XedinUnknown Avatar asked Aug 08 '26 16:08

XedinUnknown


1 Answers

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.

like image 77
Mecha Avatar answered Aug 11 '26 05:08

Mecha



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!