Implement ObjectProxy class

This commit is contained in:
Dan Jones 2021-10-15 10:43:30 -05:00
commit d857775289
10 changed files with 417 additions and 79 deletions

View file

@ -1,37 +0,0 @@
<?php
/**
* This file is part of danjones000/object-spy
*
* danjones000/object-spy is open source software: you can distribute
* it and/or modify it under the terms of the MIT License
* (the "License"). You may not use this file except in
* compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* @copyright Copyright (c) Dan Jones <danjones@goodevilgenius.org>
* @license https://opensource.org/licenses/MIT MIT License
*/
declare(strict_types=1);
namespace Danjones000\Spy;
/**
* An example class to act as a starting point for developing your library
*/
class Example
{
/**
* Returns a greeting statement using the provided name
*/
public function greet(string $name = 'World'): string
{
return "Hello, {$name}!";
}
}

157
src/ObjectProxy.php Normal file
View file

@ -0,0 +1,157 @@
<?php
/**
* This file is part of danjones000/object-spy
*
* danjones000/object-spy is open source software: you can distribute
* it and/or modify it under the terms of the MIT License
* (the "License"). You may not use this file except in
* compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* @copyright Copyright (c) Dan Jones <danjones@goodevilgenius.org>
* @license https://opensource.org/licenses/MIT MIT License
*/
declare(strict_types=1);
namespace Danjones000\Spy;
use Closure;
use InvalidArgumentException;
use function class_exists;
use function get_class;
use function is_null;
use function is_object;
use function is_string;
/**
* Proxies all calls to another object/class, via Closures.
*
* This allows access to private/protected methods and properties without Reflection.
*/
class ObjectProxy
{
protected ?string $class;
protected ?object $object;
protected Closure $caller;
protected Closure $getter;
protected Closure $setter;
/**
* @param string|object $classOrObject
*
* @psalm-suppress RedundantConditionGivenDocblockType
*/
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]
);
$this->assertValid();
}
protected function assertValid(): void
{
if (is_null($this->class)) {
throw new InvalidArgumentException('Must specify a class, or object');
}
if (!class_exists($this->class)) {
throw new InvalidArgumentException('Must specify a valid class');
}
}
/**
* Get the closure used by $this->__call.
*
* @psalm-suppress PossiblyNullArgument
*/
protected function getCaller(): Closure
{
return $this->caller ??= (
$this->object ?
fn (string $method, array $args) => $this->$method(...$args) :
static fn (string $method, array $args) => static::$method(...$args)
)->bindTo($this->object, $this->class);
}
/**
* Get the closure used by $this->__get.
*
* @psalm-suppress PossiblyNullArgument
* @psalm-suppress UnusedClosureParam
*/
protected function getGetter(): Closure
{
return $this->getter ??= (
$this->object ?
fn (string $key) => $this->$key :
static fn (string $key) => static::$$key
)->bindTo($this->object, $this->class);
}
/**
* Get the closure used by $this->__set.
*
* @psalm-suppress PossiblyNullArgument
* @psalm-suppress MixedAssignment
*/
protected function getSetter(): Closure
{
return $this->setter ??= (
$this->object ?
fn (string $key, $value) => $this->$key = $value :
static fn (string $key, $value) => static::$$key = $value
)->bindTo($this->object, $this->class);
}
/**
* Run arbitry code on object, with access to private/protected props/methods.
*
* @param mixed[] $args
* @return mixed
* @psalm-suppress PossiblyNullArgument
* @psalm-suppress PossiblyInvalidFunctionCall
*/
public function call(callable $cb, array ...$args)
{
return (Closure::fromCallable($cb)->bindTo($this->object, $this->class))(...$args);
}
/**
* @param mixed[] $args
* @return mixed
*/
public function __call(string $method, array $args)
{
return ($this->getCaller())($method, $args);
}
/**
* @return mixed
*/
public function __get(string $key)
{
return ($this->getGetter())($key);
}
/**
* @param mixed $value
* @return mixed
*/
public function __set(string $key, $value)
{
return ($this->getSetter())($key, $value);
}
}