Page MenuHomePhabricator

TestingAccessWrapper can't be used with methods that pass arguments by reference
Open, Needs TriagePublic

Description

TestingAccessWrapper can't be used with methods that pass arguments by reference. Attempting to do it causes a warning like "PHPUnit\Framework\Error\Warning: Parameter 1 to ... expected to be a reference, value given", and passes the parameter by value:

class A {
	private static function func( &$ref ) {
		$ref = 42;
	}
}

$a = Wikimedia\TestingAccessWrapper::newFromClass( A::class );

$b = 0;
$a->func( $b );
echo $b;

Expected output: 42
Actual output:

Warning: Parameter 1 to A::func() expected to be a reference, value given in ...TestingAccessWrapper.php on line 72                                                                
0

Event Timeline

Funnily enough, the following works in PHP 5.3, but it is a syntax error in all later versions where call-time pass-by-reference has been removed:

$a->func( &$b );

https://3v4l.org/EOJ07

We noticed the issue in code review of https://gerrit.wikimedia.org/r/c/mediawiki/extensions/DiscussionTools/+/704835. I think it's an unavoidable limitation of __call (per PHP documentation: "None of the arguments of these magic methods can be passed by reference."), but @DannyS712 asked me to file a bug.


Using ReflectionClass / ReflectionMethod directly, we get the same error when doing the following:

$class = new ReflectionClass( A::class );
$func = $class->getMethod( 'func' );
$func->setAccessible( true );
$func->invokeArgs( null, [ $b ] );
echo $b;

…while the following works correctly:

$func->invokeArgs( null, [ &$b ] );

But even if you could determine at runtime which arguments need to be passed as references (seems tricky, ReflectionMethod doesn't seem to have a way to do it; you can maybe parse the output of __toString(), but it's probably a bad idea), you still can't make the arguments of the __call function behave like references.

I tried testing this out, and while you can pass references to the invokeArgs method, the testing access wrapper works by using a __call handler, and the parameters to that are never passed by reference (https://www.php.net/manual/en/language.oop5.overloading.php as noted above)

Perhaps we should add a functionality to TestingAccessWrapper to support calling method that require passing by reference, by using a dedicated method instead of __call ?

$obj = TestingAccessWrapper::newFromClass( Demo::class );
$obj->_callWithReference( 'targetMethod', [ $a, &$b ] );

would invoke Demo::targetMethod with $a and &$b as the parameters - its a bit uglier than how we using TestingAccessWrapper now, where we just call the desired private method, but it should support references.

This would also be useful in core[1] where the failure of TestingAccessWrapper to handle static params was worked around by using ReflectionClass/ReflectionMethod directly.

[1] https://gerrit.wikimedia.org/g/mediawiki/core/+/9dd696882db5117040ce9b38eb235c33270c4407/tests/phpunit/includes/session/SessionManagerTest.php#913

I encountered this with writing tests for CheckUser. Worked around it by doing the reflection in the test itself.

Change 955985 had a related patch set uploaded (by D3r1ck01; author: Derick Alangi):

[testing-access-wrapper@master] TestingAccessWrapper: Support passing arguments by reference

https://gerrit.wikimedia.org/r/955985

Change 955985 abandoned by D3r1ck01:

[testing-access-wrapper@master] TestingAccessWrapper: Support passing arguments by reference

Reason:

See discussion on patch.

https://gerrit.wikimedia.org/r/955985