Skip to content

Instantly share code, notes, and snippets.

@nicolas-grekas
Last active September 11, 2017 15:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nicolas-grekas/45a3b0fc9ecb96a1f793d2e363823523 to your computer and use it in GitHub Desktop.
Save nicolas-grekas/45a3b0fc9ecb96a1f793d2e363823523 to your computer and use it in GitHub Desktop.
Wondering about dynamic proxies in PHP
<?php
// Very early draft. Not applicable yet.
// TODO: specify behavior for __destruct()
// Only public methods/properties can be intercepted, final ones also
// Magic methods are intercepted as regular ones
// Undefined properties/methods access are excluded from interception
// method_exists()/property_exists()/isset() are excluded from interception
// new method: Reflection::getInterceptor(): ?object
// true === $fooProxy instanceof Foo
// Foo::class === get_class($fooProxy)
// $fooProxy is transparent to all reflection-like methods
// serialize($foo) === serialize($fooProxy)
// as regular objects, interceptors are interceptable of course
abstract class Interceptor
{
const INTERCEPT_CALL = 1;
const INTERCEPT_GET = 2;
const INTERCEPT_SET = 3;
const INTERCEPT_UNSET = 4;
private $interceptedInstance;
private $class;
public static function proxy(self $interceptor, $inPlace = false): object
{
// returns a proxy of $interceptor->getInterceptedInstance().
// $this->getClass() is used for reflection/instanceof checks
// to prevent calling getInterceptedInstance(), thus prevent instanciating
// the object when doing so
// if $inPlace is true, $interceptor->getInterceptedInstance() is returned,
// with the intercept/interceptRef methods attached to it.
// Should we allow multi-in-place interception?
// Should we allow this at all?
}
public function __construct(string $class)
{
$this->class = $class;
}
abstract public function createInterceptedInstance(): object;
final public function getInterceptedInstance(): object
{
if (null === $this->interceptedInstance) {
$interceptedInstance = $this->createInterceptedInstance();
if (get_class($innner) !== $this->class)) {
throw new TypeError('...');
}
$this->interceptedInstance = $interceptedInstance;
}
return $this->interceptedInstance;
}
final public function getClass(): string
{
return $this->class;
}
public function intercept(int $type, string $methodOrProperty, array $args = null)
{
// For calls, $args contains references or values,
// as specified by the intercepted method's signature
// or by the access type for properties (e.g $foo->bar = &$someVar)
switch ($type) {
case self::INTERCEPT_CALL: return $this->getInterceptedInstance()->$methodOrProperty(...$args);
case self::INTERCEPT_GET: return $this->getInterceptedInstance()->$methodOrProperty;
case self::INTERCEPT_SET: $this->getInterceptedInstance()->$methodOrProperty = &$args[0]; break;
case self::INTERCEPT_UNSET: unset($this->getInterceptedInstance()->$methodOrProperty); break;
}
}
public function &interceptRef(int $type, string $methodOrProperty, array $args)
{
// the engine calls this when the intercepted method's signature returns by ref
// or when a reference to a property is required (e.g $someVar = &$foo->bar)
switch ($type) {
case self::INTERCEPT_CALL: return $this->getInterceptedInstance()->$methodOrProperty(...$args);
case self::INTERCEPT_GET: return $this->getInterceptedInstance()->$methodOrProperty;
}
}
}
// -- EXAMPLE --
// A generic lazy instantiator
class LazyInterceptor extends Interceptor
{
private $factory;
public function __construct(callable $factory, string $class)
{
$this->factory = $factory;
parent::__construct($class);
}
public function createInterceptedInstance(): object
{
$factory = $this->factory;
return $factory();
}
}
class Foo
{
function doSomething()
{
}
}
$lazyFoo = Interceptor::proxy(new LazyInterceptor(function () { return new Foo(); }, Foo::class));
@nicolas-grekas
Copy link
Author

Answer from Ryan: all of them, no need to chose :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment