Last active
November 18, 2022 23:58
-
-
Save johnkary/904de9171396909c1842 to your computer and use it in GitHub Desktop.
Exposing protected and private object methods using \Closure::bindTo(). Code below requires PHP 5.6+ due to argument unpacking (...$args). Could probably be written for PHP 5.4+ but didn't want to reinvent argument unpacking, else could also just use the Reflection implementation.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class User { | |
private $firstname; | |
private $lastname; | |
public function __construct($f, $l) { | |
$this->firstname = $f; | |
$this->lastname = $l; | |
} | |
private function buildFullName() { | |
return sprintf('%s %s', $this->firstname, $this->lastname); | |
} | |
private function greeting($say) { | |
return sprintf('%s, %s', $say, $this->buildFullName()); | |
} | |
private function greetingToUser($say, User $user) { | |
return sprintf('%s from %s', $this->greeting($say), $user->buildFullName()); | |
} | |
} | |
/** | |
* Exposes result of a protected/private method call outside the | |
* object's scope. | |
* | |
* @param object $object Object on which to call method | |
* @param string $method Method to call | |
* @param array $args Arguments to be unpacked and passed to Method | |
* @return mixed Result of invoking Method on Object given Arguments | |
*/ | |
function xray($object, $methodToExpose, array $args = []) { | |
$fn = function () use ($methodToExpose, $args) { | |
return $this->$methodToExpose(...$args); | |
}; | |
$exposed = $fn->bindTo($object, $object); | |
return $exposed(); | |
}; | |
// The traditional way | |
function xray_reflection($object, $methodToExpose, array $args = []) { | |
$m = new \ReflectionMethod($object, $methodToExpose); | |
$m->setAccessible(true); | |
return $m->invoke($object, ...$args); | |
}; | |
$john = new User('John', 'Kary'); | |
$chris = new User('Chris', 'Escalante'); | |
echo implode("\n", [ | |
xray($john, 'greeting', ['Hello']), | |
xray_reflection($chris, 'greetingToUser', ['Hello', $john]), | |
]); | |
// Output: | |
// Hello, John Kary | |
// Hello, Chris Escalante from John Kary |
replace:
return $this->$methodToExpose(...$args);
with:
return call_user_func_array([$this, $methodToExpose], $args);
and you have it.
Sorry, I'm late to the party but this actually helped me solve a problem that Reflection didn't.
I have a class extending another and I needed to call a protected method that belongs to the parent's class using the child's object. Reflection only allows you to call methods using the object the method was declared in so it was not working.
Your method works also with the child object which solved my problem.
Thanks for this!
I ran into the problem of the (new ReflectionMethod($object, 'method'))->invoke(...)
method not respecting declare(strict_types=1)
whereas this Closure method does 👍
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
What the heck am I looking at? You can use
$this
inside a closure?Why yes you can! Use bindTo() to change which class scope is to be used when the function is invoked.
From \Closure::bindTo() Description
Why would you even want to do this?
Well, you wouldn't.
I am in the "don't directly test private methods" camp. Other people who are more stubborn disagree, but an object's public methods are the contract it honors--the inputs it agrees to accept and the output it agrees to return--and by testing its internals you can no longer refactor the internal implementation without the test suite breaking. The test suite failing when refactoring internal methods is a sign you're testing at the wrong abstraction and the tests know too much about the implementation.
I would only suggest writing a test against private methods in a legacy code scenario: write the test exposing the private method, perform an extract class refactor on the private method moving it to a new public method on a new class, tests should still pass so write another test for the new class' public method, once passing again remove the private method test. The private method test should never be committed to source control.
Erm, we have Reflection for this, right?
Yep. But with all the bemoaning about how Reflection is slow I wondered if this might somehow be more efficient. Turns out this closure approach is a tiny tiny bit slower at 100,000 iterations. Unscientific benchmarks comparing Reflection vs Closure::bindTo() are below.
https://blackfire.io/profiles/compare/7c69bb80-e166-4ee6-894d-1407a198fd62/graph
Anyone have other optimization ideas?
So should I start doing it this way?
Naa. If using bindTo() was somehow faster or more memory efficient than ReflectionMethod this might make a good library? Right now it's not, at least in PHP 5.6.17. Maybe I should test against PHP 7.
At least now you know about dynamically binding scope to closures, and that the concept of
$this
is more fluid than you knew 5 minutes ago.References
...$args
) -- http://php.net/manual/en/migration56.new-features.php#migration56.new-features.splat