Skip to content

Commit

Permalink
Implement BindGetter attribute (#266)
Browse files Browse the repository at this point in the history
* build: upgrade to stable dom release

* test: bind objects in arrays with bindList

* test: ensure second check uses cache

* feature: implement BindGetter Attribute
closes #258

* test: bind callable value
  • Loading branch information
g105b authored Oct 9, 2021
1 parent 6ccfb42 commit 895b87d
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 67 deletions.
4 changes: 4 additions & 0 deletions src/BindGetterMethodDoesNotStartWithGetException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
namespace Gt\DomTemplate;

class BindGetterMethodDoesNotStartWithGetException extends DomTemplateException {}
35 changes: 31 additions & 4 deletions src/BindableCache.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<?php
namespace Gt\DomTemplate;

use Gt\DomTemplate\Test\BindGetter;
use ReflectionAttribute;
use ReflectionMethod;
use ReflectionObject;
use ReflectionProperty;
use function PHPUnit\Framework\stringStartsWith;

class BindableCache {
/**
Expand All @@ -14,7 +16,7 @@ class BindableCache {
*/
private array $classAttributes;
/**
* @var array<string, null> A cache of class names that are known to
* @var array<string, bool> A cache of class names that are known to
* NOT be bindable (to avoid having to check with reflection each time).
*/
private array $nonBindableClasses;
Expand Down Expand Up @@ -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();
}
Expand All @@ -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;
}

Expand All @@ -71,18 +73,43 @@ 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);
}

return $kvp;
}

/** @return array<ReflectionAttribute> */
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];
}
}
63 changes: 0 additions & 63 deletions src/ElementBinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, string> */
private function getBindKeys(Element $element):array {
$bindKeyList = [];
foreach($element->attributes as $attributeValue) {
array_push($bindKeyList, $attributeValue);
}

return $bindKeyList;
}
}
9 changes: 9 additions & 0 deletions test/phpunit/BindGetter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
namespace Gt\DomTemplate\Test;

use Attribute;

#[Attribute]
class BindGetter{
public function __construct() {}
}
76 changes: 76 additions & 0 deletions test/phpunit/BindableCacheTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php
namespace Gt\DomTemplate\Test;

use Gt\DomTemplate\Bind;
use Gt\DomTemplate\BindableCache;
use Gt\DomTemplate\BindGetterMethodDoesNotStartWithGetException;
use PHPUnit\Framework\TestCase;
use stdClass;

class BindableCacheTest extends TestCase {
public function testIsBindable_nonBindableCached():void {
$obj = new StdClass();
$sut = new BindableCache();
self::assertFalse($sut->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));
}
}
7 changes: 7 additions & 0 deletions test/phpunit/DocumentBinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

0 comments on commit 895b87d

Please sign in to comment.