🔖 Merge branch 'release/0.1.0' into stable
This commit is contained in:
commit
5293c88f80
12 changed files with 442 additions and 52 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
|
||||
|
||||
<!--
|
||||
TODO: Use this space to provide more details about your package. Try to be
|
||||
concise. This is the introduction to your package. Let others know what
|
||||
your package does and how it can help them build applications.
|
||||
-->
|
||||
|
||||
A debugging/testing tool which allows access to an object's or class's private
|
||||
and protected properties and methods without the use of Reflection.
|
||||
|
||||
This project adheres to a [code of conduct](CODE_OF_CONDUCT.md).
|
||||
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
|
||||
```
|
||||
|
||||
<!--
|
||||
## 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
|
||||
use Danjones000\Spy\Example;
|
||||
use Danjones000\Spy\ObjectProxy;
|
||||
|
||||
$example = new Example();
|
||||
echo $example->greet('fellow human');
|
||||
$object = new Object(); // Can be any object.
|
||||
|
||||
$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
|
||||
|
||||
Contributions are welcome! To contribute, please familiarize yourself with
|
||||
[CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Copyright and License
|
||||
|
||||
The danjones000/object-spy library is copyright © [Dan Jones](https://danielrayjones.com/)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,10 @@
|
|||
"autoload": {
|
||||
"psr-4": {
|
||||
"Danjones000\\Spy\\": "src/"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@
|
|||
<file>./src</file>
|
||||
<file>./tests</file>
|
||||
|
||||
<rule ref="Ramsey"/>
|
||||
<rule ref="PSR12"/>
|
||||
|
||||
</ruleset>
|
||||
|
|
|
|||
|
|
@ -4,3 +4,7 @@ parameters:
|
|||
paths:
|
||||
- ./src
|
||||
- ./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>
|
||||
</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>
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,15 +23,11 @@ declare(strict_types=1);
|
|||
namespace Danjones000\Spy;
|
||||
|
||||
/**
|
||||
* An example class to act as a starting point for developing your library
|
||||
* @param string|object $classOrObject
|
||||
*
|
||||
* @psalm-suppress RedundantConditionGivenDocblockType
|
||||
*/
|
||||
class Example
|
||||
function spy($classOrObject): ObjectProxy
|
||||
{
|
||||
/**
|
||||
* Returns a greeting statement using the provided name
|
||||
*/
|
||||
public function greet(string $name = 'World'): string
|
||||
{
|
||||
return "Hello, {$name}!";
|
||||
}
|
||||
return new ObjectProxy($classOrObject);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
18
tests/FunctionsTest.php
Normal file
18
tests/FunctionsTest.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Danjones000\Test\Spy;
|
||||
|
||||
use Danjones000\Spy\ObjectProxy;
|
||||
|
||||
use function Danjones000\Spy\spy;
|
||||
|
||||
class FunctionsTest extends TestCase
|
||||
{
|
||||
public function testSpy(): void
|
||||
{
|
||||
$spy = spy(Fixtures\Example::class);
|
||||
$this->assertInstanceOf(ObjectProxy::class, $spy);
|
||||
}
|
||||
}
|
||||
127
tests/ObjectProxyTest.php
Normal file
127
tests/ObjectProxyTest.php
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Danjones000\Test\Spy;
|
||||
|
||||
use Danjones000\Spy\ObjectProxy;
|
||||
|
||||
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