diff --git a/src/BindGetterMethodDoesNotStartWithGetException.php b/src/BindGetterMethodDoesNotStartWithGetException.php new file mode 100644 index 0000000..56c8fb2 --- /dev/null +++ b/src/BindGetterMethodDoesNotStartWithGetException.php @@ -0,0 +1,4 @@ + A cache of class names that are known to + * @var array A cache of class names that are known to * NOT be bindable (to avoid having to check with reflection each time). */ private array $nonBindableClasses; @@ -42,7 +44,7 @@ public function isBindable(object $object):bool { $methodName = $refMethod->getName(); foreach($refAttributes as $refAttr) { - $bindKey = $refAttr->getArguments()[0]; + $bindKey = $this->getBindKey($refAttr, $refMethod); $attributeCache[$bindKey] = fn(object $object) => $object->$methodName(); } @@ -52,14 +54,14 @@ public function isBindable(object $object):bool { $propName = $refProp->getName(); foreach($refAttributes as $refAttr) { - $bindKey = $refAttr->getArguments()[0]; + $bindKey = $this->getBindKey($refAttr); $attributeCache[$bindKey] = fn(object $object) => $object->$propName; } } if(empty($attributeCache)) { - $this->nonBindableClasses[$object::class] = null; + $this->nonBindableClasses[$object::class] = true; return false; } @@ -71,6 +73,10 @@ public function isBindable(object $object):bool { public function convertToKvp(object $object):array { $kvp = []; + if(!$this->isBindable($object)) { + return []; + } + foreach($this->classAttributes[$object::class] as $key => $closure) { $kvp[$key] = $closure($object); } @@ -78,11 +84,32 @@ public function convertToKvp(object $object):array { return $kvp; } + /** @return array */ private function getBindAttributes(ReflectionMethod|ReflectionProperty $ref):array { return array_filter( $ref->getAttributes(), fn(ReflectionAttribute $refAttr) => $refAttr->getName() === Bind::class + || $refAttr->getName() === BindGetter::class ); } + + private function getBindKey( + ReflectionAttribute $refAttr, + ?ReflectionMethod $refMethod = null, + ):string { + if($refAttr->getName() === BindGetter::class && $refMethod) { + $methodName = $refMethod->getName(); + if(!str_starts_with($methodName, "get")) { + throw new BindGetterMethodDoesNotStartWithGetException( + "Method $methodName has the BindGetter Attribute, but its name doesn't start with \"get\". For help, see https://www.php.gt/domtemplate/bindgetter" + ); + } + return lcfirst( + substr($methodName, 3) + ); + } + + return $refAttr->getArguments()[0]; + } } diff --git a/src/ElementBinder.php b/src/ElementBinder.php index e432aee..4dfa08f 100644 --- a/src/ElementBinder.php +++ b/src/ElementBinder.php @@ -42,67 +42,4 @@ public function bind( $this->placeholderBinder->bind($key, $value, $context); } - - /** - * A "bindable" object is any object with the Gt\DomTemplate\Bind - * Attribute applied to any of its public properties or methods. - * The Attribute's first parameter is required, which sets the property - * or method's bind key. For example, a method called "getTotalMessages" - * could be marked with the #[Bind("message-count")] Attribute, so the - * method will be called whenever the "message-count" bind key is used - * in the document. - */ - public function bindMethodPropertyAttributes( - object $objectWithAttributes, - Element $context - ):void { - $bindKeyList = []; - foreach($this->htmlAttributeCollection->find($context) as $bindElement) { - /** @var Element $bindElement */ - array_push($bindKeyList, ...$this->getBindKeys($bindElement)); - } - - $refClass = new ReflectionClass($objectWithAttributes); - foreach($refClass->getMethods(ReflectionMethod::IS_PUBLIC) as $refMethod) { - foreach($refMethod->getAttributes(Bind::class) as $refAttribute) { - $args = $refAttribute->getArguments(); - $bindKey = $args[0]; - if(!in_array($bindKey, $bindKeyList)) { - continue; - } - - $this->bind( - $bindKey, - call_user_func([$objectWithAttributes, $refMethod->getName()]), - $context - ); - } - } - - foreach($refClass->getProperties(ReflectionProperty::IS_PUBLIC) as $refProperty) { - foreach($refProperty->getAttributes(Bind::class) as $refAttribute) { - $args = $refAttribute->getArguments(); - $bindKey = $args[0]; - if(!in_array($bindKey, $bindKeyList)) { - continue; - } - - $this->bind( - $bindKey, - $objectWithAttributes->{$refProperty->getName()}, - $context - ); - } - } - } - - /** @return array */ - private function getBindKeys(Element $element):array { - $bindKeyList = []; - foreach($element->attributes as $attributeValue) { - array_push($bindKeyList, $attributeValue); - } - - return $bindKeyList; - } } diff --git a/test/phpunit/BindGetter.php b/test/phpunit/BindGetter.php new file mode 100644 index 0000000..f3948a4 --- /dev/null +++ b/test/phpunit/BindGetter.php @@ -0,0 +1,9 @@ +isBindable($obj)); + self::assertFalse($sut->isBindable($obj)); + } + + public function testIsBindable_bindableCached():void { + $obj1 = new class extends StdClass { + #[Bind("name")] + public function getName():string { + return "Test 1"; + } + }; + + $obj2 = new class extends StdClass { + #[Bind("name")] + public function getName():string { + return "Test 2"; + } + }; + + $sut = new BindableCache(); + self::assertTrue($sut->isBindable($obj1)); + self::assertTrue($sut->isBindable($obj1)); + self::assertTrue($sut->isBindable($obj2)); + } + + public function testConvertToKvp_getter():void { + $obj = new class { + #[BindGetter] + public function getName():string { + return "Test Name"; + } + }; + + $sut = new BindableCache(); + $kvp = $sut->convertToKvp($obj); + self::assertEquals("Test Name", $kvp["name"]); + } + + public function testConvertToKvp_getterDoesNotStartWithGet():void { + $obj = new class { + #[BindGetter] + public function retrieveName():string { + return "Test Name"; + } + }; + + $sut = new BindableCache(); + self::expectException(BindGetterMethodDoesNotStartWithGetException::class); + self::expectExceptionMessage("Method retrieveName has the BindGetter Attribute, but its name doesn't start with \"get\"."); + $sut->convertToKvp($obj); + } + + public function testConvertToKvp_notBindable():void { + $obj = new class { + public function getName():string { + return "Test"; + } + }; + + $sut = new BindableCache(); + self::assertSame([], $sut->convertToKvp($obj)); + } +} diff --git a/test/phpunit/DocumentBinderTest.php b/test/phpunit/DocumentBinderTest.php index bdea1a1..6520369 100644 --- a/test/phpunit/DocumentBinderTest.php +++ b/test/phpunit/DocumentBinderTest.php @@ -531,4 +531,11 @@ public function asArray():array { self::assertEquals("firstUser", $document->querySelector("li#user-123 h2 span")->textContent); self::assertEquals("secondUser", $document->querySelector("li#user-456 h2 span")->textContent); } + + public function testBindValue_callable():void { + $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_SINGLE_ELEMENT); + $sut = new DocumentBinder($document); + $sut->bindValue(fn() => "test"); + self::assertSame("test", $document->querySelector("output")->textContent); + } }