From 0e74e2bff797bdb35fbb8b00d0961f144ccf481b Mon Sep 17 00:00:00 2001 From: Coil Date: Thu, 30 Dec 2021 01:59:36 +0100 Subject: [PATCH] Allow decorating anonymous classes; Allow direct access to public variables --- composer.json | 2 +- src/DecoratorManager.php | 32 ++++++++++++++++++++--------- src/ProxyVariableAccessTrait.php | 21 +++++++++++++++++++ tests/ObjectDecoratorTest.php | 35 ++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 src/ProxyVariableAccessTrait.php diff --git a/composer.json b/composer.json index dd227fc..5357e22 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "c01l/phpdecorator", - "version": "1.1.0", + "version": "1.2.0", "description": "The phpdecorator library can be used to wrap functions of objects and classes with additional functionality. This is a feature that can be compared to Python decorators. ", "type": "library", "license": "MIT", diff --git a/src/DecoratorManager.php b/src/DecoratorManager.php index 587ecec..fa3edd1 100644 --- a/src/DecoratorManager.php +++ b/src/DecoratorManager.php @@ -47,8 +47,6 @@ public function decorate(object $real): object throw new DecoratorException("Cannot decorate final class: " . $real::class); } - $trait = "\\" . DecoratorHelperTrait::class; - $wrappers = []; $methods = $rc->getMethods(); $overwrite_methods = ""; @@ -64,7 +62,7 @@ public function decorate(object $real): object "' . $method->name . '" );'; } else { - $bodyFn = fn($method) => 'return parent::' . $method->name . '(...get_defined_vars());'; + $bodyFn = fn($method) => 'return $this->real->' . $method->name . '(...get_defined_vars());'; } $overwrite_methods .= $this->handleMethod( @@ -78,12 +76,20 @@ public function decorate(object $real): object return $real; } - $classBody = ' { use ' . $trait . '; public function __construct(private mixed $real) {} ' + + $trait = "\\" . DecoratorHelperTrait::class; + $trait2 = "\\" . ProxyVariableAccessTrait::class; + $classBody = ' { use ' . $trait . '; use ' . $trait2 . '; public function __construct(private mixed $real) {} ' . $overwrite_methods . '};'; $this->storeInCache($real::class, $classBody); - $classDef = 'return new class($real) extends \\' . $rc->getName() . $classBody; + $extends = ""; + if (!$rc->isAnonymous()) { + $extends = "extends \\{$rc->getName()} "; + } + + $classDef = 'return new class($real) ' . $extends . $classBody; $obj = eval($classDef); $obj->setWrappers($wrappers); return $obj; @@ -148,7 +154,12 @@ function (...$args) { return parent::' . $method->name . '(...$args);}, $classBody = '{ use ' . $trait . '; ' . $overwrite_methods . '};'; $this->storeInCache($className, $classBody); - $classDef = "return new class extends \\{$rc->getName()} $classBody"; + $extends = ""; + if (!$rc->isAnonymous()) { + $extends = "extends \\{$rc->getName()}"; + } + + $classDef = "return new class $extends $classBody"; $obj = eval($classDef); $obj->setWrappers($wrappers); return $obj; @@ -156,7 +167,7 @@ function (...$args) { return parent::' . $method->name . '(...$args);}, private function classToFilename(string $class): string { - return str_replace("\\", "_", $class); + return preg_replace("/[^[:alnum:]]/", "_", $class); } private function loadFromCache(string $class): string|false @@ -222,10 +233,11 @@ private function buildFunctionHead(ReflectionMethod $method): string { $modifiers = implode(" ", Reflection::getModifierNames($method->getModifiers())); $paramList = implode(", ", array_map([$this, "buildFunctionParam"], $method->getParameters())); - $type = $method->getReturnType()->getName(); + $type = $method->getReturnType()?->getName(); + $type = $type ? ": $type" : ""; $attrs = $this->buildAttributeList($method->getAttributes()); - return "$attrs $modifiers function " . $method->name . "($paramList): $type"; + return "$attrs $modifiers function " . $method->name . "($paramList) $type"; } private function buildFunctionParam(ReflectionParameter $parameter): string @@ -240,7 +252,7 @@ private function buildFunctionParam(ReflectionParameter $parameter): string } $attrs = $this->buildAttributeList($parameter->getAttributes()); - return "$attrs {$parameter->getType()->getName()} \${$parameter->getName()}$default"; + return "$attrs {$parameter->getType()?->getName()} \${$parameter->getName()}$default"; } private function buildAttributeList(array $attrs): string diff --git a/src/ProxyVariableAccessTrait.php b/src/ProxyVariableAccessTrait.php new file mode 100644 index 0000000..a9d08f4 --- /dev/null +++ b/src/ProxyVariableAccessTrait.php @@ -0,0 +1,21 @@ +real->$name = $value; + } + + public function __get(string $name) + { + return $this->real->$name; + } + + public function __isset(string $name): bool + { + return isset($this->real->$name); + } +} diff --git a/tests/ObjectDecoratorTest.php b/tests/ObjectDecoratorTest.php index 2bd2088..516f22b 100644 --- a/tests/ObjectDecoratorTest.php +++ b/tests/ObjectDecoratorTest.php @@ -6,6 +6,7 @@ use C01l\PhpDecorator\DecoratorManager; use C01l\PhpDecorator\Tests\TestClasses\FinalClass; use C01l\PhpDecorator\Tests\TestClasses\Logger; +use C01l\PhpDecorator\Tests\TestClasses\LoggingDecorator; use C01l\PhpDecorator\Tests\TestClasses\MultipleDecoratorClass; use C01l\PhpDecorator\Tests\TestClasses\SimpleContainer; use C01l\PhpDecorator\Tests\TestClasses\SimpleMethodsClass; @@ -78,4 +79,38 @@ public function testKeepsUnrelatedAttributes() $attrNames = array_map(fn($a) => $a->getName(), $attrs); $this->assertEquals([UnrelatedAttribute::class], $attrNames); } + + public function testDecorateAnonymousClass() + { + $obj = new class { + #[LoggingDecorator] + public function test($x) + { + return $x; + } + }; + $obj = $this->sut->decorate($obj); + $this->assertEquals(2, $obj->test(2)); + } + + public function testAccessingPublicVariables() + { + $obj = new class { + public int $i = 0; + + public function get(): int + { + return $this->i; + } + + #[LoggingDecorator] + public function test() + { + } + }; + $obj = $this->sut->decorate($obj); + $obj->i = 3; + $this->assertEquals(3, $obj->get()); + $this->assertEquals(3, $obj->i); + } }