3

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.

2
  • I think you just need to call $reflection without bitwise, since you are already returning the reference in your functions. Can you paste the code for invokeArgs function? Commented Oct 15, 2016 at 13:23
  • I can't, because the function is in C. But there's a link to the docs of that function in the beginning of the question. Those docs also state that you do need to include the &, 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. Commented Oct 15, 2016 at 15:47

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

6 Comments

I had thought of that before. However, I reasoned that it wouldn't work, since a function has to be defined as returning by reference, and the closure from #getClosure() will likely not be defined that way. But if my assumption is wrong, does the above code actually work? What happens if you add $items = array('apple'); var_dump($a); below?
I would think that the assignment operator would change the variable to point to the new literal array, but after testing it seems that the instance of A actually has its internal $items array modified to be equal to array (size=1) 0 => string 'apples'. Not quite the behavior I was expecting to be honest.
Yet exactly what I expect and need. Thank you, I will test this soon.
Also, it may be that the closure returned from the $reflection instance is not an actual Closure that wraps around the method, but an actual reference to the method itself. In either case, the reflection class has the returnsReference() method, which makes it aware of the return value's nature. So even if the closure is a wrapper, it is probably built with the reference return in mind.
This is likely the explanation. Looks like I shouldn't have given up so soon. Thanks again.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.