✨ Implement ObjectProxy class
This commit is contained in:
parent
5e431a716a
commit
d857775289
10 changed files with 417 additions and 79 deletions
22
.ac-php-conf.json
Normal file
22
.ac-php-conf.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"use-cscope": null,
|
||||||
|
"tag-dir": null,
|
||||||
|
"filter": {
|
||||||
|
"php-file-ext-list": [
|
||||||
|
"php"
|
||||||
|
],
|
||||||
|
"php-path-list": [
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"ignore-ruleset": [
|
||||||
|
"# like .gitignore file ",
|
||||||
|
"/vendor/**/[tT]ests/**/*.php",
|
||||||
|
"/vendor/**/[Ee]xamples/**/*.php",
|
||||||
|
"/vendor/composer/*.php",
|
||||||
|
"/vendor/*.php",
|
||||||
|
"# not need php_codesniffer",
|
||||||
|
"/vendor/squizlabs/php_codesniffer/**/*.php",
|
||||||
|
"# -- end -- "
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
62
README.md
62
README.md
|
|
@ -22,12 +22,8 @@ TODO: Make sure the following URLs are correct and working for your project.
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
<!--
|
A debugging/testing tool which allows access to an object's or class's private
|
||||||
TODO: Use this space to provide more details about your package. Try to be
|
and protected properties and methods without the use of Reflection.
|
||||||
concise. This is the introduction to your package. Let others know what
|
|
||||||
your package does and how it can help them build applications.
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
This project adheres to a [code of conduct](CODE_OF_CONDUCT.md).
|
This project adheres to a [code of conduct](CODE_OF_CONDUCT.md).
|
||||||
By participating in this project and its community, you are expected to
|
By participating in this project and its community, you are expected to
|
||||||
|
|
@ -42,33 +38,57 @@ Install this package as a dependency using [Composer](https://getcomposer.org).
|
||||||
composer require danjones000/object-spy
|
composer require danjones000/object-spy
|
||||||
```
|
```
|
||||||
|
|
||||||
<!--
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Provide a brief description or short example of how to use this library.
|
|
||||||
If you need to provide more detailed examples, use the `docs/` directory
|
|
||||||
and provide a link here to the documentation.
|
|
||||||
|
|
||||||
``` php
|
``` php
|
||||||
use Danjones000\Spy\Example;
|
use Danjones000\Spy\ObjectProxy;
|
||||||
|
|
||||||
$example = new Example();
|
$object = new Object(); // Can be any object.
|
||||||
echo $example->greet('fellow human');
|
|
||||||
|
$spy = new ObjectProxy($object);
|
||||||
|
|
||||||
|
// Access private property
|
||||||
|
echo $spy->privateProperty;
|
||||||
|
|
||||||
|
// Set private property
|
||||||
|
$spy->privateProperty = 42;
|
||||||
|
|
||||||
|
// Call private method
|
||||||
|
$spy->privateMethod();
|
||||||
|
|
||||||
|
// Call private method with parameters
|
||||||
|
$spy->protectedMethod(42, false);
|
||||||
|
|
||||||
|
// Run arbitry code on object, allows passing in parameters
|
||||||
|
// All arguments after the closure are passed as arguments to the closure itself
|
||||||
|
$spy->call(fn ($abc) => $this->someProp = $this->someMethod($abc) * static::MULTIPLIER, 500);
|
||||||
|
|
||||||
|
// PHP allows static methods to be called non-statically, so, this works as well
|
||||||
|
$spy->staticPrivateMethod();
|
||||||
|
|
||||||
|
// Can also access static properties/methods without an object instance
|
||||||
|
$staticSpy = new ObjectProxy(Object::class);
|
||||||
|
|
||||||
|
// If there is an Object::$privateStaticProperty
|
||||||
|
echo $staticSpy->privateStaticProperty;
|
||||||
|
$staticSpy->privateStaticProperty = 42;
|
||||||
|
|
||||||
|
$staticSpy->privateStaticMethod();
|
||||||
|
|
||||||
|
// No direct access to contstants, but we can do this
|
||||||
|
$staticSpy->call(fn () => static::PRIVATE_CONSTANT); // self also works, of course
|
||||||
```
|
```
|
||||||
-->
|
|
||||||
|
|
||||||
|
## Implementation notes
|
||||||
|
|
||||||
|
This class does not use reflections in its operation. Instead, if uses closures
|
||||||
|
that are bound to the instance or class.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome! To contribute, please familiarize yourself with
|
Contributions are welcome! To contribute, please familiarize yourself with
|
||||||
[CONTRIBUTING.md](CONTRIBUTING.md).
|
[CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Copyright and License
|
## Copyright and License
|
||||||
|
|
||||||
The danjones000/object-spy library is copyright © [Dan Jones](https://danielrayjones.com/)
|
The danjones000/object-spy library is copyright © [Dan Jones](https://danielrayjones.com/)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@
|
||||||
<file>./src</file>
|
<file>./src</file>
|
||||||
<file>./tests</file>
|
<file>./tests</file>
|
||||||
|
|
||||||
<rule ref="Ramsey"/>
|
<rule ref="PSR12"/>
|
||||||
|
|
||||||
</ruleset>
|
</ruleset>
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,7 @@ parameters:
|
||||||
paths:
|
paths:
|
||||||
- ./src
|
- ./src
|
||||||
- ./tests
|
- ./tests
|
||||||
|
ignoreErrors:
|
||||||
|
- '/Call to an undefined method/'
|
||||||
|
- '/Access to an undefined property/'
|
||||||
|
- '/Access to undefined constant/'
|
||||||
|
|
|
||||||
22
psalm.xml
22
psalm.xml
|
|
@ -14,4 +14,26 @@
|
||||||
</ignoreFiles>
|
</ignoreFiles>
|
||||||
</projectFiles>
|
</projectFiles>
|
||||||
|
|
||||||
|
<issueHandlers>
|
||||||
|
<PropertyNotSetInConstructor>
|
||||||
|
<errorLevel type="suppress">
|
||||||
|
<file name="src/ObjectProxy.php" />
|
||||||
|
</errorLevel>
|
||||||
|
</PropertyNotSetInConstructor>
|
||||||
|
<RedundantPropertyInitializationCheck>
|
||||||
|
<errorLevel type="suppress">
|
||||||
|
<file name="src/ObjectProxy.php" />
|
||||||
|
</errorLevel>
|
||||||
|
</RedundantPropertyInitializationCheck>
|
||||||
|
<MissingClosureReturnType>
|
||||||
|
<errorLevel type="suppress">
|
||||||
|
<file name="src/ObjectProxy.php" />
|
||||||
|
</errorLevel>
|
||||||
|
</MissingClosureReturnType>
|
||||||
|
<MissingClosureParamType>
|
||||||
|
<errorLevel type="suppress">
|
||||||
|
<file name="src/ObjectProxy.php" />
|
||||||
|
</errorLevel>
|
||||||
|
</MissingClosureParamType>
|
||||||
|
</issueHandlers>
|
||||||
</psalm>
|
</psalm>
|
||||||
|
|
|
||||||
|
|
@ -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
157
src/ObjectProxy.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Danjones000\Test\Spy;
|
|
||||||
|
|
||||||
use Danjones000\Spy\Example;
|
|
||||||
use Mockery\MockInterface;
|
|
||||||
|
|
||||||
class ExampleTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testGreet(): void
|
|
||||||
{
|
|
||||||
/** @var Example & MockInterface $example */
|
|
||||||
$example = $this->mockery(Example::class);
|
|
||||||
$example->shouldReceive('greet')->passthru();
|
|
||||||
|
|
||||||
$this->assertSame('Hello, Friends!', $example->greet('Friends'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
41
tests/Fixtures/Example.php
Normal file
41
tests/Fixtures/Example.php
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Danjones000\Test\Spy\Fixtures;
|
||||||
|
|
||||||
|
class Example
|
||||||
|
{
|
||||||
|
protected const PROT_CONST = 2;
|
||||||
|
private const PRIV_CONST = 3;
|
||||||
|
protected static int $protStatic = 4;
|
||||||
|
private static int $privStatic = 5;
|
||||||
|
protected int $prot = 6;
|
||||||
|
private int $priv = 7;
|
||||||
|
|
||||||
|
public static function reset(): void
|
||||||
|
{
|
||||||
|
static::$protStatic = 4;
|
||||||
|
static::$privStatic = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function protMethod(): int
|
||||||
|
{
|
||||||
|
return $this->prot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function privMethod(int $priv): void
|
||||||
|
{
|
||||||
|
$this->priv = $priv;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function protStatMethod(int $protStatic): void
|
||||||
|
{
|
||||||
|
static::$protStatic = $protStatic;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function privStatMethod(): int
|
||||||
|
{
|
||||||
|
return static::$privStatic;
|
||||||
|
}
|
||||||
|
}
|
||||||
129
tests/ObjectProxyTest.php
Normal file
129
tests/ObjectProxyTest.php
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Danjones000\Test\Spy;
|
||||||
|
|
||||||
|
use Danjones000\Spy\ObjectProxy;
|
||||||
|
use Mockery;
|
||||||
|
use Mockery\MockInterface;
|
||||||
|
|
||||||
|
class ObjectProxyTest extends TestCase
|
||||||
|
{
|
||||||
|
protected Fixtures\Example $example;
|
||||||
|
protected ObjectProxy $spy;
|
||||||
|
protected ObjectProxy $classSpy;
|
||||||
|
protected \ReflectionObject $reflection;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Fixtures\Example::reset();
|
||||||
|
$this->example = new Fixtures\Example();
|
||||||
|
$this->spy = new ObjectProxy($this->example);
|
||||||
|
$this->classSpy = new ObjectProxy(Fixtures\Example::class);
|
||||||
|
$this->reflection = new \ReflectionObject($this->example);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCallingProtectedMethod(): void
|
||||||
|
{
|
||||||
|
$ret = $this->spy->protMethod();
|
||||||
|
$this->assertEquals(6, $ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCallPrivateMethod(): void
|
||||||
|
{
|
||||||
|
$this->spy->privMethod(42);
|
||||||
|
$prop = $this->reflection->getProperty('priv');
|
||||||
|
$prop->setAccessible(true);
|
||||||
|
|
||||||
|
$this->assertEquals(42, $prop->getValue($this->example));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCallProtectedStaticMethod(): void
|
||||||
|
{
|
||||||
|
$this->classSpy->protStatMethod(42);
|
||||||
|
$prop = $this->reflection->getProperty('protStatic');
|
||||||
|
$prop->setAccessible(true);
|
||||||
|
|
||||||
|
$this->assertEquals(42, $prop->getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCallPrivateStaticMethod(): void
|
||||||
|
{
|
||||||
|
$ret = $this->classSpy->privStatMethod();
|
||||||
|
$this->assertEquals(5, $ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAccessPrivateProperty(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(7, $this->spy->priv);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAccessProtectedProperty(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(6, $this->spy->prot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAccessPrivateStaticProperty(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(5, $this->classSpy->privStatic);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAccessProtectedStaticProperty(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(4, $this->classSpy->protStatic);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAccessPrivateConstantThroughCall(): void
|
||||||
|
{
|
||||||
|
$ret = $this->classSpy->call(fn () => static::PRIV_CONST);
|
||||||
|
$this->assertEquals(3, $ret);
|
||||||
|
$ret = $this->classSpy->call(fn () => self::PRIV_CONST);
|
||||||
|
$this->assertEquals(3, $ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAccessProtectedConstantThroughCall(): void
|
||||||
|
{
|
||||||
|
$ret = $this->classSpy->call(fn () => static::PROT_CONST);
|
||||||
|
$this->assertEquals(2, $ret);
|
||||||
|
$ret = $this->classSpy->call(fn () => self::PROT_CONST);
|
||||||
|
$this->assertEquals(2, $ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModifyPrivateProperty(): void
|
||||||
|
{
|
||||||
|
$this->spy->priv = 42;
|
||||||
|
$prop = $this->reflection->getProperty('priv');
|
||||||
|
$prop->setAccessible(true);
|
||||||
|
|
||||||
|
$this->assertEquals(42, $prop->getValue($this->example));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModifyProtectedProperty(): void
|
||||||
|
{
|
||||||
|
$this->spy->prot = 42;
|
||||||
|
$prop = $this->reflection->getProperty('prot');
|
||||||
|
$prop->setAccessible(true);
|
||||||
|
|
||||||
|
$this->assertEquals(42, $prop->getValue($this->example));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModifyPrivateStaticProperty(): void
|
||||||
|
{
|
||||||
|
$this->classSpy->privStatic = 42;
|
||||||
|
$prop = $this->reflection->getProperty('privStatic');
|
||||||
|
$prop->setAccessible(true);
|
||||||
|
|
||||||
|
$this->assertEquals(42, $prop->getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModifyProtectedStaticProperty(): void
|
||||||
|
{
|
||||||
|
$this->classSpy->protStatic = 42;
|
||||||
|
$prop = $this->reflection->getProperty('protStatic');
|
||||||
|
$prop->setAccessible(true);
|
||||||
|
|
||||||
|
$this->assertEquals(42, $prop->getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue