Skip to content

Instantly share code, notes, and snippets.

@goodevilgenius
Last active September 2, 2021 21:27
Show Gist options
  • Save goodevilgenius/c34b5027dc5d6fe6677d81b32bdd397d to your computer and use it in GitHub Desktop.
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)
<?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