From d8006b5d67165b2e42e913900b30cb936d652689 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Tue, 12 Oct 2021 11:22:27 +0100 Subject: [PATCH] bindListCallback (#268) * build: upgrade to stable dom release * test: bind objects in arrays with bindList * wip: complex test for #261 * feature: bindListCallback allows callback to be called for each list item closes #261 * feature: expose and test callback functions closes #261 --- src/DocumentBinder.php | 18 ++ src/ListBinder.php | 24 ++- src/PlaceholderBinder.php | 5 +- test/phpunit/DocumentBinderTest.php | 160 ++++++++++++++++++ test/phpunit/ListBinderTest.php | 46 +++++ .../TestFactory/DocumentTestFactory.php | 47 +++++ 6 files changed, 293 insertions(+), 7 deletions(-) diff --git a/src/DocumentBinder.php b/src/DocumentBinder.php index c23e5df..d18c58c 100644 --- a/src/DocumentBinder.php +++ b/src/DocumentBinder.php @@ -107,6 +107,24 @@ public function bindList( return $this->listBinder->bindListData($listData, $context, $templateName); } + public function bindListCallback( + iterable $listData, + callable $callback, + ?Element $context = null, + ?string $templateName = null + ):int { + if(!$context) { + $context = $this->document; + } + + return $this->listBinder->bindListData( + $listData, + $context, + $templateName, + $callback + ); + } + private function bind( ?string $key, mixed $value, diff --git a/src/ListBinder.php b/src/ListBinder.php index 60f8440..c17b35c 100644 --- a/src/ListBinder.php +++ b/src/ListBinder.php @@ -20,7 +20,8 @@ public function __construct( public function bindListData( iterable $listData, Document|Element $context, - ?string $templateName = null + ?string $templateName = null, + ?callable $callback = null, ):int { if($context instanceof Document) { $context = $context->documentElement; @@ -36,7 +37,7 @@ public function bindListData( $templateName ); - $binder = new ElementBinder(); + $elementBinder = new ElementBinder(); $nestedCount = 0; $i = -1; foreach($listData as $listKey => $listItem) { @@ -45,7 +46,7 @@ public function bindListData( // If the $listItem's first value is iterable, then treat this as a nested list. if($this->isNested($listItem)) { - $binder->bind(null, $listKey, $t); + $elementBinder->bind(null, $listKey, $t); $nestedCount += $this->bindListData( $listItem, $t, @@ -64,11 +65,22 @@ public function bindListData( } if($this->isKVP($listItem)) { + if($callback) { + $listItem = call_user_func( + $callback, + $t, + $listItem, + $listKey, + ); + } + + $elementBinder->bind(null, $listKey, $t); + foreach($listItem as $key => $value) { - $binder->bind($key, $value, $t); + $elementBinder->bind($key, $value, $t); if($this->isNested($value)) { - $binder->bind(null, $key, $t); + $elementBinder->bind(null, $key, $t); $nestedCount += $this->bindListData( $value, $t, @@ -78,7 +90,7 @@ public function bindListData( } } else { - $binder->bind(null, $listItem, $t); + $elementBinder->bind(null, $listItem, $t); } } diff --git a/src/PlaceholderBinder.php b/src/PlaceholderBinder.php index 7a8e2e7..5aaf1e4 100644 --- a/src/PlaceholderBinder.php +++ b/src/PlaceholderBinder.php @@ -28,10 +28,12 @@ public function bind( ); foreach($xpathResult as $attributeOrText) { + /** @var Text|Attr $text */ $text = $attributeOrText; if($text instanceof Attr) { $text = $text->firstChild; } + /** @var Text $text */ $placeholder = $text->splitText( strpos($text->data, "{{") @@ -41,7 +43,8 @@ public function bind( ); $placeholderText = new PlaceholderText($placeholder); - if($key !== $placeholderText->getBindKey()) { + if((string)$key !== $placeholderText->getBindKey()) { + $text->parentNode->nodeValue = $text->wholeText; continue; } diff --git a/test/phpunit/DocumentBinderTest.php b/test/phpunit/DocumentBinderTest.php index 8c876d1..776ce74 100644 --- a/test/phpunit/DocumentBinderTest.php +++ b/test/phpunit/DocumentBinderTest.php @@ -1,6 +1,7 @@ bindValue(fn() => "test"); self::assertSame("test", $document->querySelector("output")->textContent); } + + public function testBindList_complexHTML():void { + $from = "Slitting Mill"; + $to = "Clipstone"; + + $routesData = [ + [ + "duration" => new DateInterval("PT3H58M"), + "method" => "Train", + "steps" => [ + "rtv471" => [ + "time" => "07:02", + "location" => "Rugeley Trent Valley", + ], + "ltv991" => [ + "time" => "07:49", + "location" => "Lichfield Trent Valley", + ], + "tem010" => [ + "time" => "08:03", + "location" => "Tamworth", + ], + "csy001" => [ + "time" => "09:03", + "location" => "Chesterfield", + ], + "ep090" => [ + "time" => "09:42", + "location" => "Eastwood Park", + ], + "mnn310" => [ + "time" => "10:25", + "location" => "Mansfield", + ], + "c0390" => [ + "time" => "11:00", + "location" => "Clipstone", + ] + ] + ], [ + "duration" => new DateInterval("PT4H11M"), + "method" => "Bus", + "steps" => [ + "stv472" => [ + "time" => "06:20", + "location" => "Rugeley Trent Valley", + ], + "ltv050" => [ + "time" => "07:40", + "location" => "Lichfield City Centre", + ], + "ltv921" => [ + "time" => "08:00", + "location" => "Mosley Street", + ], + "sd094" => [ + "time" => "08:18", + "location" => "Burton-on-Trent" + ], + "ng001" => [ + "time" => "09:06", + "location" => "Nottingham", + ], + "mnn310" => [ + "time" => "10:01", + "location" => "Mansfield", + ], + "c0353" => [ + "time" => "10:31", + "location" => "Greendale Crescent", + ] + ] + ] + ]; + + $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_TRANSPORT_ROUTES); + $sut = new DocumentBinder($document); + $sut->bindKeyValue("from", $from); + $sut->bindKeyValue("to", $to); + + $callback = function( + Element $templateElement, + array $kvp, + int|string $key, + ):array { + if($duration = $kvp["duration"]) { + /** @var DateInterval $duration */ + $kvp["duration"] = $duration->format("%H:%m"); + } + + return $kvp; + }; + + $sut->bindListCallback($routesData, $callback); + $routeLiList = $document->querySelectorAll("ul>li"); + self::assertCount(2, $routeLiList); + self::assertCount(count($routesData[0]["steps"]), $routeLiList[0]->querySelectorAll("ol>li")); + self::assertCount(count($routesData[1]["steps"]), $routeLiList[1]->querySelectorAll("ol>li")); + + foreach($routesData as $i => $route) { + self::assertEquals($route["method"], $routeLiList[$i]->querySelector("p")->textContent); + self::assertEquals($route["duration"]->format("%H:%m"), $routeLiList[$i]->querySelector("time")->textContent); + $stepLiList = $routeLiList[$i]->querySelectorAll("ol>li"); + $j = 0; + + foreach($route["steps"] as $id => $step) { + $stepLi = $stepLiList[$j]; + self::assertEquals($step["time"], $stepLi->querySelector("time")->textContent); + self::assertEquals($step["location"], $stepLi->querySelector("span")->textContent); + self::assertEquals("/route/step/$id", $stepLi->querySelector("a")->href); + $j++; + } + } + } + + public function testBindListData_callback():void { + $salesData = [ + [ + "name" => "Cactus", + "count" => 14, + "price" => 5.50, + "cost" => 3.55, + ], + [ + "name" => "Succulent", + "count" => 9, + "price" => 3.50, + "cost" => 2.10, + ] + ]; + $salesCallback = function(Element $template, array $listItem, string $key):array { + $totalPrice = $listItem["price"] * $listItem["count"]; + $totalCost = $listItem["cost"] * $listItem["count"]; + + $listItem["profit"] = round($totalPrice - $totalCost, 2); + return $listItem; + }; + + $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_SALES); + $sut = new DocumentBinder($document); + $sut->bindListCallback( + $salesData, + $salesCallback + ); + + $salesLiList = $document->querySelectorAll("ul>li"); + self::assertCount(count($salesData), $salesLiList); + foreach($salesData as $i => $sale) { + $li = $salesLiList[$i]; + $profitValue = round(($sale["count"] * $sale["price"]) - ($sale["count"] * $sale["cost"]), 2); + self::assertEquals($sale["name"], $li->querySelector(".name span")->textContent); + self::assertEquals($sale["count"], $li->querySelector(".count span")->textContent); + self::assertEquals($sale["price"], $li->querySelector(".price span")->textContent); + self::assertEquals($sale["cost"], $li->querySelector(".cost span")->textContent); + self::assertEquals($profitValue, $li->querySelector(".profit span")->textContent); + } + } } diff --git a/test/phpunit/ListBinderTest.php b/test/phpunit/ListBinderTest.php index 0e7d105..46c5f2b 100644 --- a/test/phpunit/ListBinderTest.php +++ b/test/phpunit/ListBinderTest.php @@ -4,6 +4,7 @@ use ArrayIterator; use DateInterval; use DateTime; +use Gt\Dom\Element; use Gt\Dom\HTMLElement\HTMLLiElement; use Gt\DomTemplate\Bind; use Gt\DomTemplate\ElementBinder; @@ -497,4 +498,49 @@ public function testBindListData_multipleTemplateSiblings():void { self::assertEquals($expected[$i], $li->querySelector("span")->textContent); } } + + public function testBindListData_callback():void { + $salesData = [ + [ + "name" => "Cactus", + "count" => 14, + "price" => 5.50, + "cost" => 3.55, + ], + [ + "name" => "Succulent", + "count" => 9, + "price" => 3.50, + "cost" => 2.10, + ] + ]; + $salesCallback = function(Element $template, array $listItem, string $key):array { + $totalPrice = $listItem["price"] * $listItem["count"]; + $totalCost = $listItem["cost"] * $listItem["count"]; + + $listItem["profit"] = round($totalPrice - $totalCost, 2); + return $listItem; + }; + + $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_SALES); + $templateCollection = new TemplateCollection($document); + $sut = new ListBinder($templateCollection); + $sut->bindListData( + $salesData, + $document, + callback: $salesCallback + ); + + $salesLiList = $document->querySelectorAll("ul>li"); + self::assertCount(count($salesData), $salesLiList); + foreach($salesData as $i => $sale) { + $li = $salesLiList[$i]; + $profitValue = round(($sale["count"] * $sale["price"]) - ($sale["count"] * $sale["cost"]), 2); + self::assertEquals($sale["name"], $li->querySelector(".name span")->textContent); + self::assertEquals($sale["count"], $li->querySelector(".count span")->textContent); + self::assertEquals($sale["price"], $li->querySelector(".price span")->textContent); + self::assertEquals($sale["cost"], $li->querySelector(".cost span")->textContent); + self::assertEquals($profitValue, $li->querySelector(".profit span")->textContent); + } + } } diff --git a/test/phpunit/TestFactory/DocumentTestFactory.php b/test/phpunit/TestFactory/DocumentTestFactory.php index 743d985..9ffe611 100644 --- a/test/phpunit/TestFactory/DocumentTestFactory.php +++ b/test/phpunit/TestFactory/DocumentTestFactory.php @@ -488,6 +488,53 @@ class DocumentTestFactory {

If there's matching modular content in the _compnent directory, the above element will be filled with its content.

HTML; + const HTML_TRANSPORT_ROUTES = << +

Transport Routes

+
+ + + +
+ + +HTML; + + const HTML_SALES = << +

Sales

+ +HTML; + + public static function createHTML(string $html = ""):HTMLDocument { return HTMLDocumentFactory::create($html);