diff --git a/.ac-php-conf.json b/.ac-php-conf.json
new file mode 100644
index 0000000..7edd623
--- /dev/null
+++ b/.ac-php-conf.json
@@ -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 -- "
+ ]
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 0511278..b27f740 100644
--- a/README.md
+++ b/README.md
@@ -22,12 +22,8 @@ TODO: Make sure the following URLs are correct and working for your project.
## About
-
-
+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
```
-
+## 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/)
diff --git a/composer.json b/composer.json
index c05eb21..363f0d2 100644
--- a/composer.json
+++ b/composer.json
@@ -37,7 +37,10 @@
"autoload": {
"psr-4": {
"Danjones000\\Spy\\": "src/"
- }
+ },
+ "files": [
+ "src/functions.php"
+ ]
},
"autoload-dev": {
"psr-4": {
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index f76cd66..10906e3 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -8,6 +8,6 @@
./src
./tests
-
+
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 7cad6b1..7061e15 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -4,3 +4,7 @@ parameters:
paths:
- ./src
- ./tests
+ ignoreErrors:
+ - '/Call to an undefined method/'
+ - '/Access to an undefined property/'
+ - '/Access to undefined constant/'
diff --git a/psalm.xml b/psalm.xml
index cba5289..471f8b4 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -14,4 +14,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ObjectProxy.php b/src/ObjectProxy.php
new file mode 100644
index 0000000..fcb64c8
--- /dev/null
+++ b/src/ObjectProxy.php
@@ -0,0 +1,157 @@
+
+ * @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);
+ }
+}
diff --git a/src/Example.php b/src/functions.php
similarity index 74%
rename from src/Example.php
rename to src/functions.php
index 89fa948..c1173ed 100644
--- a/src/Example.php
+++ b/src/functions.php
@@ -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);
}
diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php
deleted file mode 100644
index a572c15..0000000
--- a/tests/ExampleTest.php
+++ /dev/null
@@ -1,20 +0,0 @@
-mockery(Example::class);
- $example->shouldReceive('greet')->passthru();
-
- $this->assertSame('Hello, Friends!', $example->greet('Friends'));
- }
-}
diff --git a/tests/Fixtures/Example.php b/tests/Fixtures/Example.php
new file mode 100644
index 0000000..2aba996
--- /dev/null
+++ b/tests/Fixtures/Example.php
@@ -0,0 +1,41 @@
+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;
+ }
+}
diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php
new file mode 100644
index 0000000..95bc192
--- /dev/null
+++ b/tests/FunctionsTest.php
@@ -0,0 +1,18 @@
+assertInstanceOf(ObjectProxy::class, $spy);
+ }
+}
diff --git a/tests/ObjectProxyTest.php b/tests/ObjectProxyTest.php
new file mode 100644
index 0000000..938be7f
--- /dev/null
+++ b/tests/ObjectProxyTest.php
@@ -0,0 +1,127 @@
+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());
+ }
+}