Last active
February 8, 2024 07:29
-
-
Save kmuenkel/c35ae7fd835774e4f45c5174999d2317 to your computer and use it in GitHub Desktop.
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 | |
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