From 6b06a89a2bccf61a4296c7c648631d52785a855a Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 15 Nov 2021 17:45:50 +0000 Subject: [PATCH] Binding null values (#298) * fix: binding null value does nothing on Element and Placeholder binds * test: null binds by key * test: null binds by data * test: null binds by table * test: null binds by list * stan: fix type annotations * build: update dependencies --- composer.json | 4 +- composer.lock | 56 ++++++------- src/DocumentBinder.php | 3 +- src/ElementBinder.php | 2 +- src/HTMLAttributeBinder.php | 4 + src/ListBinder.php | 4 + src/PlaceholderBinder.php | 6 +- src/TableBinder.php | 5 +- src/TemplateCollection.php | 2 +- test/phpunit/DocumentBinderTest.php | 125 ++++++++++++++++++++++++++++ 10 files changed, 174 insertions(+), 37 deletions(-) diff --git a/composer.json b/composer.json index f57dac4..e93db92 100644 --- a/composer.json +++ b/composer.json @@ -9,8 +9,8 @@ "phpgt/dom": "^3.0.0" }, "require-dev": { - "phpunit/phpunit": "9.*", - "phpstan/phpstan": "1.*" + "phpunit/phpunit": "~9.5", + "phpstan/phpstan": "~1.1" }, "autoload": { diff --git a/composer.lock b/composer.lock index 0982b90..2e92a3b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,24 +4,24 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3451a60663657506032188d0f40aa82e", + "content-hash": "f0aeb116e1a0dc9759b1fc9bd107edcf", "packages": [ { "name": "phpgt/cssxpath", - "version": "v1.1.3", + "version": "v1.1.4", "source": { "type": "git", "url": "https://github.com/PhpGt/CssXPath.git", - "reference": "64813864ab00e52cbde2cd517d6c6235b7f9ac69" + "reference": "7f073ba346c49a339a7b2cda9ccfdb1994c5d271" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/CssXPath/zipball/64813864ab00e52cbde2cd517d6c6235b7f9ac69", - "reference": "64813864ab00e52cbde2cd517d6c6235b7f9ac69", + "url": "https://api.github.com/repos/PhpGt/CssXPath/zipball/7f073ba346c49a339a7b2cda9ccfdb1994c5d271", + "reference": "7f073ba346c49a339a7b2cda9ccfdb1994c5d271", "shasum": "" }, "require": { - "php": ">=7.4" + "php": ">=7.3" }, "require-dev": { "ext-dom": "*", @@ -50,15 +50,15 @@ "description": "Convert CSS selectors to XPath queries.", "support": { "issues": "https://github.com/PhpGt/CssXPath/issues", - "source": "https://github.com/PhpGt/CssXPath/tree/v1.1.3" + "source": "https://github.com/PhpGt/CssXPath/tree/v1.1.4" }, "funding": [ { - "url": "https://github.com/phpgt", + "url": "https://github.com/sponsors/PhpGt", "type": "github" } ], - "time": "2021-01-28T13:27:01+00:00" + "time": "2021-11-13T15:40:44+00:00" }, { "name": "phpgt/dom", @@ -389,16 +389,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.13.0", + "version": "v4.13.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "50953a2691a922aa1769461637869a0a2faa3f53" + "reference": "63a79e8daa781cac14e5195e63ed8ae231dd10fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/50953a2691a922aa1769461637869a0a2faa3f53", - "reference": "50953a2691a922aa1769461637869a0a2faa3f53", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/63a79e8daa781cac14e5195e63ed8ae231dd10fd", + "reference": "63a79e8daa781cac14e5195e63ed8ae231dd10fd", "shasum": "" }, "require": { @@ -439,9 +439,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.1" }, - "time": "2021-09-20T12:20:58+00:00" + "time": "2021-11-03T20:52:16+00:00" }, { "name": "phar-io/manifest", @@ -783,16 +783,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.0.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "0eb6ecdfbcebf2207668087dfb2e215581a75023" + "reference": "bcea0ae85868a89d5789c75f012c93129f842934" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0eb6ecdfbcebf2207668087dfb2e215581a75023", - "reference": "0eb6ecdfbcebf2207668087dfb2e215581a75023", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/bcea0ae85868a89d5789c75f012c93129f842934", + "reference": "bcea0ae85868a89d5789c75f012c93129f842934", "shasum": "" }, "require": { @@ -823,7 +823,7 @@ "description": "PHPStan - PHP Static Analysis Tool", "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.0.1" + "source": "https://github.com/phpstan/phpstan/tree/1.1.2" }, "funding": [ { @@ -843,7 +843,7 @@ "type": "tidelift" } ], - "time": "2021-11-02T10:25:31+00:00" + "time": "2021-11-09T12:41:09+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1695,16 +1695,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", "shasum": "" }, "require": { @@ -1753,14 +1753,14 @@ } ], "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" }, "funding": [ { @@ -1768,7 +1768,7 @@ "type": "github" } ], - "time": "2020-09-28T05:24:23+00:00" + "time": "2021-11-11T14:18:36+00:00" }, { "name": "sebastian/global-state", diff --git a/src/DocumentBinder.php b/src/DocumentBinder.php index 01dfff6..6ee2c2a 100644 --- a/src/DocumentBinder.php +++ b/src/DocumentBinder.php @@ -4,6 +4,7 @@ use Gt\Dom\Attr; use Gt\Dom\Document; use Gt\Dom\Element; +use Gt\Dom\XPathResult; class DocumentBinder { private ElementBinder $elementBinder; @@ -129,11 +130,11 @@ public function bindListCallback( } public function cleanDatasets():void { + /** @var XPathResult $xpathResult */ $xpathResult = $this->document->evaluate( "//*/@*[starts-with(name(), 'data-bind')] | //*/@*[starts-with(name(), 'data-template')]" ); foreach($xpathResult as $item) { - /** @var Attr $item */ $item->ownerElement->removeAttribute($item->name); } } diff --git a/src/ElementBinder.php b/src/ElementBinder.php index 4dfa08f..c44f713 100644 --- a/src/ElementBinder.php +++ b/src/ElementBinder.php @@ -34,8 +34,8 @@ public function bind( mixed $value, Element $context ):void { + /** @var Element $element */ foreach($this->htmlAttributeCollection->find($context) as $element) { - /** @var Element $element */ $this->htmlAttributeBinder->expandAttributes($element); $this->htmlAttributeBinder->bind($key, $value, $element); } diff --git a/src/HTMLAttributeBinder.php b/src/HTMLAttributeBinder.php index 9de4866..fa4f0b7 100644 --- a/src/HTMLAttributeBinder.php +++ b/src/HTMLAttributeBinder.php @@ -14,6 +14,10 @@ public function bind( mixed $value, Document|Element $element ):void { + if(is_null($value)) { + return; + } + if($element instanceof Document) { $element = $element->documentElement; } diff --git a/src/ListBinder.php b/src/ListBinder.php index 606889f..dd7b0b9 100644 --- a/src/ListBinder.php +++ b/src/ListBinder.php @@ -73,6 +73,10 @@ public function bindListData( ); } + if(is_null($listItem)) { + continue; + } + if($this->isKVP($listItem)) { $elementBinder->bind(null, $listKey, $t); diff --git a/src/PlaceholderBinder.php b/src/PlaceholderBinder.php index 5aaf1e4..1c5a2b5 100644 --- a/src/PlaceholderBinder.php +++ b/src/PlaceholderBinder.php @@ -12,6 +12,10 @@ public function bind( mixed $value, Node|Document $context ):void { + if(is_null($value)) { + return; + } + if($context instanceof Document) { $context = $context->documentElement; } @@ -31,10 +35,10 @@ public function bind( /** @var Text|Attr $text */ $text = $attributeOrText; if($text instanceof Attr) { + /** @var Text $text */ $text = $text->firstChild; } - /** @var Text $text */ $placeholder = $text->splitText( strpos($text->data, "{{") ); diff --git a/src/TableBinder.php b/src/TableBinder.php index a0a59b2..eefd92b 100644 --- a/src/TableBinder.php +++ b/src/TableBinder.php @@ -37,9 +37,8 @@ public function bindTableData( } $headerRow = array_shift($tableData); + /** @var HTMLTableElement $table */ foreach($tableArray as $table) { - /** @var HTMLTableElement $table */ - $allowedHeaders = $headerRow; $tHead = $table->tHead; @@ -95,7 +94,7 @@ public function bindTableData( } $cellElement = $tr->ownerDocument->createElement($cellTypeToCreate); - $cellElement->textContent = $columnValue; + $cellElement->textContent = $columnValue ?? ""; $tr->appendChild($cellElement); } } diff --git a/src/TemplateCollection.php b/src/TemplateCollection.php index 50e9bec..d6974bf 100644 --- a/src/TemplateCollection.php +++ b/src/TemplateCollection.php @@ -35,8 +35,8 @@ public function get( private function extractTemplates(Document $document):void { $dataTemplateArray = []; + /** @var Element $element */ foreach($document->querySelectorAll("[data-template]") as $element) { - /** @var Element $element */ $nodePath = (string)(new NodePathCalculator($element)); $templateElement = new TemplateElement($element); $key = $templateElement->getTemplateName() ?? $nodePath; diff --git a/test/phpunit/DocumentBinderTest.php b/test/phpunit/DocumentBinderTest.php index 4c433ab..32d3c84 100644 --- a/test/phpunit/DocumentBinderTest.php +++ b/test/phpunit/DocumentBinderTest.php @@ -2,6 +2,7 @@ namespace Gt\DomTemplate\Test; use DateInterval; +use Exception; use Gt\Dom\Element; use Gt\Dom\HTMLCollection; use Gt\Dom\HTMLElement\HTMLButtonElement; @@ -104,6 +105,19 @@ public function testBindValue_synonymousProperties():void { self::assertSame("updated bold", $document->getElementById("o9")->innerHTML); } + public function testBindValue_null():void { + $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_SINGLE_ELEMENT); + $sut = new DocumentBinder($document); + + $exception = null; + try { + $sut->bindValue(null); + } + catch(Exception $exception) {} + + self::assertNull($exception); + } + public function testBindKeyValue_noMatches():void { $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_SINGLE_ELEMENT); $sut = new DocumentBinder($document); @@ -127,6 +141,21 @@ public function testBindKeyValue():void { self::assertSame("This should bind", $document->querySelector("#container3 p span")->textContent); } + public function testBindKeyValue_null():void { + $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_MULTIPLE_NESTED_ELEMENTS); + $sut = new DocumentBinder($document); + + $exception = null; + try { + $sut->bindKeyValue("title", null); + } + catch(Exception $exception) {} + + self::assertNull($exception); + self::assertSame("Default title", $document->querySelector("#container3 h1")->textContent); + self::assertSame("default title", $document->querySelector("#container3 p span")->textContent); + } + public function testBindData_assocArray():void { $username = uniqid("user"); $email = uniqid() . "@example.com"; @@ -145,6 +174,24 @@ public function testBindData_assocArray():void { self::assertSame($category, $document->getElementById("dd3")->textContent); } + public function testBindData_assocArray_withNull():void { + $username = uniqid("user"); + $email = null; + $category = uniqid("category-"); + + $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_USER_PROFILE); + $sut = new DocumentBinder($document); + $sut->bindData([ + "username" => $username, + "email" => $email, + "category" => $category, + ]); + + self::assertSame($username, $document->getElementById("dd1")->textContent); + self::assertSame("you@example.com", $document->getElementById("dd2")->textContent); + self::assertSame($category, $document->getElementById("dd3")->textContent); + } + public function testBindData_indexedArray():void { $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_USER_PROFILE); $sut = new DocumentBinder($document); @@ -168,6 +215,21 @@ public function testBindData_object():void { self::assertSame($userObject->category, $document->getElementById("dd3")->textContent); } + public function testBindData_object_withNull():void { + $userObject = new StdClass(); + $userObject->username = "g105b"; + $userObject->email = "greg.bowler@g105b.com"; + $userObject->category = null; + + $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_USER_PROFILE); + $sut = new DocumentBinder($document); + $sut->bindData($userObject); + + self::assertSame($userObject->username, $document->getElementById("dd1")->textContent); + self::assertSame($userObject->email, $document->getElementById("dd2")->textContent); + self::assertSame("N/A", $document->getElementById("dd3")->textContent); + } + public function testBindData_indexArray_shouldThrowException():void { $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_USER_PROFILE); $sut = new DocumentBinder($document); @@ -339,6 +401,50 @@ public function testBindTable():void { } } + public function testBindTable_withNullData():void { + $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_TABLES); + $sut = new DocumentBinder($document); + + $tableData = [ + ["Name", "Position"], + ["Alan Statham", "Head of Radiology"], + ["Sue White", "Staff Liason Officer"], + ["Mac Macartney", null], + ["Joanna Clore", "HR"], + ["Caroline Todd", null], + ]; + + $exception = null; + /** @var HTMLTableElement $table */ + $table = $document->getElementById("tbl1"); + try { + $sut->bindTable($tableData, $table); + } + catch(Exception $exception) {} + self::assertNull($exception); + + foreach($tableData as $rowIndex => $rowData) { + /** @var HTMLTableRowElement $row */ + $row = $table->rows[$rowIndex]; + + foreach($rowData as $cellIndex => $cellValue) { + if(($rowIndex === 3 || $rowIndex === 5) + && $cellIndex === 1) { + self::assertSame( + "", + $row->cells[$cellIndex]->textContent + ); + } + else { + self::assertSame( + $cellValue, + $row->cells[$cellIndex]->textContent + ); + } + } + } + } + public function testBindKeyValue_tableData():void { $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_TABLES); $sut = new DocumentBinder($document); @@ -383,6 +489,25 @@ public function testBindList():void { } } + public function testBindList_nullData():void { + $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_LIST_TEMPLATE); + $sut = new DocumentBinder($document); + + $listData = ["One", null, "Three"]; + $sut->bindList($listData); + + $liElementList = $document->querySelectorAll("ul li"); + + foreach($listData as $i => $listItem) { + if(is_null($listItem)) { + self::assertSame("Template item!", $liElementList[$i]->textContent); + } + else { + self::assertSame($listItem, $liElementList[$i]->textContent); + } + } + } + public function testBindList_emptyLeavesNoWhiteSpace():void { $document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_LIST_TEMPLATE); $sut = new DocumentBinder($document);