Last active
September 2, 2021 21:27
-
-
Save goodevilgenius/c34b5027dc5d6fe6677d81b32bdd397d to your computer and use it in GitHub Desktop.
Class to access private/protected methods/properties without Reflections. Requires PHP 8 (could be rewritten to support 7)
This file contains hidden or 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 | |
/** | |
* Method for accessing private/protected properties/methods without Reflection. | |
* | |
* This is particularly useful when debugging from the command-line, for example, with psysh. | |
* | |
* Static methods/properties can be accessed by passing the class instead. | |
* Constants are not acccessible. | |
* | |
* Usage: | |
* $object; | |
* spy($object)->prop; // Return private prop on $object | |
* | |
* // These two lines are equivalent | |
* $objSpy = spy($object); | |
* $objSpy = new ClassProxy($object); | |
* | |
* $objSpy->protectedMethod($param1); | |
* $objSpy->call(fn () => strtolower($this->prop)); // Returns lowercased version of private prop on $object | |
* | |
* spy(SomeClass::class)->staticProjectedProperty; | |
*/ | |
declare(strict_types=1); | |
/** | |
* Proxies all calls to another object/class, via Closures. | |
* | |
* This allows access to private/protected methods and properties without Reflection. | |
*/ | |
class ClassProxy | |
{ | |
protected ?string $class; | |
protected ?object $object; | |
protected Closure $caller; | |
protected Closure $getter; | |
protected Closure $setter; | |
/** | |
* @param string|object $classOrObject | |
*/ | |
public function __construct($classOrObject) | |
{ | |
[$this->object, $this->class] = is_object($classOrObject) ? | |
[$classOrObject, get_class($classOrObject)] : | |
( | |
is_string($classOrObject) && class_exists($classOrObject) ? | |
[null, $classOrObject] : | |
[null, null] | |
); | |
} | |
/** | |
* Get the closure used by $this->__call. | |
*/ | |
protected function getCaller(): \Closure | |
{ | |
return $this->caller ??= ( | |
$this->object ? | |
fn (string $method, array $args) => $this->$method(...$args) : | |
fn (string $method, array $args) => static::$method($args) | |
)->bindTo($this->object, $this->class); | |
} | |
/** | |
* Get the closure used by $this->__get. | |
*/ | |
protected function getGetter(): \Closure | |
{ | |
return $this->getter ??= ( | |
$this->object ? | |
fn (string $key) => $this->$key : | |
fn (string $key) => static::$$key | |
)->bindTo($this->object, $this->class); | |
} | |
/** | |
* Get the closure used by $this->__set. | |
*/ | |
protected function getSetter(): \Closure | |
{ | |
return $this->setter ??= ( | |
$this->object ? | |
fn (string $key, $value) => $this->$key = $value : | |
fn (string $key, $value) => static::$$key = $value | |
)->bindTo($this->object, $this->class); | |
} | |
/** | |
* Run arbitry code on object, with access to private/protected props/methods. | |
*/ | |
public function call(callable $cb, ...$args) | |
{ | |
return (\Closure::fromCallable($cb)->bindTo($this->object, $this->class))(...$args); | |
} | |
public function __call(string $method, array $args) | |
{ | |
return ($this->getCaller())($method, $args); | |
} | |
public function __get(string $key) | |
{ | |
return ($this->getGetter())($key); | |
} | |
public function __set(string $key, $value) | |
{ | |
return ($this->getSetter())($key, $value); | |
} | |
} | |
/** | |
* Instantiates a ClassProxy. | |
* | |
* Useful for quick spying. | |
* | |
* @param string|object $classOrObject | |
*/ | |
function spy($classOrObject): ClassProxy | |
{ | |
return new ClassProxy($classOrObject); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment