Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component binder #472

Merged
merged 37 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
00d1189
build: upgrade dom requirement and loosen version range
g105b Jul 24, 2022
3fcbcb9
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Aug 14, 2022
2a79afb
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Aug 18, 2022
5d58fc4
docs: update examples
g105b Aug 18, 2022
e3957a8
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Aug 26, 2022
73d0b85
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Sep 21, 2022
3cbb825
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Oct 5, 2022
fadbba7
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Oct 8, 2022
5403158
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Oct 31, 2022
224999b
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Jan 10, 2023
3def753
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Jan 17, 2023
3dbda21
feature: trim whitespace when there are only template children
g105b Jan 17, 2023
a28c12c
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Jan 17, 2023
da35c48
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Jan 23, 2023
b2f8fa5
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Jan 26, 2023
2f51576
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Jan 30, 2023
9655705
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Jan 31, 2023
7745df0
maintenance: phpstorm analysis improvements
g105b Jan 31, 2023
5ab8d14
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Feb 15, 2023
14029b2
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Feb 15, 2023
fa5c45b
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Mar 2, 2023
45007a4
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Jul 19, 2023
b47f5b2
tweak: remove data-element attribute
g105b Jul 19, 2023
5f0bbdf
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Jul 20, 2023
28f4cee
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Jul 28, 2023
9c98cd8
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Oct 12, 2023
acfca16
Merge branch 'master' of github.com:/PhpGt/DomTemplate
g105b Oct 25, 2023
659fb6f
feature: implement ComponentBinder and abstract Binder class
g105b Oct 25, 2023
3b3a883
fix: bindable cache allows nested nullable objects
g105b Oct 27, 2023
cbfdff3
refactor: all domtemplate classes set their dependencies outside of t…
g105b Oct 27, 2023
818de59
tweak: stricter reflection type checking
g105b Oct 27, 2023
edbfd6b
wip: better exception on missing list element
g105b Nov 1, 2023
53f767b
tweak: handle nullable iterables
g105b Nov 3, 2023
e32c049
tweak: handle binding of outer list item
g105b Nov 10, 2023
5e89fc7
tidy: improve types and coverage
g105b Nov 10, 2023
358bfbf
ci: php 8.2
g105b Nov 10, 2023
d4c189e
ci: phpstan level 6
g105b Nov 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Composer install
uses: php-actions/composer@v6
with:
php_version: 8.1
php_version: 8.2

- name: Archive build
run: mkdir /tmp/github-actions/ && tar -cvf /tmp/github-actions/build.tar ./
Expand Down Expand Up @@ -50,7 +50,7 @@ jobs:
env:
XDEBUG_MODE: cover
with:
php_version: 8.1
php_version: 8.2
php_extensions: xdebug
configuration: test/phpunit/phpunit.xml
bootstrap: vendor/autoload.php
Expand Down Expand Up @@ -97,7 +97,9 @@ jobs:
- name: PHP Static Analysis
uses: php-actions/phpstan@v3
with:
php_version: 8.2
path: src/
level: 6

remove_old_artifacts:
runs-on: ubuntu-latest
Expand Down
46 changes: 34 additions & 12 deletions src/BindableCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use ReflectionNamedType;
use ReflectionObject;
use ReflectionProperty;
use ReflectionType;
use stdClass;
use Stringable;

Expand Down Expand Up @@ -69,8 +70,7 @@ public function isBindable(object $object):bool {

foreach($refAttributes as $refAttr) {
$bindKey = $this->getBindKey($refAttr, $refMethod);
$attributeCache[$bindKey] = fn(object $object):null|iterable|string
=> $this->nullableStringOrIterable($object->$methodName());
$attributeCache[$bindKey] = fn(object $object):null|iterable|string => $this->nullableStringOrIterable($object->$methodName());
if(class_exists($refReturnName)) {
$cacheObjectKeys[$bindKey] = $refReturnName;
}
Expand All @@ -91,11 +91,17 @@ public function isBindable(object $object):bool {
elseif($refProp->isPublic()) {
$bindKey = $propName;

/** @var ?ReflectionNamedType $refType */
$refTypeName = null;
$refType = $refProp->getType();
$refTypeName = $refType?->getName();
$attributeCache[$bindKey]
= fn(object $object, $key):null|iterable|string => isset($object->$key) ? $this->nullableStringOrIterable($object->$key) : null;
if($refType instanceof ReflectionNamedType) {
$refTypeName = $refType->getName();
}
$attributeCache[$bindKey] =
fn(object $object, $key):null|iterable|string =>
isset($object->$key)
? $this->nullableStringOrIterable($object->$key)
: null;

if(class_exists($refTypeName)) {
$cacheObjectKeys[$bindKey] = $refTypeName;
}
Expand Down Expand Up @@ -143,9 +149,15 @@ private function expandObjects(array $cache, array $objectKeys):array {
return $cache;
}

/** @return array<string, string> */
public function convertToKvp(object $object):array {
/**
* @param object|array<string, string> $object
* @return array<string, string>
*/
public function convertToKvp(object|array $object):array {
$kvp = [];
if(is_array($object)) {
return $object;
}

if($object instanceof stdClass) {
foreach(get_object_vars($object) as $key => $value) {
Expand Down Expand Up @@ -178,10 +190,21 @@ public function convertToKvp(object $object):array {
// TODO: This "get*()" function should not be hard coded here - it should load the appropriate
// Bind/BindGetter by matching the correct Attribute.
$bindFunc = "get" . ucfirst($propName);
$objectToExtract = $objectToExtract->$propName ?? $objectToExtract->$bindFunc();
if($objectToExtract) {
if(property_exists($objectToExtract, $propName)) {
$objectToExtract = $objectToExtract->$propName;
}
elseif(method_exists($objectToExtract, $bindFunc)) {
$objectToExtract = $objectToExtract->$bindFunc();
}
}
}

$value = null;
if($objectToExtract) {
$value = $closure($objectToExtract, $deepestKey);
}

$value = $closure($objectToExtract, $deepestKey);
if(is_null($value)) {
$kvp[$key] = null;
}
Expand All @@ -200,8 +223,7 @@ public function convertToKvp(object $object):array {
private function getBindAttributes(ReflectionMethod|ReflectionProperty $ref):array {
return array_filter(
$ref->getAttributes(),
fn(ReflectionAttribute $refAttr) =>
$refAttr->getName() === Bind::class
fn(ReflectionAttribute $refAttr) => $refAttr->getName() === Bind::class
|| $refAttr->getName() === BindGetter::class
);
}
Expand Down
53 changes: 53 additions & 0 deletions src/Binder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php
namespace Gt\DomTemplate;

use Gt\Dom\Element;

abstract class Binder {
abstract public function bindValue(
mixed $value,
?Element $context = null
):void;

/**
* Applies the string value of $value to any elements within $context
* that have the data-bind attribute matching the provided key.
*/
abstract public function bindKeyValue(
string $key,
mixed $value,
?Element $context = null
):void;

/**
* Binds multiple key-value-pairs to any matching elements within
* the $context element.
*/
abstract public function bindData(
mixed $kvp,
?Element $context = null
):void;

abstract public function bindTable(
mixed $tableData,
?Element $context = null,
?string $bindKey = null
):void;

/**
* @param iterable<int, mixed> $listData
*/
abstract public function bindList(
iterable $listData,
?Element $context = null,
?string $templateName = null
):int;

/** @param iterable<int, mixed> $listData */
abstract public function bindListCallback(
iterable $listData,
callable $callback,
?Element $context = null,
?string $templateName = null
):int;
}
4 changes: 3 additions & 1 deletion src/CommentIni.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ public function __construct(
$data = trim($commentNode->data);

try {
$ini = parse_ini_string($data, true);
// We know that sometimes this data will not be correct ini format, and it might actually be a textual comment.
// Therefore, we must suppress the warning that is emitted by parse_ini_string:
$ini = @parse_ini_string($data, true);
$commentNodeToRemove = $commentNode;
}
catch(Throwable) {
Expand Down
56 changes: 56 additions & 0 deletions src/ComponentBinder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
namespace Gt\DomTemplate;

use Gt\Dom\Document;
use Gt\Dom\Element;

class ComponentBinder extends DocumentBinder {
private Element $componentElement;

public function setComponentBinderDependencies(Element $componentElement):void {
$this->componentElement = $componentElement;
}

public function bindList(
iterable $listData,
?Element $context = null,
?string $templateName = null
):int {
if($context) {
$this->checkElementContainedWithinComponent($context);
}
else {
$context = $this->componentElement;
}

return parent::bindList($listData, $context, $templateName);
}

protected function bind(
?string $key,
mixed $value,
?Element $context = null
):void {
if($context) {
$this->checkElementContainedWithinComponent($context);
}
else {
$context = $this->componentElement;
}

if(is_callable($value) && !is_string($value)) {
$value = call_user_func($value);
}

$this->elementBinder->bind($key, $value, $context);
$this->placeholderBinder->bind($key, $value, $context);
}

private function checkElementContainedWithinComponent(Element $context):void {
if($this->componentElement !== $context && !$this->componentElement->contains($context)) {
throw new ComponentDoesNotContainContextException(
"<{$this->componentElement->tagName}> does not contain requested <$context->tagName>."
);
}
}
}
4 changes: 4 additions & 0 deletions src/ComponentDoesNotContainContextException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
namespace Gt\DomTemplate;

class ComponentDoesNotContainContextException extends DomTemplateException {}
62 changes: 26 additions & 36 deletions src/DocumentBinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,32 @@
use Gt\Dom\Document;
use Gt\Dom\Element;

class DocumentBinder {
private ElementBinder $elementBinder;
private PlaceholderBinder $placeholderBinder;
private TableBinder $tableBinder;
private ListBinder $listBinder;
private ListElementCollection $templateCollection;
private BindableCache $bindableCache;
class DocumentBinder extends Binder {
protected ElementBinder $elementBinder;
protected PlaceholderBinder $placeholderBinder;
protected TableBinder $tableBinder;
protected ListBinder $listBinder;
protected ListElementCollection $templateCollection;
protected BindableCache $bindableCache;

/**
* @param array<string, string> $config
*/
public function __construct(
private readonly Document $document,
private array $config = [],
?ElementBinder $elementBinder = null,
?PlaceholderBinder $placeholderBinder = null,
?TableBinder $tableBinder = null,
?ListBinder $listBinder = null,
?ListElementCollection $templateCollection = null,
?BindableCache $bindableCache = null
) {
$this->templateCollection = $templateCollection ?? new ListElementCollection($document);
$this->elementBinder = $elementBinder ?? new ElementBinder();
$this->placeholderBinder = $placeholderBinder ?? new PlaceholderBinder();
$this->tableBinder = $tableBinder ?? new TableBinder($this->templateCollection);
$this->listBinder = $listBinder ?? new ListBinder($this->templateCollection);
$this->bindableCache = $bindableCache ?? new BindableCache();

// This is temporary, to suppress PHPStan's complaints for declaring a variable
// without using it. There are plans to use the config variable, but it is
// currently not yet used, and this technique prevents the constructor
// parameters from changing over time.
if(!$this->config) {
$this->config = [];
}
protected readonly Document $document,
) {}

public function setDependencies(
ElementBinder $elementBinder,
PlaceholderBinder $placeholderBinder,
TableBinder $tableBinder,
ListBinder $listBinder,
ListElementCollection $listElementCollection,
BindableCache $bindableCache,
):void {
$this->elementBinder = $elementBinder;
$this->placeholderBinder = $placeholderBinder;
$this->tableBinder = $tableBinder;
$this->listBinder = $listBinder;
$this->templateCollection = $listElementCollection;
$this->bindableCache = $bindableCache;
}

/**
Expand Down Expand Up @@ -87,7 +78,7 @@ public function bindData(
}
}

foreach($kvp as $key => $value) {
foreach($kvp ?? [] as $key => $value) {
$this->bindKeyValue($key, $value, $context);
}
}
Expand Down Expand Up @@ -163,7 +154,7 @@ public function cleanupDocument():void {
}
}

private function bind(
protected function bind(
?string $key,
mixed $value,
?Element $context = null
Expand All @@ -177,7 +168,6 @@ private function bind(
}

$this->elementBinder->bind($key, $value, $context);
$this->placeholderBinder->bind($key, $value, $context);
}

private function isIndexedArray(mixed $data):bool {
Expand Down
16 changes: 8 additions & 8 deletions src/ElementBinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ class ElementBinder {
private HTMLAttributeCollection $htmlAttributeCollection;
private PlaceholderBinder $placeholderBinder;

public function __construct(
?HTMLAttributeBinder $htmlAttributeBinder = null,
?HTMLAttributeCollection $htmlAttributeCollection = null,
?PlaceholderBinder $placeholderBinder = null,
) {
$this->htmlAttributeBinder = $htmlAttributeBinder ?? new HTMLAttributeBinder();
$this->htmlAttributeCollection = $htmlAttributeCollection ?? new HTMLAttributeCollection();
$this->placeholderBinder = $placeholderBinder ?? new PlaceholderBinder();
public function setDependencies(
HTMLAttributeBinder $htmlAttributeBinder,
HTMLAttributeCollection $htmlAttributeCollection,
PlaceholderBinder $placeholderBinder,
):void {
$this->htmlAttributeBinder = $htmlAttributeBinder;
$this->htmlAttributeCollection = $htmlAttributeCollection;
$this->placeholderBinder = $placeholderBinder;
}

/**
Expand Down
10 changes: 7 additions & 3 deletions src/HTMLAttributeBinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@
use Gt\Dom\Element;

class HTMLAttributeBinder {
private ListBinder $listBinder;
private TableBinder $tableBinder;

public function setDependencies(ListBinder $listBinder, TableBinder $tableBinder):void {
$this->listBinder = $listBinder;
$this->tableBinder = $tableBinder;
}

public function bind(
?string $key,
mixed $value,
Expand Down Expand Up @@ -202,9 +208,6 @@ private function setBindProperty(
break;

case "table":
if(!isset($this->tableBinder)) {
$this->tableBinder = new TableBinder();
}
$this->tableBinder->bindTableData(
$bindValue,
$element,
Expand All @@ -217,6 +220,7 @@ private function setBindProperty(
break;

case "list";
$this->listBinder->bindListData($bindValue, $element);
break;

default:
Expand Down
Loading