diff --git a/composer.json b/composer.json index eec3516..056cbca 100644 --- a/composer.json +++ b/composer.json @@ -34,14 +34,14 @@ "scripts": { "phpunit": "vendor/bin/phpunit --configuration phpunit.xml", - "phpstan": "vendor/bin/phpstan analyse --level 6 src --memory-limit 256M", "phpcs": "vendor/bin/phpcs src --standard=phpcs.xml", "phpmd": "vendor/bin/phpmd src/ text phpmd.xml", + "phpstan": "vendor/bin/phpstan analyse --level 6 src --memory-limit 256M", "test": [ "@phpunit", - "@phpstan", "@phpcs", - "@phpmd" + "@phpmd", + "@phpstan" ] }, diff --git a/composer.lock b/composer.lock index 2513867..78da669 100644 --- a/composer.lock +++ b/composer.lock @@ -272,28 +272,29 @@ "packages-dev": [ { "name": "composer/pcre", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -331,7 +332,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -341,13 +342,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2026-06-07T11:47:49+00:00" }, { "name": "composer/xdebug-handler", @@ -417,20 +414,20 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.4", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.0" }, "conflict": { "doctrine/collections": "<1.6.8", @@ -465,32 +462,31 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" + "url": "https://github.com/mnapoli", + "type": "github" } ], - "time": "2025-08-01T08:46:24+00:00" + "time": "2026-08-11T10:17:44+00:00" }, { "name": "nikic/php-parser", - "version": "v5.7.0", + "version": "v5.8.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" @@ -529,9 +525,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" }, - "time": "2025-12-06T11:56:16+00:00" + "time": "2026-07-04T14:30:18+00:00" }, { "name": "pdepend/pdepend", @@ -799,11 +795,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.2.6", + "version": "2.2.9", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a6e9b5a9420f6109c091e87d82683bd1a80b87ed", - "reference": "a6e9b5a9420f6109c091e87d82683bd1a80b87ed", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/13d6b4f347bad222da436580c8304fa6f83e6bd0", + "reference": "13d6b4f347bad222da436580c8304fa6f83e6bd0", "shasum": "" }, "require": { @@ -859,7 +855,7 @@ "type": "github" } ], - "time": "2026-07-26T21:22:49+00:00" + "time": "2026-08-22T07:38:16+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1210,24 +1206,24 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.55", + "version": "11.5.56", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + "reference": "5f83edffa6967c3db468d48a695ec7bcb02e9256" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", - "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5f83edffa6967c3db468d48a695ec7bcb02e9256", + "reference": "5f83edffa6967c3db468d48a695ec7bcb02e9256", "shasum": "" }, "require": { "ext-dom": "*", + "ext-filter": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", - "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", @@ -1292,31 +1288,15 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.56" }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2026-02-18T12:37:06+00:00" + "time": "2026-07-06T14:52:39+00:00" }, { "name": "psr/container", @@ -2409,19 +2389,20 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "4.0.1", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "0525c73950de35ded110cffafb9892946d7771b5" + "reference": "bbdc3d0532623e21838b7041a4364383a8126f96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0525c73950de35ded110cffafb9892946d7771b5", - "reference": "0525c73950de35ded110cffafb9892946d7771b5", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/bbdc3d0532623e21838b7041a4364383a8126f96", + "reference": "bbdc3d0532623e21838b7041a4364383a8126f96", "shasum": "" }, "require": { + "ext-libxml": "*", "ext-simplexml": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", @@ -2430,6 +2411,10 @@ "require-dev": { "phpunit/phpunit": "^8.4.0 || ^9.3.4 || ^10.5.32 || 11.3.3 - 11.5.28 || ^11.5.31" }, + "suggest": { + "ext-iconv": "For accurate character length calculation when the checked files contain multi-byte characters.", + "ext-pcntl": "For parallel processing support via the --parallel CLI option." + }, "bin": [ "bin/phpcbf", "bin/phpcs" @@ -2484,7 +2469,7 @@ "type": "thanks_dev" } ], - "time": "2025-11-10T16:43:36+00:00" + "time": "2026-08-06T02:45:27+00:00" }, { "name": "staabm/side-effects-detector", @@ -2540,16 +2525,16 @@ }, { "name": "symfony/config", - "version": "v7.4.10", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57" + "reference": "696e12da8eea497a1a3808d714b43ff67a7df43e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", - "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", + "url": "https://api.github.com/repos/symfony/config/zipball/696e12da8eea497a1a3808d714b43ff67a7df43e", + "reference": "696e12da8eea497a1a3808d714b43ff67a7df43e", "shasum": "" }, "require": { @@ -2595,7 +2580,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.10" + "source": "https://github.com/symfony/config/tree/v7.4.17" }, "funding": [ { @@ -2615,20 +2600,20 @@ "type": "tidelift" } ], - "time": "2026-05-03T14:20:49+00:00" + "time": "2026-08-20T09:55:18+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.4.10", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d" + "reference": "f318ac9da5aba0be2cf43ecdd562c05a33bc11c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d", - "reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f318ac9da5aba0be2cf43ecdd562c05a33bc11c0", + "reference": "f318ac9da5aba0be2cf43ecdd562c05a33bc11c0", "shasum": "" }, "require": { @@ -2679,7 +2664,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.10" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.17" }, "funding": [ { @@ -2699,20 +2684,20 @@ "type": "tidelift" } ], - "time": "2026-05-06T11:55:30+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -2750,7 +2735,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -2770,20 +2755,20 @@ "type": "tidelift" } ], - "time": "2026-04-13T15:52:40+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/filesystem", - "version": "v7.4.11", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" + "reference": "ee7bc7bca4c7079b88e57d5000aeeb20df570c8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", - "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/ee7bc7bca4c7079b88e57d5000aeeb20df570c8d", + "reference": "ee7bc7bca4c7079b88e57d5000aeeb20df570c8d", "shasum": "" }, "require": { @@ -2820,7 +2805,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.11" + "source": "https://github.com/symfony/filesystem/tree/v7.4.17" }, "funding": [ { @@ -2840,7 +2825,7 @@ "type": "tidelift" } ], - "time": "2026-05-11T16:38:44+00:00" + "time": "2026-08-21T12:09:28+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2927,16 +2912,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.37.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -2988,7 +2973,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -3008,20 +2993,20 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", "shasum": "" }, "require": { @@ -3075,7 +3060,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" }, "funding": [ { @@ -3095,20 +3080,20 @@ "type": "tidelift" } ], - "time": "2026-03-28T09:44:51+00:00" + "time": "2026-06-16T09:55:08+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.4.9", + "version": "v7.4.16", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "22e03a49c95ef054a43601cd159b222bfab1c701" + "reference": "ca31404415670aa3834809005b529df1b84f0790" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/22e03a49c95ef054a43601cd159b222bfab1c701", - "reference": "22e03a49c95ef054a43601cd159b222bfab1c701", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/ca31404415670aa3834809005b529df1b84f0790", + "reference": "ca31404415670aa3834809005b529df1b84f0790", "shasum": "" }, "require": { @@ -3156,7 +3141,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.9" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.16" }, "funding": [ { @@ -3176,7 +3161,7 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:18:21+00:00" + "time": "2026-07-30T12:37:26+00:00" }, { "name": "theseer/tokenizer", diff --git a/src/DuplicateListElementNameException.php b/src/DuplicateListElementNameException.php new file mode 100644 index 0000000..3173c4a --- /dev/null +++ b/src/DuplicateListElementNameException.php @@ -0,0 +1,4 @@ + */ private array $elementKVP; + /** @var array> */ + private array $namedElementKVP; public function __construct( Document $document ) { $this->elementKVP = []; + $this->namedElementKVP = []; $this->extractTemplates($document); } @@ -25,13 +28,7 @@ public function get( } if($templateName) { - if(!isset($this->elementKVP[$templateName])) { - throw new ListElementNotFoundInContextException( - "List element with name \"$templateName\" can not be " - . "found within the context $context->tagName element." - ); - } - return $this->elementKVP[$templateName]; + return $this->findNamedMatch($context, $templateName); } return $this->findMatch($context); @@ -43,22 +40,67 @@ private function extractTemplates(Document $document):void { foreach($document->querySelectorAll("[data-list],[data-template]") as $element) { $templateElement = new ListElement($element); $nodePath = (string)(new NodePathCalculator($element)); - $key = $templateElement->getListItemName() ?? $nodePath; - $dataTemplateArray[$key] = $templateElement; + $dataTemplateArray[] = [ + "path" => $nodePath, + "template" => $templateElement, + ]; } - uksort( + usort( $dataTemplateArray, - fn(string $a, string $b):int => substr_count($a, "/") > substr_count($b, "/") + fn(array $a, array $b):int => substr_count($a["path"], "/") > substr_count($b["path"], "/") ? -1 : 1 ); - foreach($dataTemplateArray as $template) { + foreach($dataTemplateArray as ["template" => $template]) { $template->removeOriginalElement(); } - $this->elementKVP = array_reverse($dataTemplateArray, true); + foreach(array_reverse($dataTemplateArray) as $templateData) { + $template = $templateData["template"]; + if($name = $template->getListItemName()) { + $this->namedElementKVP[$name][] = $template; + } + else { + $this->elementKVP[$templateData["path"]] = $template; + } + } + } + + private function findNamedMatch( + Element $context, + string $templateName, + ):ListElement { + $matchedElementArray = []; + foreach($this->namedElementKVP[$templateName] ?? [] as $element) { + try { + $listItemParent = $element->getListItemParent(); + } + catch(Throwable) { + continue; + } + + if($listItemParent === $context || $context->contains($listItemParent)) { + $matchedElementArray[] = $element; + } + } + + if(count($matchedElementArray) > 1) { + throw new DuplicateListElementNameException( + "More than one list element with name \"$templateName\" " + . "was found within the context $context->tagName element." + ); + } + + if($matchedElementArray) { + return $matchedElementArray[0]; + } + + throw new ListElementNotFoundInContextException( + "List element with name \"$templateName\" can not be " + . "found within the context $context->tagName element." + ); } private function findMatch(Element $context):ListElement { diff --git a/test/phpunit/ListElementCollectionTest.php b/test/phpunit/ListElementCollectionTest.php index e4802cc..8996668 100644 --- a/test/phpunit/ListElementCollectionTest.php +++ b/test/phpunit/ListElementCollectionTest.php @@ -3,6 +3,7 @@ use Gt\Dom\HTMLDocument; use GT\DomTemplate\BindableCache; +use GT\DomTemplate\DuplicateListElementNameException; use GT\DomTemplate\ElementBinder; use GT\DomTemplate\HTMLAttributeBinder; use GT\DomTemplate\HTMLAttributeCollection; @@ -70,6 +71,60 @@ public function testGet_name():void { ); } + public function testGet_name_duplicateWithinContextThrows():void { + $document = new HTMLDocument(<< + + + + + + HTML); + $sut = new ListElementCollection($document); + + self::expectException(DuplicateListElementNameException::class); + self::expectExceptionMessage('item'); + $sut->get($document->getElementById("items"), "item"); + } + + public function testGet_name_duplicateInDifferentContextsResolvesWithinContext():void { + $document = new HTMLDocument(<< + + + + + + + HTML); + $sut = new ListElementCollection($document); + + $firstListItem = $sut->get( + $document->getElementById("first-list"), + "item" + ); + $secondListItem = $sut->get( + $document->getElementById("second-list"), + "item" + ); + + self::assertSame( + $document->getElementById("first-list"), + $firstListItem->getListItemParent() + ); + self::assertSame( + $document->getElementById("second-list"), + $secondListItem->getListItemParent() + ); + } + /** * Baby steps...(this comment was written as part of DomTemplate's TDD). * Instead of jumping into the implementation of recursive nested list