Skip to content

Instantly share code, notes, and snippets.

@kmuenkel
Last active February 8, 2024 07:29
Show Gist options
  • Save kmuenkel/c35ae7fd835774e4f45c5174999d2317 to your computer and use it in GitHub Desktop.
Save kmuenkel/c35ae7fd835774e4f45c5174999d2317 to your computer and use it in GitHub Desktop.
<?php
namespace Tests\HighOrderReflection;
use BadMethodCallException;
use Closure;
use ReflectionClass;
use ReflectionException;
use Throwable;
use UnexpectedValueException;
/**
* Grant the ability to invoke protected or private methods of the given object or its ancestor
* @template T of object
**/
class _
{
private readonly Closure $scopeRetention;
/**
* @var array{self, string}|null
*/
private ?array $originalScope = null;
public function __construct(
private object &$object
) {
$this->scopeRetention = function (object $newObject) use (&$object) {
$object = $newObject;
};
}
/**
* @throws ReflectionException
*/
public function __call(string $method, array $args = []): mixed
{
$objectReflection = new ReflectionClass($this->object);
do {
if ($objectReflection->hasMethod($method)) {
$response = $objectReflection->getMethod($method)->invokeArgs($this->object, $args);
return $this->nested($response);
} elseif ($objectReflection->hasMethod('__call')) {
$response = $objectReflection->getMethod('__call')->invokeArgs($this->object, [$method, $args]);
return $this->nested($response);
}
} while ($objectReflection = $objectReflection->getParentClass());
throw new BadMethodCallException(sprintf("Method '%s' not found on '%s'.", $method, get_class($this->object)));
}
/**
* @psalm-return mixed|self
* @throws ReflectionException|Throwable
*/
public function __get(string $property): mixed
{
$objectReflection = new ReflectionClass($this->object);
do {
if ($objectReflection->hasProperty($property)) {
$response = $objectReflection->getProperty($property)->getValue($this->object);
return $this->nested($response, $property);
} elseif ($objectReflection->hasMethod('__get')) {
try {
$response = $objectReflection->getMethod('__get')->invokeArgs($this->object, [$property]);
return $this->nested($response, $property);
} catch (Throwable $exception) {
if (!str_starts_with($exception->getMessage(), 'Cannot bind closure to scope of internal class')) {
throw $exception;
}
return $this->getFromCache($property);
}
}
} while ($objectReflection = $objectReflection->getParentClass());
trigger_error(sprintf('Undefined property: %s::$%s', get_class($this->object), $property), E_USER_WARNING);
return null;
}
/**
* @throws ReflectionException|Throwable
*/
public function __set(string $property, mixed $value): void
{
$value = $value instanceof self ? $value->object : $value;
$mutableReflection = $objectReflection = new ReflectionClass($this->object);
$mutableObject = $objectReflection->newInstanceWithoutConstructor();
do {
foreach ($mutableReflection->getProperties() as $originalProperty) {
$originalProperty->isInitialized($this->object)
&& $originalProperty->getName() != $property
&& $mutableReflection
->getProperty($originalProperty->getName())
->setValue($mutableObject, $originalProperty->getValue($this->object));
}
} while ($mutableReflection = $mutableReflection->getParentClass());
($this->scopeRetention)($this->object = $mutableObject);
do {
if ($objectReflection->hasProperty($property)) {
$objectReflection->getProperty($property)->setValue($this->object, $value);
$this->originalScope && $this->originalScope[0]->{$this->originalScope[1]} = $this;
return;
} elseif ($objectReflection->hasMethod('__set')) {
try {
$objectReflection->getMethod('__set')->invokeArgs($this->object, [$property, $value]);
$this->originalScope && $this->originalScope[0]->{$this->originalScope[1]} = $this;
} catch (Throwable $exception) {
if (!str_starts_with($exception->getMessage(), 'Cannot bind closure to scope of internal class')) {
throw $exception;
}
$this->setFromCache($property, $value);
}
return;
}
} while ($objectReflection = $objectReflection->getParentClass());
trigger_error(sprintf('Undefined property: %s::$%s', get_class($this->object), $property), E_USER_WARNING);
}
/**
* @psalm-return T
*/
public function __(): object
{
return $this->object;
}
public function setOriginalScope(self $originalScope, string $property): self
{
$this->originalScope = [$originalScope, $property];
return $this;
}
/**
* @psalm-param T $object
* @psalm-return T|self
*/
public static function _(object &$object): object
{
return new self($object);
}
private function nested(mixed $var, string $property = null): mixed
{
$convert = function (mixed $var) use ($property): mixed {
if (is_object($var) && !($var instanceof self)) {
$wrapped = self::_($var);
return $property ? $wrapped->setOriginalScope($this, $property) : $wrapped;
} elseif (is_array($var)) {
return array_map([$this, 'nested'], $var);
}
return (is_object($var) && !($var instanceof self)) ? self::_($var) : $var;
};
return is_array($convert) ? array_map($convert, $var) : $convert($var);
}
private function getFromCache(string $property): mixed
{
$objectReflection = new ReflectionClass($this->object);
foreach ($objectReflection->getProperties() as $originalProperty) {
if (preg_match('/^(valueHolder)(.{5})$/', $originalProperty->getName())) {
$this->object = $originalProperty->getValue($this->object);
($this->scopeRetention)($this->object);
$response = self::_($this->object)->$property;
return $this->nested($response, $property);
}
}
throw new UnexpectedValueException('Unable to locate cached value holder instance.');
}
private function setFromCache(string $property, mixed $value): void
{
$objectReflection = new ReflectionClass($this->object);
foreach ($objectReflection->getProperties() as $originalProperty) {
if (preg_match('/^(valueHolder)(.{5})$/', $originalProperty->getName())) {
$this->object = $originalProperty->getValue($this->object);
($this->scopeRetention)($this->object);
self::_($this->object)->$property = $value;
$this->originalScope && $this->originalScope[0]->{$this->originalScope[1]} = $this;
return;
}
}
throw new UnexpectedValueException('Unable to locate cached value holder instance.');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment